mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-28 21:55:42 +00:00
Compare commits
512
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0693e1ff3 | ||
|
|
3f6d32ba69 | ||
|
|
c6336979c8 | ||
|
|
a7f630d89c | ||
|
|
fe18ffe9a3 | ||
|
|
f7ee55b888 | ||
|
|
d94f731c50 | ||
|
|
791d0b9b29 | ||
|
|
80417fd3bf | ||
|
|
4c25b16cf1 | ||
|
|
6165bb695e | ||
|
|
65501b046e | ||
|
|
0419dc23fc | ||
|
|
4df0e75137 | ||
|
|
c35126c6d1 | ||
|
|
809c1e256a | ||
|
|
a7990f65aa | ||
|
|
39a374fc42 | ||
|
|
0f72494fef | ||
|
|
9dba9deaa1 | ||
|
|
6ff2a91417 | ||
|
|
3add561a82 | ||
|
|
a7c8cb11fc | ||
|
|
865d974a51 | ||
|
|
4898a1fe80 | ||
|
|
3b819db9ee | ||
|
|
154702ad35 | ||
|
|
98eb64749f | ||
|
|
067aadfec1 | ||
|
|
80d17ee750 | ||
|
|
4e59580d5e | ||
|
|
4950bd3609 | ||
|
|
f913116132 | ||
|
|
82fc200d51 | ||
|
|
af1e467cb8 | ||
|
|
c7d5f36ad5 | ||
|
|
78e5d25b89 | ||
|
|
f672126a00 | ||
|
|
4ff32ec0b2 | ||
|
|
ecc61c7d2c | ||
|
|
49185c103a | ||
|
|
71a6c9f791 | ||
|
|
6a91935a1f | ||
|
|
a148a8a2a3 | ||
|
|
a588c2dd9c | ||
|
|
6ae78186e2 | ||
|
|
a9e51e619d | ||
|
|
912f53c42d | ||
|
|
e371368e0b | ||
|
|
9fa1cb9e5d | ||
|
|
16a52d5557 | ||
|
|
2641be9c91 | ||
|
|
132cf36673 | ||
|
|
9b42eb90a0 | ||
|
|
e07b077b7f | ||
|
|
0766b63b9e | ||
|
|
46400dd6e1 | ||
|
|
85b2fe39aa | ||
|
|
5e9109a523 | ||
|
|
2785015212 | ||
|
|
e5be07b3b8 | ||
|
|
68873cc219 | ||
|
|
54283cbeb1 | ||
|
|
d30dd07c28 | ||
|
|
34c3b244fd | ||
|
|
07163dec15 | ||
|
|
c7c7167b6a | ||
|
|
e6bac582b1 | ||
|
|
e406229055 | ||
|
|
b0d1a8181b | ||
|
|
422abdb208 | ||
|
|
b3614f70b8 | ||
|
|
18bcc871e5 | ||
|
|
2b6d5e8258 | ||
|
|
62d8cc08ab | ||
|
|
3bb4cd84dc | ||
|
|
d17bb209a5 | ||
|
|
20bf47ab01 | ||
|
|
741bc690fb | ||
|
|
bedc104d0b | ||
|
|
6410ad2eef | ||
|
|
8164e91125 | ||
|
|
70f40aea25 | ||
|
|
aec99378ad | ||
|
|
cd2e00cd47 | ||
|
|
504374291d | ||
|
|
6b052bf3ad | ||
|
|
0e1536842e | ||
|
|
c6b73d24be | ||
|
|
ccde86cb81 | ||
|
|
8d54d80f4d | ||
|
|
5c1d68960b | ||
|
|
4a33d4b30f | ||
|
|
5d21ee0915 | ||
|
|
ce82c952c5 | ||
|
|
3b5deebb92 | ||
|
|
027c5b6598 | ||
|
|
8a29847cc4 | ||
|
|
33f4c41fdd | ||
|
|
f7d79639b5 | ||
|
|
da0d1c04fd | ||
|
|
0c0ff85513 | ||
|
|
c2c18d8a07 | ||
|
|
5e7223aa8e | ||
|
|
14a475032e | ||
|
|
3dce099b2a | ||
|
|
009a4ffe53 | ||
|
|
7781bbcc46 | ||
|
|
e6438302e2 | ||
|
|
a919e8edf0 | ||
|
|
a59b732b04 | ||
|
|
077e0d2bc3 | ||
|
|
3ad7c3e567 | ||
|
|
62fd16cec5 | ||
|
|
89d1d15979 | ||
|
|
1bea610232 | ||
|
|
8cce072614 | ||
|
|
fb74935ad9 | ||
|
|
36915b8184 | ||
|
|
e07cdc8123 | ||
|
|
fe4e205a07 | ||
|
|
4506ea2672 | ||
|
|
ba69263f06 | ||
|
|
2a660f05d7 | ||
|
|
4dd30a9863 | ||
|
|
1f18f3296d | ||
|
|
467d2e2c82 | ||
|
|
73b8087e07 | ||
|
|
76be4a49ea | ||
|
|
27aae790ad | ||
|
|
fdf7f2a88d | ||
|
|
f1cf03e49e | ||
|
|
5213fb4805 | ||
|
|
a884b60991 | ||
|
|
1fdc679dfd | ||
|
|
afabe092c2 | ||
|
|
3624279c4e | ||
|
|
eca0289a43 | ||
|
|
90bedd4be5 | ||
|
|
2231cf1b9b | ||
|
|
30a68bdca0 | ||
|
|
4f69ea79b4 | ||
|
|
665d2291ef | ||
|
|
a6ab25fcee | ||
|
|
e2a82f7225 | ||
|
|
5e5244ead4 | ||
|
|
74d3af589e | ||
|
|
a6641d5eb1 | ||
|
|
3ecb665709 | ||
|
|
9b033230c9 | ||
|
|
bc813b8659 | ||
|
|
0fe61c71c9 | ||
|
|
315c1daf82 | ||
|
|
45abe81540 | ||
|
|
8141770268 | ||
|
|
fa7775a023 | ||
|
|
b3bf291a7d | ||
|
|
557669546b | ||
|
|
42958cb34e | ||
|
|
91f02da9cf | ||
|
|
8d8b55e2c4 | ||
|
|
947cedc0f0 | ||
|
|
4e5206220c | ||
|
|
acd299c346 | ||
|
|
74b816d3f4 | ||
|
|
4abdefd40d | ||
|
|
08f7e4ebfc | ||
|
|
afbf4c226c | ||
|
|
0afa842efe | ||
|
|
e78ae6ad75 | ||
|
|
9631435dea | ||
|
|
5ae910b19b | ||
|
|
dc9b1171fb | ||
|
|
508eb64bfb | ||
|
|
9aa7be4620 | ||
|
|
f3d887e370 | ||
|
|
ce4ed6a305 | ||
|
|
2b5e4aa96e | ||
|
|
41b9f5526c | ||
|
|
6b24776e0a | ||
|
|
b1b44ebb6c | ||
|
|
45a2d50b57 | ||
|
|
84f329ab80 | ||
|
|
fcf9246562 | ||
|
|
0d361f5ad7 | ||
|
|
217d495747 | ||
|
|
ca80ad0057 | ||
|
|
c49c4d26e1 | ||
|
|
68906df5b9 | ||
|
|
dc54584fa3 | ||
|
|
d665eb53f3 | ||
|
|
59143a2e04 | ||
|
|
f9825861e3 | ||
|
|
9b5dcc40a0 | ||
|
|
44514fb2f2 | ||
|
|
3014602331 | ||
|
|
bb182efc3e | ||
|
|
3a04d865b6 | ||
|
|
f2f81d1cda | ||
|
|
72dc8ba9fb | ||
|
|
7adc38fe12 | ||
|
|
67dbf928d6 | ||
|
|
efad456bef | ||
|
|
9ebc517c9f | ||
|
|
49020af5e9 | ||
|
|
e5e5dd9fdc | ||
|
|
0dd780fc63 | ||
|
|
ae4c441427 | ||
|
|
f01a2ee378 | ||
|
|
e4901b6414 | ||
|
|
f0b2fd5f5e | ||
|
|
a28a0ef8d5 | ||
|
|
88ff742bb1 | ||
|
|
d549c0642b | ||
|
|
00c677d12a | ||
|
|
6c47019e8e | ||
|
|
f7932c6681 | ||
|
|
13e734d9ae | ||
|
|
a4f934105f | ||
|
|
1300ec9ab9 | ||
|
|
bfba377c57 | ||
|
|
56564c1bee | ||
|
|
1728d719a4 | ||
|
|
3314ef191f | ||
|
|
1fbd2e8438 | ||
|
|
7f50b61419 | ||
|
|
c7dea02861 | ||
|
|
afd8028665 | ||
|
|
2263977668 | ||
|
|
4cfec68ef8 | ||
|
|
432276f550 | ||
|
|
ef179e67ef | ||
|
|
7ee9fbb8da | ||
|
|
3d46bee7dd | ||
|
|
c6dc5477f5 | ||
|
|
ab7499ca7a | ||
|
|
7d95c81a00 | ||
|
|
e523ebf96e | ||
|
|
b740e1ad59 | ||
|
|
3a612426f6 | ||
|
|
452d0cac5a | ||
|
|
bdb7b324c4 | ||
|
|
145f7e9f9c | ||
|
|
09e5f4801d | ||
|
|
2aa5fa181a | ||
|
|
f438c4bbe2 | ||
|
|
66ecc25075 | ||
|
|
562d32d296 | ||
|
|
bdf9b09b26 | ||
|
|
d2559ff67c | ||
|
|
88433fbc18 | ||
|
|
9426488913 | ||
|
|
81395a9467 | ||
|
|
fb4e0c1bac | ||
|
|
94b273ff60 | ||
|
|
b179e250dd | ||
|
|
b3e35b60c0 | ||
|
|
3a0fa9e59c | ||
|
|
cdf7c5649b | ||
|
|
743737555e | ||
|
|
94f2366af3 | ||
|
|
5d4399cad6 | ||
|
|
011562a2d6 | ||
|
|
781c7a8c3d | ||
|
|
bce1ddaf81 | ||
|
|
d734c591a8 | ||
|
|
c5aca2666c | ||
|
|
6e261ef7b5 | ||
|
|
71240cd9ed | ||
|
|
15d7d30ccf | ||
|
|
f52d54b7c2 | ||
|
|
6314d3e4e3 | ||
|
|
3f54abc3c0 | ||
|
|
0e126faee2 | ||
|
|
763ab63860 | ||
|
|
38a15fdf9f | ||
|
|
b0446d99e4 | ||
|
|
2f6a58255b | ||
|
|
a4257efc0e | ||
|
|
b155494b4d | ||
|
|
04a51c9fb6 | ||
|
|
ee5cf07db3 | ||
|
|
5c4470b090 | ||
|
|
86171cc1ae | ||
|
|
d5da95c5e3 | ||
|
|
23c7385d8f | ||
|
|
cc5b6dbc75 | ||
|
|
025bcbbff6 | ||
|
|
fa6133f3ee | ||
|
|
af35d42792 | ||
|
|
d3f99ccada | ||
|
|
f4e3944716 | ||
|
|
c67dc76ed5 | ||
|
|
8bd9342e68 | ||
|
|
55c55adf8a | ||
|
|
3f6fee72b5 | ||
|
|
35d441d7f6 | ||
|
|
ddb49f411b | ||
|
|
130c0a5f37 | ||
|
|
9c3eb2d6e7 | ||
|
|
a84154bc74 | ||
|
|
08a5dc6104 | ||
|
|
33d963488b | ||
|
|
2c3135a493 | ||
|
|
b3b69474e4 | ||
|
|
ee74455b57 | ||
|
|
bd7378a971 | ||
|
|
588fd4217a | ||
|
|
1a60af2cd2 | ||
|
|
8398da469d | ||
|
|
f7280d37a6 | ||
|
|
1e98f4e3e8 | ||
|
|
3b203ac1b2 | ||
|
|
0ac3bd1e6e | ||
|
|
b56df59977 | ||
|
|
441841ec41 | ||
|
|
b8ebd1d234 | ||
|
|
c1d9c53b30 | ||
|
|
b4fbc7770a | ||
|
|
065d5b466a | ||
|
|
59a28f75e5 | ||
|
|
86dc430ebc | ||
|
|
a062d6e8b2 | ||
|
|
04425edc34 | ||
|
|
5a622b276a | ||
|
|
9c809d6453 | ||
|
|
2838e8b280 | ||
|
|
3b03f3384a | ||
|
|
6c219a6d43 | ||
|
|
062c19dacf | ||
|
|
82b9c0f7df | ||
|
|
13cc93345a | ||
|
|
a5652e43f2 | ||
|
|
09237eb9b1 | ||
|
|
b15a9f0878 | ||
|
|
a478906805 | ||
|
|
9619cb8076 | ||
|
|
588bcbd81f | ||
|
|
16cacb9ee8 | ||
|
|
150970fcae | ||
|
|
de6ccb4345 | ||
|
|
ab64052fa7 | ||
|
|
38129c2a8e | ||
|
|
2fd16d962c | ||
|
|
4dcb4bacdb | ||
|
|
e1dd455af3 | ||
|
|
1cd7474864 | ||
|
|
c1c60c406a | ||
|
|
b20c8e55db | ||
|
|
ca485e2c05 | ||
|
|
1f44105ac4 | ||
|
|
b200d6d631 | ||
|
|
5ca66b9090 | ||
|
|
a714271ef2 | ||
|
|
21a311e9eb | ||
|
|
4b7a26b12b | ||
|
|
fb35514410 | ||
|
|
88e9c46fce | ||
|
|
5a8c791e4c | ||
|
|
f80718719c | ||
|
|
8f1371dd61 | ||
|
|
91244dd06d | ||
|
|
5c8811441c | ||
|
|
090d440501 | ||
|
|
e123c0fade | ||
|
|
da89636165 | ||
|
|
3eb2607a7a | ||
|
|
51be13b8b1 | ||
|
|
90d1cf702f | ||
|
|
6a0162109b | ||
|
|
25e1fd4f90 | ||
|
|
8e30daa63b | ||
|
|
311cf6e262 | ||
|
|
43ee2bfd1f | ||
|
|
c358567a3f | ||
|
|
b142acbf3f | ||
|
|
5875062bd3 | ||
|
|
20cf458c61 | ||
|
|
72c392a2dc | ||
|
|
bc17291691 | ||
|
|
c2438e093f | ||
|
|
93b639e452 | ||
|
|
0adf8b3765 | ||
|
|
931ae4b926 | ||
|
|
59d000f792 | ||
|
|
a5348a54ee | ||
|
|
bb777da84a | ||
|
|
06c76daba2 | ||
|
|
ecd0d3605b | ||
|
|
d1160855d4 | ||
|
|
700f090331 | ||
|
|
390bdcb77f | ||
|
|
138b00fdc4 | ||
|
|
d169a38761 | ||
|
|
3c9e01f4b8 | ||
|
|
610f32ae5c | ||
|
|
e84d608638 | ||
|
|
349e38cad8 | ||
|
|
90796d8932 | ||
|
|
d5ac8d0ff1 | ||
|
|
7df9a7f074 | ||
|
|
b56665b65b | ||
|
|
6891fe2fce | ||
|
|
dee7724ec2 | ||
|
|
caca9e4f6a | ||
|
|
fb34a6f3d1 | ||
|
|
2b65a547a1 | ||
|
|
5fd3adcf80 | ||
|
|
14e8343eaa | ||
|
|
15b870152a | ||
|
|
df84625b15 | ||
|
|
7b41d8f4f9 | ||
|
|
b67f6b3ceb | ||
|
|
e839a60270 | ||
|
|
2db68d61f2 | ||
|
|
455d6c1aff | ||
|
|
1b6df9fa62 | ||
|
|
74052faecf | ||
|
|
aaa890ed8a | ||
|
|
775b0a915b | ||
|
|
c489ae1511 | ||
|
|
6274ba4195 | ||
|
|
09a2f0bf30 | ||
|
|
0f89f7a2ea | ||
|
|
29c1d4a232 | ||
|
|
043166c35b | ||
|
|
a69cee40d3 | ||
|
|
f6192d408e | ||
|
|
688fea67af | ||
|
|
73748072b5 | ||
|
|
92e7482c92 | ||
|
|
9e47778bdd | ||
|
|
8bbf0ba6d5 | ||
|
|
2e2f24596a | ||
|
|
7f2acd73ad | ||
|
|
4a081a691d | ||
|
|
bf60a59fbd | ||
|
|
d08fe34434 | ||
|
|
31904a2bb4 | ||
|
|
0b870de473 | ||
|
|
0ac81eadec | ||
|
|
0aa7edbb06 | ||
|
|
0950fecb2e | ||
|
|
2eace68955 | ||
|
|
c048b8ed2f | ||
|
|
83de80c73d | ||
|
|
9d83e48371 | ||
|
|
f02ea4b1ad | ||
|
|
b56607cfb6 | ||
|
|
caf21766a9 | ||
|
|
c6cfda11ec | ||
|
|
cf2071f3b1 | ||
|
|
38a73e0de3 | ||
|
|
3613949d7e | ||
|
|
e26e18670b | ||
|
|
a22fad2487 | ||
|
|
53c84e030e | ||
|
|
e7285b6000 | ||
|
|
efc518a74a | ||
|
|
483657d520 | ||
|
|
af1eb2113a | ||
|
|
0b52492d92 | ||
|
|
fdb1e9b338 | ||
|
|
cef5a16f05 | ||
|
|
d973746917 | ||
|
|
9063fe0468 | ||
|
|
20e8063185 | ||
|
|
482634a7c7 | ||
|
|
b2f02dfdcd | ||
|
|
36ddd24ce1 | ||
|
|
7b9d01678d | ||
|
|
5bb59f8dff | ||
|
|
107ffe0e09 | ||
|
|
c7d33a479b | ||
|
|
c5400544b0 | ||
|
|
76a797a663 | ||
|
|
d42308a2cd | ||
|
|
bfa6f5c5ed | ||
|
|
c93ac21b90 | ||
|
|
2132355a7b | ||
|
|
ec9ef43868 | ||
|
|
f33c455ecf | ||
|
|
266ddf5570 | ||
|
|
5e31e3ff6b | ||
|
|
c5c8252325 | ||
|
|
36124919b9 | ||
|
|
4ffdca7c8d | ||
|
|
6eaf748d6f | ||
|
|
5a346cd8df | ||
|
|
585cbb135c | ||
|
|
292573e137 | ||
|
|
3492e49433 | ||
|
|
4529e0fa2b | ||
|
|
9a68094b07 | ||
|
|
1ca8e81062 | ||
|
|
9e430e786e | ||
|
|
bb302001f2 | ||
|
|
7c300733e0 | ||
|
|
57b6f6f1eb | ||
|
|
5bddf66240 | ||
|
|
c8fc611182 | ||
|
|
cbd1058523 | ||
|
|
e0d67f4750 | ||
|
|
5b2c0214db | ||
|
|
511c69b14e | ||
|
|
9ebe6dc841 | ||
|
|
f2bed66119 | ||
|
|
96eebb87af | ||
|
|
716ee0ca50 | ||
|
|
6492215005 | ||
|
|
ab5c337b22 | ||
|
|
16d82332cd |
@@ -14,6 +14,9 @@ common --worker_sandboxing
|
||||
common --local_test_jobs=64
|
||||
common --jobs=64
|
||||
|
||||
# Rebuild remote-only intermediate inputs if the remote cache loses them.
|
||||
common --rewind_lost_inputs
|
||||
|
||||
# Keep these C++ standard flags in sync with tools/copts.bzl and scripts/shardok_cpp_config.sh.
|
||||
common --cxxopt="--std=c++23"
|
||||
common --cxxopt="-Wno-deprecated-non-prototype"
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
9.1.1
|
||||
9.2.0
|
||||
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
rm -f .git/hooks/post-checkout .git/hooks/post-merge .git/hooks/pre-push
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
env:
|
||||
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig-no-lfs
|
||||
GIT_CONFIG_NOSYSTEM: "1"
|
||||
@@ -182,7 +182,7 @@ jobs:
|
||||
FASTMAIL_FROM_NAME: ${{ secrets.FASTMAIL_FROM_NAME }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
brew install git-lfs
|
||||
git-lfs version
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
lfs: false
|
||||
@@ -94,7 +94,7 @@ jobs:
|
||||
brew install git-lfs
|
||||
git-lfs version
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
lfs: false
|
||||
@@ -109,7 +109,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
lfs: false
|
||||
|
||||
@@ -15,6 +15,7 @@ on:
|
||||
- '.clang-tidy'
|
||||
- 'scripts/run-clang-tidy.sh'
|
||||
- 'scripts/check_shardok_clang_tidy.sh'
|
||||
- 'scripts/sync_bazel_xcode.sh'
|
||||
- '.github/actions/setup-bazel/**'
|
||||
- '.github/workflows/bazel_test.yml'
|
||||
- '!src/main/csharp/**'
|
||||
@@ -32,6 +33,7 @@ on:
|
||||
- '.clang-tidy'
|
||||
- 'scripts/run-clang-tidy.sh'
|
||||
- 'scripts/check_shardok_clang_tidy.sh'
|
||||
- 'scripts/sync_bazel_xcode.sh'
|
||||
- '.github/actions/setup-bazel/**'
|
||||
- '.github/workflows/bazel_test.yml'
|
||||
- '!src/main/csharp/**'
|
||||
@@ -72,14 +74,14 @@ jobs:
|
||||
brew install git-lfs
|
||||
git-lfs version
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
lfs: false
|
||||
- name: Check BUILD.bazel dependencies
|
||||
run: ./scripts/check_build_deps.sh --strict
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 'lts/*'
|
||||
- name: Check JavaScript syntax
|
||||
@@ -114,12 +116,14 @@ jobs:
|
||||
brew install git-lfs
|
||||
git-lfs version
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
lfs: false
|
||||
- name: Setup Bazel
|
||||
uses: ./.github/actions/setup-bazel
|
||||
- name: Sync Bazel Xcode config
|
||||
run: ./scripts/sync_bazel_xcode.sh
|
||||
- name: Run tests
|
||||
run: bazel test --build_event_json_file=test.json //src/test/... //src/main/go/...
|
||||
- name: Summarize Bazel build metrics
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ on:
|
||||
- 'src/main/resources/**'
|
||||
- 'ci/BUILD.bazel'
|
||||
- 'ci/github_actions/ensure_bazel_installed.sh'
|
||||
- 'scripts/sync_bazel_xcode.sh'
|
||||
- '.github/actions/setup-bazel/**'
|
||||
- 'MODULE.bazel'
|
||||
- 'MODULE.bazel.lock'
|
||||
@@ -41,6 +42,7 @@ on:
|
||||
- 'ci/BUILD.bazel'
|
||||
- 'ci/github_actions/ensure_bazel_installed.sh'
|
||||
- 'ci/github_actions/fetch_lfs.sh'
|
||||
- 'scripts/sync_bazel_xcode.sh'
|
||||
- '.github/actions/setup-bazel/**'
|
||||
- 'go.mod'
|
||||
- 'go.sum'
|
||||
@@ -114,7 +116,7 @@ jobs:
|
||||
|
||||
- name: Checkout repository
|
||||
if: steps.check-latest.outputs.skip != 'true'
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
env:
|
||||
GIT_LFS_SKIP_SMUDGE: 1
|
||||
with:
|
||||
@@ -125,6 +127,10 @@ jobs:
|
||||
if: steps.check-latest.outputs.skip != 'true'
|
||||
uses: ./.github/actions/setup-bazel
|
||||
|
||||
- name: Sync Bazel Xcode config
|
||||
if: steps.check-latest.outputs.skip != 'true'
|
||||
run: ./scripts/sync_bazel_xcode.sh
|
||||
|
||||
- name: Fetch LFS files needed for admin server
|
||||
if: steps.check-latest.outputs.skip != 'true'
|
||||
env:
|
||||
@@ -319,7 +325,7 @@ jobs:
|
||||
git-lfs --version
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
env:
|
||||
GIT_LFS_SKIP_SMUDGE: 1
|
||||
with:
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
name: Eagle0pedia Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
paths:
|
||||
- 'docs/eagle0pedia/**'
|
||||
- 'ci/github_actions/ensure_bazel_installed.sh'
|
||||
- '.github/actions/setup-bazel/**'
|
||||
- '.github/workflows/eagle0pedia_build.yml'
|
||||
- 'MODULE.bazel'
|
||||
- 'MODULE.bazel.lock'
|
||||
- '.bazelrc'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'docs/eagle0pedia/**'
|
||||
- 'ci/github_actions/ensure_bazel_installed.sh'
|
||||
- '.github/actions/setup-bazel/**'
|
||||
- '.github/workflows/eagle0pedia_build.yml'
|
||||
- 'MODULE.bazel'
|
||||
- 'MODULE.bazel.lock'
|
||||
- '.bazelrc'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: [self-hosted, bazel]
|
||||
steps:
|
||||
- name: Prepare workspace
|
||||
run: |
|
||||
chmod -R u+w eagle0pedia-dist 2>/dev/null || true
|
||||
rm -rf eagle0pedia-dist
|
||||
|
||||
- name: Prepare non-LFS checkout
|
||||
env:
|
||||
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig-no-lfs
|
||||
GIT_CONFIG_NOSYSTEM: "1"
|
||||
run: |
|
||||
git config --global --unset-all filter.lfs.process || true
|
||||
git config --global filter.lfs.smudge "cat"
|
||||
git config --global filter.lfs.clean "cat"
|
||||
git config --global filter.lfs.required false
|
||||
if [ -d ".git" ]; then
|
||||
git config --local --unset-all filter.lfs.process || true
|
||||
git config --local filter.lfs.smudge "cat"
|
||||
git config --local filter.lfs.clean "cat"
|
||||
git config --local filter.lfs.required false
|
||||
fi
|
||||
rm -f .git/hooks/post-checkout .git/hooks/post-merge .git/hooks/pre-push
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
env:
|
||||
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig-no-lfs
|
||||
GIT_CONFIG_NOSYSTEM: "1"
|
||||
with:
|
||||
persist-credentials: false
|
||||
lfs: false
|
||||
|
||||
- name: Setup Bazel
|
||||
uses: ./.github/actions/setup-bazel
|
||||
|
||||
- name: Build Eagle0pedia
|
||||
run: bazel build //docs/eagle0pedia:site
|
||||
|
||||
- name: Stage static site artifact
|
||||
run: |
|
||||
rm -rf eagle0pedia-dist
|
||||
mkdir -p eagle0pedia-dist
|
||||
cp -R bazel-bin/docs/eagle0pedia/dist/. eagle0pedia-dist/
|
||||
chmod -R u+w eagle0pedia-dist
|
||||
|
||||
- name: Upload static site artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: eagle0pedia-dist
|
||||
path: eagle0pedia-dist/
|
||||
if-no-files-found: error
|
||||
retention-days: 3
|
||||
@@ -0,0 +1,102 @@
|
||||
name: Gitea Offsite Backup
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
full_restore_verify:
|
||||
description: 'Download and validate a complete restored snapshot'
|
||||
required: true
|
||||
default: false
|
||||
type: boolean
|
||||
maintenance:
|
||||
description: 'Apply retention and prune unreferenced Restic packs'
|
||||
required: true
|
||||
default: false
|
||||
type: boolean
|
||||
schedule:
|
||||
- cron: '23 10 * * *'
|
||||
- cron: '53 11 * * 0'
|
||||
|
||||
concurrency:
|
||||
group: gitea-offsite-backup
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
backup:
|
||||
if: (github.event_name == 'workflow_dispatch' && inputs.maintenance == false) || (github.event_name == 'schedule' && github.event.schedule == '23 10 * * *')
|
||||
runs-on: [self-hosted, macOS, ARM64, halfdan]
|
||||
timeout-minutes: 240
|
||||
|
||||
steps:
|
||||
- name: Prepare non-LFS checkout
|
||||
env:
|
||||
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig-no-lfs
|
||||
GIT_CONFIG_NOSYSTEM: '1'
|
||||
run: |
|
||||
git config --global --unset-all filter.lfs.process || true
|
||||
git config --global filter.lfs.smudge "cat"
|
||||
git config --global filter.lfs.clean "cat"
|
||||
git config --global filter.lfs.required false
|
||||
if [ -d ".git" ]; then
|
||||
git config --local --unset-all filter.lfs.process || true
|
||||
git config --local filter.lfs.smudge "cat"
|
||||
git config --local filter.lfs.clean "cat"
|
||||
git config --local filter.lfs.required false
|
||||
fi
|
||||
rm -f .git/hooks/post-checkout .git/hooks/post-merge .git/hooks/pre-push
|
||||
|
||||
- uses: actions/checkout@v7
|
||||
env:
|
||||
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig-no-lfs
|
||||
GIT_CONFIG_NOSYSTEM: '1'
|
||||
with:
|
||||
persist-credentials: false
|
||||
lfs: false
|
||||
clean: false
|
||||
|
||||
- name: Back up Gitea
|
||||
env:
|
||||
DO_SPACES_ACCESS_KEY: ${{ secrets.GITEA_DO_SPACES_ACCESS_KEY }}
|
||||
DO_SPACES_ENDPOINT: ${{ secrets.DO_SPACES_ENDPOINT }}
|
||||
DO_SPACES_SECRET_KEY: ${{ secrets.GITEA_DO_SPACES_SECRET_KEY }}
|
||||
FULL_RESTORE_VERIFY: ${{ inputs.full_restore_verify }}
|
||||
GITEA_BACKUP_SSH_KEY: ${{ secrets.GITEA_BACKUP_SSH_KEY }}
|
||||
RESTIC_PASSWORD: ${{ secrets.GITEA_RESTIC_PASSWORD }}
|
||||
run: ./ci/github_actions/backup_gitea.sh backup
|
||||
|
||||
maintenance:
|
||||
if: (github.event_name == 'schedule' && github.event.schedule == '53 11 * * 0') || (github.event_name == 'workflow_dispatch' && inputs.maintenance)
|
||||
runs-on: [self-hosted, macOS, ARM64, halfdan]
|
||||
timeout-minutes: 240
|
||||
|
||||
steps:
|
||||
- name: Prepare non-LFS checkout
|
||||
env:
|
||||
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig-no-lfs
|
||||
GIT_CONFIG_NOSYSTEM: '1'
|
||||
run: |
|
||||
git config --global --unset-all filter.lfs.process || true
|
||||
git config --global filter.lfs.smudge "cat"
|
||||
git config --global filter.lfs.clean "cat"
|
||||
git config --global filter.lfs.required false
|
||||
|
||||
- uses: actions/checkout@v7
|
||||
env:
|
||||
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig-no-lfs
|
||||
GIT_CONFIG_NOSYSTEM: '1'
|
||||
with:
|
||||
persist-credentials: false
|
||||
lfs: false
|
||||
clean: false
|
||||
|
||||
- name: Apply retention and verify repository
|
||||
env:
|
||||
DO_SPACES_ACCESS_KEY: ${{ secrets.GITEA_DO_SPACES_ACCESS_KEY }}
|
||||
DO_SPACES_ENDPOINT: ${{ secrets.DO_SPACES_ENDPOINT }}
|
||||
DO_SPACES_SECRET_KEY: ${{ secrets.GITEA_DO_SPACES_SECRET_KEY }}
|
||||
GITEA_BACKUP_SSH_KEY: ${{ secrets.GITEA_BACKUP_SSH_KEY }}
|
||||
RESTIC_PASSWORD: ${{ secrets.GITEA_RESTIC_PASSWORD }}
|
||||
run: ./ci/github_actions/backup_gitea.sh maintenance
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
|
||||
brew install git-lfs
|
||||
git-lfs version
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
lfs: false
|
||||
|
||||
@@ -138,7 +138,7 @@ jobs:
|
||||
fi
|
||||
git-lfs --version
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
env:
|
||||
GIT_LFS_SKIP_SMUDGE: 1
|
||||
with:
|
||||
@@ -160,6 +160,12 @@ jobs:
|
||||
- name: Ensure Unity version installed
|
||||
run: ./ci/github_actions/ensure_unity_installed.sh ios
|
||||
|
||||
- name: Ensure Bazel installed
|
||||
uses: ./.github/actions/setup-bazel
|
||||
|
||||
- name: Sync Bazel Xcode config
|
||||
run: ./scripts/sync_bazel_xcode.sh
|
||||
|
||||
- name: Build iOS Unity Project
|
||||
id: build
|
||||
run: |
|
||||
@@ -229,7 +235,7 @@ jobs:
|
||||
fi
|
||||
git-lfs --version
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
env:
|
||||
GIT_LFS_SKIP_SMUDGE: 1
|
||||
with:
|
||||
|
||||
@@ -113,7 +113,7 @@ jobs:
|
||||
fi
|
||||
git-lfs --version
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
env:
|
||||
GIT_LFS_SKIP_SMUDGE: 1
|
||||
with:
|
||||
@@ -166,7 +166,7 @@ jobs:
|
||||
run: ./ci/github_actions/ensure_unity_installed.sh mac
|
||||
|
||||
- name: Ensure .NET SDK installed
|
||||
uses: actions/setup-dotnet@v5
|
||||
uses: actions/setup-dotnet@v6
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
@@ -287,7 +287,7 @@ jobs:
|
||||
fi
|
||||
git-lfs --version
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
env:
|
||||
GIT_LFS_SKIP_SMUDGE: 1
|
||||
with:
|
||||
|
||||
@@ -4,17 +4,20 @@ on:
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize, ready_for_review]
|
||||
paths:
|
||||
- '.bazelversion'
|
||||
- 'MODULE.bazel'
|
||||
- 'MODULE.bazel.lock'
|
||||
- 'maven_install.json'
|
||||
- 'renovate.json'
|
||||
- 'ci/github_actions/ensure_bazel_installed.sh'
|
||||
- 'ci/github_actions/update_llvm_distributions.py'
|
||||
- '.github/actions/setup-bazel/**'
|
||||
- '.github/workflows/renovate_dependency_artifacts.yml'
|
||||
|
||||
pull_request_target:
|
||||
types: [opened, reopened, synchronize, ready_for_review]
|
||||
paths:
|
||||
- '.bazelversion'
|
||||
- 'MODULE.bazel'
|
||||
- 'MODULE.bazel.lock'
|
||||
- 'maven_install.json'
|
||||
@@ -22,6 +25,7 @@ on:
|
||||
- 'go.sum'
|
||||
- 'renovate.json'
|
||||
- 'ci/github_actions/ensure_bazel_installed.sh'
|
||||
- 'ci/github_actions/update_llvm_distributions.py'
|
||||
- '.github/actions/setup-bazel/**'
|
||||
- '.github/workflows/renovate_dependency_artifacts.yml'
|
||||
|
||||
@@ -58,11 +62,13 @@ jobs:
|
||||
|
||||
const changed = files.map((file) => file.filename);
|
||||
const bazelPatterns = [
|
||||
/^\.bazelversion$/,
|
||||
/^MODULE\.bazel$/,
|
||||
/^MODULE\.bazel\.lock$/,
|
||||
/^maven_install\.json$/,
|
||||
/^renovate\.json$/,
|
||||
/^ci\/github_actions\/ensure_bazel_installed\.sh$/,
|
||||
/^ci\/github_actions\/update_llvm_distributions\.py$/,
|
||||
/^\.github\/actions\/setup-bazel\//,
|
||||
/^\.github\/workflows\/renovate_dependency_artifacts\.yml$/,
|
||||
];
|
||||
@@ -87,7 +93,7 @@ jobs:
|
||||
include.push({
|
||||
artifact: 'bazel',
|
||||
name: 'Bazel lockfiles',
|
||||
changed_files: 'MODULE.bazel.lock maven_install.json',
|
||||
changed_files: 'MODULE.bazel MODULE.bazel.lock maven_install.json',
|
||||
commit_message: 'Update generated dependency lockfiles',
|
||||
});
|
||||
}
|
||||
@@ -111,13 +117,18 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
lfs: false
|
||||
|
||||
- name: Ensure Bazel installed
|
||||
uses: ./.github/actions/setup-bazel
|
||||
|
||||
- name: Refresh LLVM distribution metadata
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: python3 ci/github_actions/update_llvm_distributions.py
|
||||
|
||||
- name: Update Maven lockfile
|
||||
env:
|
||||
REPIN: "1"
|
||||
@@ -128,15 +139,16 @@ jobs:
|
||||
|
||||
- name: Verify generated lockfiles are current
|
||||
run: |
|
||||
if git diff --quiet -- MODULE.bazel.lock maven_install.json; then
|
||||
if git diff --quiet -- MODULE.bazel MODULE.bazel.lock maven_install.json; then
|
||||
echo "Generated dependency lockfiles are current"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Generated dependency lockfiles are stale. Run the lockfile update commands and commit the result:"
|
||||
echo " python3 ci/github_actions/update_llvm_distributions.py"
|
||||
echo " REPIN=1 bazel run @maven//:pin"
|
||||
echo " bazel build --nobuild //src/main/scala/net/eagle0/eagle:eagle_server //src/main/cpp/net/eagle0/shardok:shardok-server"
|
||||
git diff -- MODULE.bazel.lock maven_install.json
|
||||
git diff -- MODULE.bazel MODULE.bazel.lock maven_install.json
|
||||
exit 1
|
||||
|
||||
update-artifacts:
|
||||
@@ -173,13 +185,19 @@ jobs:
|
||||
git-lfs version
|
||||
|
||||
- name: Checkout Renovate branch
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
token: ${{ secrets.RENOVATE_LOCKFILE_TOKEN || github.token }}
|
||||
lfs: false
|
||||
|
||||
- name: Refresh LLVM distribution metadata
|
||||
if: matrix.artifact == 'bazel'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: python3 ci/github_actions/update_llvm_distributions.py
|
||||
|
||||
- name: Ensure Bazel installed
|
||||
if: matrix.artifact == 'bazel'
|
||||
uses: ./.github/actions/setup-bazel
|
||||
@@ -196,7 +214,7 @@ jobs:
|
||||
|
||||
- name: Set up Go
|
||||
if: matrix.artifact == 'go'
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: false
|
||||
|
||||
@@ -143,7 +143,7 @@ jobs:
|
||||
brew install git-lfs
|
||||
git-lfs version
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
lfs: false
|
||||
|
||||
@@ -10,6 +10,8 @@ on:
|
||||
- 'src/main/go/net/eagle0/build/**'
|
||||
- 'src/main/resources/net/eagle0/shardok/**'
|
||||
- 'ci/BUILD.bazel'
|
||||
- 'ci/shardok_perf.lock.json'
|
||||
- 'ci/shardok_perf.yaml'
|
||||
- 'ci/github_actions/ensure_bazel_installed.sh'
|
||||
- '.github/actions/setup-bazel/**'
|
||||
- 'go.mod'
|
||||
@@ -57,7 +59,7 @@ jobs:
|
||||
rm -f .git/hooks/post-checkout .git/hooks/post-merge .git/hooks/pre-push
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
env:
|
||||
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig-no-lfs
|
||||
GIT_CONFIG_NOSYSTEM: "1"
|
||||
@@ -68,12 +70,18 @@ jobs:
|
||||
- name: Ensure Bazel installed
|
||||
uses: ./.github/actions/setup-bazel
|
||||
|
||||
- name: Build Shardok ARM64 binary (cross-compile for Linux ARM64)
|
||||
- name: Build optimized, profileable Shardok ARM64 binary
|
||||
run: |
|
||||
set -ex
|
||||
|
||||
echo "=== Building shardok-server binary for linux-aarch64 ==="
|
||||
bazel build \
|
||||
-c opt \
|
||||
--strip=never \
|
||||
--copt=-gline-tables-only \
|
||||
--copt=-fno-omit-frame-pointer \
|
||||
--copt=-mno-omit-leaf-frame-pointer \
|
||||
--linkopt=-Wl,--build-id=sha1 \
|
||||
--platforms=//:linux_arm64 \
|
||||
--extra_toolchains=@llvm_toolchain_linux_arm64//:cc-toolchain-aarch64-linux \
|
||||
//src/main/cpp/net/eagle0/shardok:shardok-server
|
||||
@@ -103,6 +111,15 @@ jobs:
|
||||
else
|
||||
echo "WARNING: Binary e_machine is $E_MACHINE (expected 00b7 for aarch64)"
|
||||
fi
|
||||
|
||||
BINARY_DESCRIPTION=$(file "$LINUX_BIN")
|
||||
echo "Binary description: $BINARY_DESCRIPTION"
|
||||
for REQUIRED_TEXT in "ARM aarch64" "BuildID" "with debug_info" "not stripped"; do
|
||||
if [[ "$BINARY_DESCRIPTION" != *"$REQUIRED_TEXT"* ]]; then
|
||||
echo "ERROR: Profileable release binary is missing '$REQUIRED_TEXT'"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
elif [ "$MAGIC" = "cfaeedfe" ] || [ "$MAGIC" = "cffaedfe" ]; then
|
||||
echo "ERROR: Binary is Mach-O format (macOS) - cross-compilation failed!"
|
||||
exit 1
|
||||
@@ -111,13 +128,19 @@ jobs:
|
||||
file "$LINUX_BIN" || true
|
||||
fi
|
||||
|
||||
- name: Build Shardok ARM64 Docker image
|
||||
- name: Build optimized, profileable Shardok ARM64 Docker image
|
||||
id: build-shardok
|
||||
run: |
|
||||
set -ex
|
||||
|
||||
# Keep this in sync with the standalone binary build above.
|
||||
bazel build \
|
||||
-c opt \
|
||||
--strip=never \
|
||||
--copt=-gline-tables-only \
|
||||
--copt=-fno-omit-frame-pointer \
|
||||
--copt=-mno-omit-leaf-frame-pointer \
|
||||
--linkopt=-Wl,--build-id=sha1 \
|
||||
--platforms=//:linux_arm64 \
|
||||
--extra_toolchains=@llvm_toolchain_linux_arm64//:cc-toolchain-aarch64-linux \
|
||||
//ci:shardok_server_image_arm64
|
||||
@@ -262,6 +285,12 @@ jobs:
|
||||
echo "Loaded image ID: $(docker image inspect --format '{{.Id}}' "$SHARDOK_IMAGE")"
|
||||
echo "Shardok binary SHA256:"
|
||||
docker run --rm --entrypoint sha256sum "$SHARDOK_IMAGE" /app/shardok-server
|
||||
echo "Verifying perf capture permissions:"
|
||||
docker run --rm \
|
||||
--cap-add PERFMON \
|
||||
--entrypoint perf \
|
||||
"$SHARDOK_IMAGE" \
|
||||
stat --event cpu-clock:u -- perf version
|
||||
|
||||
# Stop and remove any container using port 40042 or named shardok*
|
||||
docker ps -q --filter "publish=40042" | xargs -r docker stop
|
||||
@@ -272,6 +301,7 @@ jobs:
|
||||
docker run -d \
|
||||
--name shardok-ai \
|
||||
--restart unless-stopped \
|
||||
--cap-add PERFMON \
|
||||
-p 40042:40042 \
|
||||
-v /opt/eagle0/data:/data \
|
||||
-v /etc/shardok:/etc/shardok:ro \
|
||||
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
brew install git-lfs
|
||||
git-lfs version
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
lfs: false
|
||||
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
fi
|
||||
rm -f .git/hooks/post-checkout .git/hooks/post-merge .git/hooks/pre-push
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
env:
|
||||
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig-no-lfs
|
||||
GIT_CONFIG_NOSYSTEM: "1"
|
||||
|
||||
@@ -14,6 +14,7 @@ on:
|
||||
- "scripts/build_protos.sh"
|
||||
- "scripts/build_plugins.sh"
|
||||
- "scripts/build_windows_plugin.sh"
|
||||
- "scripts/sync_bazel_xcode.sh"
|
||||
- "ci/github_actions/build_unity.sh"
|
||||
- "ci/github_actions/test_unity_editmode.sh"
|
||||
- "ci/github_actions/ensure_bazel_installed.sh"
|
||||
@@ -39,6 +40,7 @@ on:
|
||||
- "scripts/build_protos.sh"
|
||||
- "scripts/build_plugins.sh"
|
||||
- "scripts/build_windows_plugin.sh"
|
||||
- "scripts/sync_bazel_xcode.sh"
|
||||
- "ci/github_actions/build_unity.sh"
|
||||
- "ci/github_actions/test_unity_editmode.sh"
|
||||
- "ci/github_actions/ensure_bazel_installed.sh"
|
||||
@@ -94,7 +96,7 @@ jobs:
|
||||
fi
|
||||
git-lfs --version
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
env:
|
||||
GIT_LFS_SKIP_SMUDGE: 1
|
||||
with:
|
||||
@@ -151,11 +153,13 @@ jobs:
|
||||
- name: Ensure Unity version installed
|
||||
run: ./ci/github_actions/ensure_unity_installed.sh windows
|
||||
- name: Ensure .NET SDK installed
|
||||
uses: actions/setup-dotnet@v5
|
||||
uses: actions/setup-dotnet@v6
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
- name: Ensure Bazel installed
|
||||
uses: ./.github/actions/setup-bazel
|
||||
- name: Sync Bazel Xcode configuration
|
||||
run: ./scripts/sync_bazel_xcode.sh
|
||||
|
||||
- name: Run Unity EditMode tests
|
||||
run: ./ci/github_actions/test_unity_editmode.sh
|
||||
@@ -209,7 +213,7 @@ jobs:
|
||||
fi
|
||||
git-lfs --version
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
env:
|
||||
GIT_LFS_SKIP_SMUDGE: 1
|
||||
with:
|
||||
@@ -266,11 +270,13 @@ jobs:
|
||||
- name: Ensure Unity version installed
|
||||
run: ./ci/github_actions/ensure_unity_installed.sh windows
|
||||
- name: Ensure .NET SDK installed
|
||||
uses: actions/setup-dotnet@v5
|
||||
uses: actions/setup-dotnet@v6
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
- name: Ensure Bazel installed
|
||||
uses: ./.github/actions/setup-bazel
|
||||
- name: Sync Bazel Xcode configuration
|
||||
run: ./scripts/sync_bazel_xcode.sh
|
||||
|
||||
- name: Detect Addressables changes
|
||||
id: addressables
|
||||
|
||||
+55
-26
@@ -17,18 +17,35 @@ SLF4J_VERSION = "2.0.18"
|
||||
# Core Build Tools
|
||||
#
|
||||
|
||||
bazel_dep(name = "bazel_skylib", version = "1.9.0")
|
||||
bazel_dep(name = "bazel_skylib", version = "1.9.2")
|
||||
bazel_dep(name = "aspect_rules_esbuild", version = "0.26.0")
|
||||
bazel_dep(name = "aspect_rules_js", version = "3.2.2")
|
||||
bazel_dep(name = "aspect_rules_js", version = "3.3.0")
|
||||
bazel_dep(name = "bazel_jar_jar", version = "0.1.15", repo_name = "bazel_jar_jar")
|
||||
bazel_dep(name = "platforms", version = "1.1.0")
|
||||
bazel_dep(name = "rules_dotnet", version = "0.21.5")
|
||||
bazel_dep(name = "rules_dotnet", version = "0.22.1")
|
||||
bazel_dep(name = "rules_foreign_cc", version = "0.15.1")
|
||||
bazel_dep(name = "rules_android", version = "0.7.3")
|
||||
bazel_dep(name = "rules_nodejs", version = "6.7.4")
|
||||
bazel_dep(name = "rules_nodejs", version = "6.7.5")
|
||||
bazel_dep(name = "rules_pkg", version = "1.2.0")
|
||||
bazel_dep(name = "rules_shell", version = "0.8.0")
|
||||
|
||||
multiple_version_override(
|
||||
module_name = "aspect_rules_js",
|
||||
versions = [
|
||||
"2.3.8",
|
||||
"3.3.0",
|
||||
],
|
||||
)
|
||||
|
||||
npm = use_extension("@aspect_rules_js//npm:extensions.bzl", "npm")
|
||||
npm.npm_translate_lock(
|
||||
name = "eagle0pedia_npm",
|
||||
pnpm_lock = "//docs/eagle0pedia:pnpm-lock.yaml",
|
||||
run_lifecycle_hooks = False,
|
||||
update_pnpm_lock = False,
|
||||
)
|
||||
use_repo(npm, "eagle0pedia_npm")
|
||||
|
||||
#
|
||||
# Language Support - Scala
|
||||
#
|
||||
@@ -57,12 +74,12 @@ bazel_dep(name = "toolchains_llvm", version = "1.8.0")
|
||||
|
||||
llvm = use_extension("@toolchains_llvm//toolchain/extensions:llvm.bzl", "llvm")
|
||||
|
||||
LLVM_VERSION = "22.1.7"
|
||||
LLVM_VERSION = "22.1.8"
|
||||
|
||||
LLVM_DISTRIBUTIONS = {
|
||||
"LLVM-22.1.7-Linux-ARM64.tar.xz": "118ca2d3ad9da34367e05735317854e7977db45dc4c02a32af58da64c23b8789",
|
||||
"LLVM-22.1.7-Linux-X64.tar.xz": "edb0522b41e261819c06ea437d249f9b8acfa413d3805bc9920eec6fb76ff830",
|
||||
"LLVM-22.1.7-macOS-ARM64.tar.xz": "4177245188b0a30a6539c96b361dea56f253485756bfd8927a6a59e7301e7806",
|
||||
"LLVM-22.1.8-Linux-ARM64.tar.xz": "805efad2bb91cb4967fa569e0881d10c0f69c04461cf671cccbae19f547acc34",
|
||||
"LLVM-22.1.8-Linux-X64.tar.xz": "df0e1ecf16caf3489a272a5eea4eec9b0d82878f6477fa309504f918a0006384",
|
||||
"LLVM-22.1.8-macOS-ARM64.tar.xz": "f260f4f7c0d430828a81ae8a3826a1d63fc0963ec2459489308cc23b1f7eab4f",
|
||||
}
|
||||
|
||||
# Native toolchain (macOS -> macOS, Linux -> Linux)
|
||||
@@ -125,8 +142,8 @@ sysroot(
|
||||
# Language Support - Go
|
||||
#
|
||||
|
||||
bazel_dep(name = "rules_go", version = "0.61.1", repo_name = "io_bazel_rules_go")
|
||||
bazel_dep(name = "gazelle", version = "0.51.3", repo_name = "bazel_gazelle")
|
||||
bazel_dep(name = "rules_go", version = "0.62.0", repo_name = "io_bazel_rules_go")
|
||||
bazel_dep(name = "gazelle", version = "0.52.2", repo_name = "bazel_gazelle")
|
||||
|
||||
go_sdk = use_extension("@io_bazel_rules_go//go:extensions.bzl", "go_sdk")
|
||||
go_sdk.download(version = "1.25.11")
|
||||
@@ -152,7 +169,7 @@ use_repo(
|
||||
# Language Support - Rust
|
||||
#
|
||||
|
||||
bazel_dep(name = "rules_rust", version = "0.70.0")
|
||||
bazel_dep(name = "rules_rust", version = "0.71.3")
|
||||
|
||||
rust = use_extension("@rules_rust//rust:extensions.bzl", "rust")
|
||||
rust.toolchain(edition = "2021")
|
||||
@@ -160,7 +177,7 @@ use_repo(rust, "rust_toolchains")
|
||||
|
||||
register_toolchains("@rust_toolchains//:all")
|
||||
|
||||
crate = use_extension("@rules_rust//crate_universe:extension.bzl", "crate")
|
||||
crate = use_extension("@rules_rust//crate_universe:extensions.bzl", "crate")
|
||||
crate.from_cargo(
|
||||
name = "map_generator_crates",
|
||||
cargo_lockfile = "//src/main/rust/net/eagle0/eagle/map_generator:Cargo.lock",
|
||||
@@ -172,7 +189,7 @@ use_repo(crate, "map_generator_crates")
|
||||
# Platform Support - Apple/iOS
|
||||
#
|
||||
|
||||
bazel_dep(name = "apple_support", version = "2.6.1", repo_name = "build_bazel_apple_support")
|
||||
bazel_dep(name = "apple_support", version = "2.8.0", repo_name = "build_bazel_apple_support")
|
||||
bazel_dep(name = "rules_apple", version = "4.5.3", repo_name = "build_bazel_rules_apple")
|
||||
|
||||
multiple_version_override(
|
||||
@@ -197,6 +214,8 @@ use_repo(apple_cc_configure, "local_config_apple_cc", "local_config_apple_cc_too
|
||||
bazel_dep(name = "protobuf", version = "35.1", repo_name = "com_google_protobuf")
|
||||
single_version_override(
|
||||
module_name = "protobuf",
|
||||
patch_strip = 1,
|
||||
patches = ["//third_party/protobuf/patches:rust_crates_dev_dependency.patch"],
|
||||
version = "35.1",
|
||||
)
|
||||
|
||||
@@ -217,10 +236,10 @@ use_repo(
|
||||
)
|
||||
|
||||
bazel_dep(name = "rules_proto", version = "7.1.0")
|
||||
bazel_dep(name = "rules_cc", version = "0.2.19")
|
||||
bazel_dep(name = "grpc", version = "1.81.1")
|
||||
bazel_dep(name = "rules_cc", version = "0.2.22")
|
||||
bazel_dep(name = "grpc", version = "1.83.0")
|
||||
|
||||
# gRPC 1.81.1's generated upb module map trips Clang's layering check in the
|
||||
# gRPC's generated upb module map trips Clang's layering check in the
|
||||
# Linux ARM64 cross-toolchain even though the normal Bazel deps are present.
|
||||
single_version_override(
|
||||
module_name = "grpc",
|
||||
@@ -228,7 +247,7 @@ single_version_override(
|
||||
patches = ["//third_party/grpc/patches:disable_xds_client_layering_check.patch"],
|
||||
)
|
||||
|
||||
bazel_dep(name = "grpc-java", version = "1.78.0.bcr.1")
|
||||
bazel_dep(name = "grpc-java", version = "1.82.0")
|
||||
bazel_dep(name = "rules_proto_grpc_csharp", version = "5.8.0")
|
||||
bazel_dep(name = "flatbuffers", version = "25.12.19")
|
||||
|
||||
@@ -244,16 +263,25 @@ bazel_dep(name = "googletest", version = "1.17.0.bcr.2")
|
||||
|
||||
bazel_dep(name = "rules_oci", version = "2.3.0")
|
||||
bazel_dep(name = "aspect_bazel_lib", version = "2.22.5")
|
||||
bazel_dep(name = "rules_distroless", version = "0.8.0")
|
||||
|
||||
oci = use_extension("@rules_oci//oci:extensions.bzl", "oci")
|
||||
|
||||
apt = use_extension("@rules_distroless//apt:extensions.bzl", "apt")
|
||||
apt.install(
|
||||
lock = "//ci:shardok_perf.lock.json",
|
||||
name = "shardok_perf",
|
||||
manifest = "//ci:shardok_perf.yaml",
|
||||
)
|
||||
use_repo(apt, "shardok_perf")
|
||||
|
||||
# Base image for Eagle (Java 25 JRE - smaller runtime, no dev tools needed)
|
||||
# Digest pinned for Bazel repository cache hit; update with:
|
||||
# TOKEN=$(curl -s "https://auth.docker.io/token?service=registry.docker.io&scope=repository:library/eclipse-temurin:pull" | python3 -c "import json,sys; print(json.load(sys.stdin)['token'])")
|
||||
# curl -s -H "Authorization: Bearer $TOKEN" -H "Accept: application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.list.v2+json" "https://registry-1.docker.io/v2/library/eclipse-temurin/manifests/25-jre" | python3 -c "import json,sys; m=json.load(sys.stdin); [print(p['digest']) for p in m.get('manifests',[]) if p['platform']['architecture']=='amd64']"
|
||||
oci.pull(
|
||||
name = "eclipse_temurin_25_jre",
|
||||
digest = "sha256:0beb258e10ad28d2bae0f74349a170bdfa67da0ef87c028cbf3d95c3e1170120",
|
||||
digest = "sha256:882787e465d6c0c4c08daa8d9e209d57ec052228eff8244c959f271af24ad27b",
|
||||
image = "docker.io/library/eclipse-temurin",
|
||||
platforms = ["linux/amd64"],
|
||||
tag = "26-jre",
|
||||
@@ -262,7 +290,7 @@ oci.pull(
|
||||
# Base image for JFR sidecar (Java 25 JDK - includes jcmd for JFR dumps)
|
||||
oci.pull(
|
||||
name = "eclipse_temurin_25_jdk",
|
||||
digest = "sha256:41c3a5b8a607d226c73e81538cd94d0e66f746f41f057561447eb47b40a91a65",
|
||||
digest = "sha256:625889835aab66a096f999ee16cd7061ac846b155dc7adbed693686bbae5c59a",
|
||||
image = "docker.io/library/eclipse-temurin",
|
||||
platforms = ["linux/amd64"],
|
||||
tag = "26-jdk",
|
||||
@@ -293,8 +321,8 @@ use_repo(oci, "alpine_linux", "alpine_linux_linux_amd64", "eclipse_temurin_25_jd
|
||||
# Java/Scala Dependencies
|
||||
#
|
||||
|
||||
bazel_dep(name = "rules_java", version = "9.6.1")
|
||||
bazel_dep(name = "rules_jvm_external", version = "7.0")
|
||||
bazel_dep(name = "rules_java", version = "9.7.0")
|
||||
bazel_dep(name = "rules_jvm_external", version = "7.1")
|
||||
|
||||
maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")
|
||||
maven.install(
|
||||
@@ -317,7 +345,7 @@ maven.install(
|
||||
"com.thesamet.scalapb:scalapb-runtime_3:%s" % SCALAPB_VERSION,
|
||||
"com.thesamet.scalapb:scalapb-runtime-grpc_3:%s" % SCALAPB_VERSION,
|
||||
"com.thesamet.scalapb:compilerplugin_3:%s" % SCALAPB_VERSION,
|
||||
"com.thesamet.scalapb:protoc-bridge_3:0.9.9",
|
||||
"com.thesamet.scalapb:protoc-bridge_3:0.9.10",
|
||||
|
||||
# JSON
|
||||
"org.json4s:json4s-ast_3:4.1.0-M8",
|
||||
@@ -328,7 +356,7 @@ maven.install(
|
||||
"org.scalamock:scalamock_3:7.5.5",
|
||||
|
||||
# Developer tools
|
||||
"org.scalameta:scalafmt-cli_2.13:3.11.1",
|
||||
"org.scalameta:scalafmt-cli_2.13:3.11.5",
|
||||
|
||||
# AWS SDK
|
||||
"software.amazon.awssdk:s3-transfer-manager:%s" % AWS_SDK_VERSION,
|
||||
@@ -359,13 +387,14 @@ maven.install(
|
||||
"com.nimbusds:nimbus-jose-jwt:10.9.1",
|
||||
|
||||
# Error tracking
|
||||
"io.sentry:sentry:8.43.2",
|
||||
"io.sentry:sentry:8.50.1",
|
||||
|
||||
# SQLite for client text storage
|
||||
"org.xerial:sqlite-jdbc:3.53.2.0",
|
||||
"org.xerial:sqlite-jdbc:3.53.2.1",
|
||||
|
||||
# Postgres for production history benchmarking and migration work
|
||||
"org.postgresql:postgresql:42.7.11",
|
||||
"com.zaxxer:HikariCP:7.1.0",
|
||||
"org.postgresql:postgresql:42.7.13",
|
||||
],
|
||||
duplicate_version_warning = "error",
|
||||
fail_if_repin_required = True,
|
||||
|
||||
Generated
+103
-1847
File diff suppressed because one or more lines are too long
+2
-1
@@ -214,7 +214,7 @@ oci_push(
|
||||
#
|
||||
# Shardok Server ARM64 Docker Image (for the Hetzner deployment host)
|
||||
#
|
||||
# Build: bazel build //ci:shardok_server_image_arm64 --platforms=//:linux_arm64 --extra_toolchains=@llvm_toolchain_linux_arm64//:cc-toolchain-aarch64-linux
|
||||
# Build: ./scripts/build_shardok_linux_arm64_ci.sh //ci:shardok_server_image_arm64
|
||||
# Load: bazel run //ci:shardok_server_load_arm64
|
||||
# Push: bazel run //ci:shardok_server_push_arm64
|
||||
#
|
||||
@@ -235,6 +235,7 @@ oci_image(
|
||||
"40052/tcp",
|
||||
],
|
||||
tars = [
|
||||
"@shardok_perf//:flat",
|
||||
# Note: busybox_layer_arm64 omitted - busybox.net has SSL issues
|
||||
# Health checks can use the shardok-server binary itself or be added later
|
||||
":shardok_binary_layer_arm64",
|
||||
|
||||
@@ -46,6 +46,45 @@ fi
|
||||
SCHEME="Unity-iPhone"
|
||||
echo "Using scheme: $SCHEME"
|
||||
|
||||
# Xcode can be upgraded on a self-hosted runner before its matching iOS platform
|
||||
# component is installed. In that state, xcrun may exist but the generic iOS
|
||||
# destination is ineligible, and archive fails with exit code 70. Probe the exact
|
||||
# destination used below and repair the Xcode installation before starting the
|
||||
# expensive archive.
|
||||
if ! xcodebuild \
|
||||
-project "$XCODEPROJ" \
|
||||
-scheme "$SCHEME" \
|
||||
-showBuildSettings \
|
||||
-destination "generic/platform=iOS" >/dev/null; then
|
||||
echo "The generic iOS destination is unavailable; installing Xcode's iOS platform..."
|
||||
|
||||
if ! xcodebuild -checkFirstLaunchStatus; then
|
||||
echo "Completing pending Xcode first-launch setup..."
|
||||
xcodebuild -runFirstLaunch
|
||||
fi
|
||||
|
||||
xcodebuild -downloadPlatform iOS
|
||||
fi
|
||||
|
||||
echo "Installed Xcode SDKs:"
|
||||
xcodebuild -showsdks
|
||||
IOS_SDK_PATH=$(xcrun --sdk iphoneos --show-sdk-path)
|
||||
if [ ! -d "$IOS_SDK_PATH" ]; then
|
||||
echo "Error: Xcode reported an invalid iOS SDK path: $IOS_SDK_PATH"
|
||||
exit 1
|
||||
fi
|
||||
echo "Using iOS SDK: $IOS_SDK_PATH"
|
||||
|
||||
if ! xcodebuild \
|
||||
-project "$XCODEPROJ" \
|
||||
-scheme "$SCHEME" \
|
||||
-showBuildSettings \
|
||||
-destination "generic/platform=iOS" >/dev/null; then
|
||||
echo "Error: The generic iOS destination is still unavailable after platform setup."
|
||||
xcodebuild -project "$XCODEPROJ" -scheme "$SCHEME" -showdestinations || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Archive without signing - we'll sign during export
|
||||
# This avoids issues with provisioning profiles on framework targets
|
||||
xcodebuild archive \
|
||||
|
||||
Executable
+296
@@ -0,0 +1,296 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
BACKUP_MODE=${1:-backup}
|
||||
GITEA_BACKUP_SSH_HOST=${GITEA_BACKUP_SSH_HOST:-gitea-backup-source}
|
||||
GITEA_HTTP_URL=${GITEA_HTTP_URL:-http://zog:3000/admin/eagle0.git}
|
||||
GITEA_VERSION_URL=${GITEA_VERSION_URL:-http://zog:3000/api/v1/version}
|
||||
GITEA_REMOTE_ROOT=${GITEA_REMOTE_ROOT:-/volume2/docker/gitea}
|
||||
GITEA_BACKUP_BUCKET=${GITEA_BACKUP_BUCKET:-eagle0-gitea-backups}
|
||||
GITEA_BACKUP_PREFIX=${GITEA_BACKUP_PREFIX:-restic}
|
||||
RUN_ID=${GITHUB_RUN_ID:-manual}
|
||||
GITEA_DATA_DIRECTORIES=(
|
||||
actions_artifacts
|
||||
actions_log
|
||||
attachments
|
||||
avatars
|
||||
conf
|
||||
home
|
||||
packages
|
||||
repo-avatars
|
||||
)
|
||||
|
||||
require_environment_variable() {
|
||||
local variable_name=$1
|
||||
if [ -z "${!variable_name:-}" ]; then
|
||||
echo "ERROR: Required environment variable '$variable_name' is not set."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
require_environment_variable RUNNER_TEMP
|
||||
require_environment_variable DO_SPACES_ENDPOINT
|
||||
require_environment_variable DO_SPACES_ACCESS_KEY
|
||||
require_environment_variable DO_SPACES_SECRET_KEY
|
||||
require_environment_variable GITEA_BACKUP_SSH_KEY
|
||||
require_environment_variable RESTIC_PASSWORD
|
||||
|
||||
case "$RUN_ID" in
|
||||
*[!A-Za-z0-9._-]*)
|
||||
echo "ERROR: Unsafe run identifier '$RUN_ID'."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
STAGING_ROOT="$RUNNER_TEMP/eagle0-gitea-backup"
|
||||
VERIFY_ROOT="$RUNNER_TEMP/eagle0-gitea-restore-verification"
|
||||
REMOTE_SNAPSHOT_DIR="$GITEA_REMOTE_ROOT/backup-staging"
|
||||
REMOTE_DATABASE="$GITEA_REMOTE_ROOT/data/gitea/gitea.db"
|
||||
REMOTE_DATABASE_SNAPSHOT="$REMOTE_SNAPSHOT_DIR/gitea-$RUN_ID.db"
|
||||
RESTIC_REPOSITORY="s3:${DO_SPACES_ENDPOINT%/}/$GITEA_BACKUP_BUCKET/$GITEA_BACKUP_PREFIX"
|
||||
SSH_CONFIG="$RUNNER_TEMP/eagle0-gitea-backup-ssh-config"
|
||||
SSH_PRIVATE_KEY="$RUNNER_TEMP/eagle0-gitea-backup-ssh-key"
|
||||
|
||||
export AWS_ACCESS_KEY_ID="$DO_SPACES_ACCESS_KEY"
|
||||
export AWS_SECRET_ACCESS_KEY="$DO_SPACES_SECRET_KEY"
|
||||
export AWS_DEFAULT_REGION=us-east-1
|
||||
export RESTIC_REPOSITORY
|
||||
export RESTIC_PASSWORD
|
||||
export RESTIC_CACHE_DIR="$RUNNER_TEMP/restic-cache"
|
||||
export HOMEBREW_NO_AUTO_UPDATE=1
|
||||
export HOMEBREW_NO_INSTALL_CLEANUP=1
|
||||
|
||||
COMMON_PATHS=(/opt/homebrew/bin /usr/local/bin)
|
||||
for common_path in "${COMMON_PATHS[@]}"; do
|
||||
if [ -d "$common_path" ]; then
|
||||
export PATH="$common_path:$PATH"
|
||||
fi
|
||||
done
|
||||
|
||||
ensure_tool() {
|
||||
local command_name=$1
|
||||
local brew_package=$2
|
||||
|
||||
if command -v "$command_name" >/dev/null 2>&1; then
|
||||
return
|
||||
fi
|
||||
|
||||
if ! command -v brew >/dev/null 2>&1; then
|
||||
echo "ERROR: '$command_name' is unavailable and Homebrew is not installed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
brew install "$brew_package"
|
||||
}
|
||||
|
||||
safe_reset_directory() {
|
||||
local directory=$1
|
||||
|
||||
case "$directory" in
|
||||
"$RUNNER_TEMP"/eagle0-gitea-*)
|
||||
rm -rf "$directory"
|
||||
mkdir -p "$directory"
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: Refusing to reset unsafe directory '$directory'."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
remove_remote_database_snapshot() {
|
||||
ssh -F "$SSH_CONFIG" -o BatchMode=yes -o ConnectTimeout=15 "$GITEA_BACKUP_SSH_HOST" \
|
||||
"rm -f '$REMOTE_DATABASE_SNAPSHOT'" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
remove_remote_database_snapshot
|
||||
|
||||
case "$STAGING_ROOT" in
|
||||
"$RUNNER_TEMP"/eagle0-gitea-*)
|
||||
rm -rf "$STAGING_ROOT"
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$VERIFY_ROOT" in
|
||||
"$RUNNER_TEMP"/eagle0-gitea-*)
|
||||
rm -rf "$VERIFY_ROOT"
|
||||
;;
|
||||
esac
|
||||
|
||||
rm -f "$SSH_CONFIG" "$SSH_PRIVATE_KEY"
|
||||
}
|
||||
|
||||
trap cleanup EXIT
|
||||
|
||||
umask 077
|
||||
printf '%s\n' "$GITEA_BACKUP_SSH_KEY" > "$SSH_PRIVATE_KEY"
|
||||
printf '%s\n' \
|
||||
"Host $GITEA_BACKUP_SSH_HOST" \
|
||||
" HostName zog" \
|
||||
" User \"Dan Crosby\"" \
|
||||
" IdentityFile $SSH_PRIVATE_KEY" \
|
||||
" IdentitiesOnly yes" \
|
||||
" HostKeyAlias zog" \
|
||||
> "$SSH_CONFIG"
|
||||
|
||||
ensure_tool aws awscli
|
||||
ensure_tool restic restic
|
||||
ensure_tool sqlite3 sqlite
|
||||
|
||||
require_backup_bucket() {
|
||||
if ! aws s3api head-bucket \
|
||||
--bucket "$GITEA_BACKUP_BUCKET" \
|
||||
--endpoint-url "$DO_SPACES_ENDPOINT" >/dev/null 2>&1; then
|
||||
echo "ERROR: DigitalOcean Space '$GITEA_BACKUP_BUCKET' is unavailable."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
initialize_repository_if_needed() {
|
||||
if restic cat config >/dev/null 2>&1; then
|
||||
return
|
||||
fi
|
||||
|
||||
echo "Initializing encrypted Restic repository."
|
||||
restic init
|
||||
}
|
||||
|
||||
require_initialized_repository() {
|
||||
if restic cat config >/dev/null 2>&1; then
|
||||
return
|
||||
fi
|
||||
|
||||
echo "ERROR: The encrypted Restic repository is not initialized."
|
||||
exit 1
|
||||
}
|
||||
|
||||
prepare_staging_data() {
|
||||
safe_reset_directory "$STAGING_ROOT"
|
||||
mkdir -p "$STAGING_ROOT/gitea"
|
||||
mkdir -p "$STAGING_ROOT/repositories/admin"
|
||||
|
||||
ssh -F "$SSH_CONFIG" -o BatchMode=yes -o ConnectTimeout=15 "$GITEA_BACKUP_SSH_HOST" true
|
||||
ssh -F "$SSH_CONFIG" -o BatchMode=yes -o ConnectTimeout=15 "$GITEA_BACKUP_SSH_HOST" \
|
||||
"mkdir -p '$REMOTE_SNAPSHOT_DIR'"
|
||||
ssh -F "$SSH_CONFIG" -o BatchMode=yes -o ConnectTimeout=15 "$GITEA_BACKUP_SSH_HOST" \
|
||||
"sqlite3 '$REMOTE_DATABASE' \".backup '$REMOTE_DATABASE_SNAPSHOT'\""
|
||||
|
||||
scp -O -F "$SSH_CONFIG" -o BatchMode=yes -o ConnectTimeout=15 -pr \
|
||||
"$GITEA_BACKUP_SSH_HOST:$GITEA_REMOTE_ROOT/config" \
|
||||
"$STAGING_ROOT/"
|
||||
|
||||
for directory in "${GITEA_DATA_DIRECTORIES[@]}"; do
|
||||
scp -O -F "$SSH_CONFIG" -o BatchMode=yes -o ConnectTimeout=15 -pr \
|
||||
"$GITEA_BACKUP_SSH_HOST:$GITEA_REMOTE_ROOT/data/gitea/$directory" \
|
||||
"$STAGING_ROOT/gitea/"
|
||||
done
|
||||
|
||||
scp -O -F "$SSH_CONFIG" -o BatchMode=yes -o ConnectTimeout=15 -p \
|
||||
"$GITEA_BACKUP_SSH_HOST:$REMOTE_DATABASE_SNAPSHOT" \
|
||||
"$STAGING_ROOT/gitea/gitea.db"
|
||||
|
||||
remove_remote_database_snapshot
|
||||
|
||||
scp -O -F "$SSH_CONFIG" -o BatchMode=yes -o ConnectTimeout=15 -pr \
|
||||
"$GITEA_BACKUP_SSH_HOST:$GITEA_REMOTE_ROOT/data/lfs" \
|
||||
"$STAGING_ROOT/"
|
||||
|
||||
git clone --mirror \
|
||||
"$GITEA_HTTP_URL" \
|
||||
"$STAGING_ROOT/repositories/admin/eagle0.git"
|
||||
|
||||
local database_check
|
||||
database_check=$(sqlite3 "$STAGING_ROOT/gitea/gitea.db" "PRAGMA integrity_check;")
|
||||
if [ "$database_check" != "ok" ]; then
|
||||
echo "ERROR: Gitea database snapshot failed SQLite integrity checking."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git --git-dir="$STAGING_ROOT/repositories/admin/eagle0.git" fsck --full
|
||||
|
||||
local gitea_version
|
||||
gitea_version=$(curl --fail --silent "$GITEA_VERSION_URL")
|
||||
local main_commit
|
||||
main_commit=$(git --git-dir="$STAGING_ROOT/repositories/admin/eagle0.git" rev-parse refs/heads/main)
|
||||
local captured_at
|
||||
captured_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
printf '%s\n' \
|
||||
"captured_at=$captured_at" \
|
||||
"gitea_version=$gitea_version" \
|
||||
"main_commit=$main_commit" \
|
||||
"source_host=$GITEA_BACKUP_SSH_HOST" \
|
||||
"source_root=$GITEA_REMOTE_ROOT" \
|
||||
> "$STAGING_ROOT/manifest.txt"
|
||||
}
|
||||
|
||||
verify_restored_snapshot() {
|
||||
safe_reset_directory "$VERIFY_ROOT"
|
||||
restic restore latest --host zog --tag gitea --target "$VERIFY_ROOT"
|
||||
|
||||
local restored_root="$VERIFY_ROOT/${STAGING_ROOT#/}"
|
||||
local database_check
|
||||
database_check=$(sqlite3 "$restored_root/gitea/gitea.db" "PRAGMA integrity_check;")
|
||||
if [ "$database_check" != "ok" ]; then
|
||||
echo "ERROR: Restored Gitea database failed SQLite integrity checking."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git --git-dir="$restored_root/repositories/admin/eagle0.git" fsck --full
|
||||
test -s "$restored_root/manifest.txt"
|
||||
test -d "$restored_root/lfs"
|
||||
echo "Full restore verification completed successfully."
|
||||
}
|
||||
|
||||
run_backup() {
|
||||
require_backup_bucket
|
||||
initialize_repository_if_needed
|
||||
prepare_staging_data
|
||||
|
||||
restic backup "$STAGING_ROOT" --host zog --tag gitea
|
||||
restic forget \
|
||||
--host zog \
|
||||
--tag gitea \
|
||||
--group-by host,tags \
|
||||
--keep-daily 14 \
|
||||
--keep-weekly 8 \
|
||||
--keep-monthly 12 \
|
||||
--keep-yearly 3
|
||||
|
||||
if [ "${FULL_RESTORE_VERIFY:-false}" = "true" ]; then
|
||||
restic check --read-data
|
||||
verify_restored_snapshot
|
||||
else
|
||||
restic check --read-data-subset=5%
|
||||
fi
|
||||
|
||||
restic snapshots --host zog --tag gitea
|
||||
}
|
||||
|
||||
run_maintenance() {
|
||||
require_initialized_repository
|
||||
restic forget \
|
||||
--host zog \
|
||||
--tag gitea \
|
||||
--keep-daily 14 \
|
||||
--keep-weekly 8 \
|
||||
--keep-monthly 12 \
|
||||
--keep-yearly 3 \
|
||||
--prune
|
||||
restic check --read-data-subset=10%
|
||||
}
|
||||
|
||||
case "$BACKUP_MODE" in
|
||||
backup)
|
||||
run_backup
|
||||
;;
|
||||
maintenance)
|
||||
run_maintenance
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 [backup|maintenance]"
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""Update pinned LLVM release asset names and SHA-256 digests in MODULE.bazel."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
LLVM_VERSION_PATTERN = re.compile(r'^LLVM_VERSION = "(?P<version>\d+\.\d+\.\d+)"$', re.MULTILINE)
|
||||
LLVM_DISTRIBUTIONS_PATTERN = re.compile(
|
||||
r"^LLVM_DISTRIBUTIONS = \{\n.*?^\}$",
|
||||
re.MULTILINE | re.DOTALL,
|
||||
)
|
||||
LLVM_DISTRIBUTION_PLATFORMS = (
|
||||
"Linux-ARM64",
|
||||
"Linux-X64",
|
||||
"macOS-ARM64",
|
||||
)
|
||||
|
||||
|
||||
def fetch_release(version: str, token: str | None) -> dict[str, Any]:
|
||||
url = f"https://api.github.com/repos/llvm/llvm-project/releases/tags/llvmorg-{version}"
|
||||
headers = {
|
||||
"Accept": "application/vnd.github+json",
|
||||
"User-Agent": "eagle0-llvm-distribution-updater",
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
}
|
||||
if token:
|
||||
headers["Authorization"] = f"Bearer {token}"
|
||||
|
||||
request = urllib.request.Request(url, headers=headers)
|
||||
with urllib.request.urlopen(request, timeout=30) as response:
|
||||
return json.load(response)
|
||||
|
||||
|
||||
def load_release(path: Path) -> dict[str, Any]:
|
||||
with path.open(encoding="utf-8") as release_file:
|
||||
return json.load(release_file)
|
||||
|
||||
|
||||
def llvm_distributions(version: str, release: dict[str, Any]) -> dict[str, str]:
|
||||
expected_tag = f"llvmorg-{version}"
|
||||
if release.get("tag_name") != expected_tag:
|
||||
raise ValueError(
|
||||
f"Expected release tag {expected_tag}, got {release.get('tag_name')!r}"
|
||||
)
|
||||
|
||||
assets = {asset["name"]: asset for asset in release.get("assets", [])}
|
||||
distributions: dict[str, str] = {}
|
||||
for platform in LLVM_DISTRIBUTION_PLATFORMS:
|
||||
name = f"LLVM-{version}-{platform}.tar.xz"
|
||||
asset = assets.get(name)
|
||||
if asset is None:
|
||||
raise ValueError(f"LLVM release {version} is missing required asset {name}")
|
||||
|
||||
digest = asset.get("digest")
|
||||
if not isinstance(digest, str) or not digest.startswith("sha256:"):
|
||||
raise ValueError(f"LLVM release asset {name} has no SHA-256 digest")
|
||||
distributions[name] = digest.removeprefix("sha256:")
|
||||
|
||||
return distributions
|
||||
|
||||
|
||||
def render_distributions(distributions: dict[str, str]) -> str:
|
||||
lines = ["LLVM_DISTRIBUTIONS = {"]
|
||||
lines.extend(f' "{name}": "{digest}",' for name, digest in distributions.items())
|
||||
lines.append("}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def update_module(module_path: Path, release: dict[str, Any]) -> bool:
|
||||
original = module_path.read_text(encoding="utf-8")
|
||||
version_match = LLVM_VERSION_PATTERN.search(original)
|
||||
if version_match is None:
|
||||
raise ValueError(f"Could not find LLVM_VERSION in {module_path}")
|
||||
|
||||
distributions = llvm_distributions(version_match.group("version"), release)
|
||||
replacement = render_distributions(distributions)
|
||||
updated, replacements = LLVM_DISTRIBUTIONS_PATTERN.subn(replacement, original, count=1)
|
||||
if replacements != 1:
|
||||
raise ValueError(f"Could not find exactly one LLVM_DISTRIBUTIONS block in {module_path}")
|
||||
|
||||
if updated == original:
|
||||
return False
|
||||
|
||||
module_path.write_text(updated, encoding="utf-8")
|
||||
return True
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--module", type=Path, default=Path("MODULE.bazel"))
|
||||
parser.add_argument(
|
||||
"--release-json",
|
||||
type=Path,
|
||||
help="Read GitHub release JSON from a file instead of the GitHub API",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
module_text = args.module.read_text(encoding="utf-8")
|
||||
version_match = LLVM_VERSION_PATTERN.search(module_text)
|
||||
if version_match is None:
|
||||
print(f"ERROR: Could not find LLVM_VERSION in {args.module}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
version = version_match.group("version")
|
||||
try:
|
||||
release = (
|
||||
load_release(args.release_json)
|
||||
if args.release_json
|
||||
else fetch_release(version, os.environ.get("GITHUB_TOKEN"))
|
||||
)
|
||||
changed = update_module(args.module, release)
|
||||
except (OSError, ValueError) as error:
|
||||
print(f"ERROR: {error}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if changed:
|
||||
print(f"Updated LLVM {version} distribution metadata in {args.module}")
|
||||
else:
|
||||
print(f"LLVM {version} distribution metadata is current")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
import update_llvm_distributions
|
||||
|
||||
|
||||
class UpdateLlvmDistributionsTest(unittest.TestCase):
|
||||
def release(self, *, missing_digest: bool = False) -> dict:
|
||||
assets = []
|
||||
for index, platform in enumerate(
|
||||
update_llvm_distributions.LLVM_DISTRIBUTION_PLATFORMS, start=1
|
||||
):
|
||||
asset = {
|
||||
"name": f"LLVM-22.1.8-{platform}.tar.xz",
|
||||
"digest": f"sha256:{index:064x}",
|
||||
}
|
||||
assets.append(asset)
|
||||
if missing_digest:
|
||||
assets[0]["digest"] = None
|
||||
return {"tag_name": "llvmorg-22.1.8", "assets": assets}
|
||||
|
||||
def test_updates_asset_versions_and_digests(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
module = Path(directory) / "MODULE.bazel"
|
||||
module.write_text(
|
||||
'LLVM_VERSION = "22.1.8"\n\n'
|
||||
"LLVM_DISTRIBUTIONS = {\n"
|
||||
' "LLVM-22.1.7-Linux-ARM64.tar.xz": "old-arm",\n'
|
||||
' "LLVM-22.1.7-Linux-X64.tar.xz": "old-x64",\n'
|
||||
' "LLVM-22.1.7-macOS-ARM64.tar.xz": "old-mac",\n'
|
||||
"}\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
changed = update_llvm_distributions.update_module(module, self.release())
|
||||
|
||||
self.assertTrue(changed)
|
||||
updated = module.read_text(encoding="utf-8")
|
||||
self.assertNotIn("22.1.7", updated)
|
||||
self.assertIn('"LLVM-22.1.8-Linux-ARM64.tar.xz": "' + "1".zfill(64), updated)
|
||||
self.assertIn('"LLVM-22.1.8-macOS-ARM64.tar.xz": "' + "3".zfill(64), updated)
|
||||
|
||||
def test_rejects_asset_without_sha256_digest(self) -> None:
|
||||
with self.assertRaisesRegex(ValueError, "has no SHA-256 digest"):
|
||||
update_llvm_distributions.llvm_distributions(
|
||||
"22.1.8", self.release(missing_digest=True)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,15 @@
|
||||
version: 1
|
||||
|
||||
sources:
|
||||
- channel: resolute main universe
|
||||
url: https://ports.ubuntu.com/ubuntu-ports
|
||||
- channel: resolute-security main universe
|
||||
url: https://ports.ubuntu.com/ubuntu-ports
|
||||
- channel: resolute-updates main universe
|
||||
url: https://ports.ubuntu.com/ubuntu-ports
|
||||
|
||||
archs:
|
||||
- "arm64"
|
||||
|
||||
packages:
|
||||
- "linux-perf"
|
||||
@@ -39,6 +39,13 @@ services:
|
||||
EAGLE_POSTGRES_USER: "${EAGLE_POSTGRES_USER:-}"
|
||||
EAGLE_POSTGRES_PASSWORD: "${EAGLE_POSTGRES_PASSWORD:-}"
|
||||
EAGLE_POSTGRES_SSLMODE: "${EAGLE_POSTGRES_SSLMODE:-require}"
|
||||
EAGLE_POSTGRES_MAX_POOL_SIZE: "${EAGLE_POSTGRES_MAX_POOL_SIZE:-7}"
|
||||
EAGLE_POSTGRES_MIN_IDLE: "${EAGLE_POSTGRES_MIN_IDLE:-0}"
|
||||
EAGLE_POSTGRES_CONNECTION_TIMEOUT_MILLIS: "${EAGLE_POSTGRES_CONNECTION_TIMEOUT_MILLIS:-10000}"
|
||||
EAGLE_POSTGRES_IDLE_TIMEOUT_MILLIS: "${EAGLE_POSTGRES_IDLE_TIMEOUT_MILLIS:-120000}"
|
||||
EAGLE_POSTGRES_MAX_LIFETIME_MILLIS: "${EAGLE_POSTGRES_MAX_LIFETIME_MILLIS:-1500000}"
|
||||
EAGLE_POSTGRES_KEEPALIVE_TIME_MILLIS: "${EAGLE_POSTGRES_KEEPALIVE_TIME_MILLIS:-300000}"
|
||||
EAGLE_POSTGRES_LEAK_DETECTION_THRESHOLD_MILLIS: "${EAGLE_POSTGRES_LEAK_DETECTION_THRESHOLD_MILLIS:-0}"
|
||||
# JWT public key for token validation (auth service handles signing)
|
||||
# Reads from /etc/eagle0/keys/public.pem via shared volume
|
||||
# Auth token for Shardok on Hetzner (required)
|
||||
@@ -95,6 +102,13 @@ services:
|
||||
EAGLE_POSTGRES_USER: "${EAGLE_POSTGRES_USER:-}"
|
||||
EAGLE_POSTGRES_PASSWORD: "${EAGLE_POSTGRES_PASSWORD:-}"
|
||||
EAGLE_POSTGRES_SSLMODE: "${EAGLE_POSTGRES_SSLMODE:-require}"
|
||||
EAGLE_POSTGRES_MAX_POOL_SIZE: "${EAGLE_POSTGRES_MAX_POOL_SIZE:-7}"
|
||||
EAGLE_POSTGRES_MIN_IDLE: "${EAGLE_POSTGRES_MIN_IDLE:-0}"
|
||||
EAGLE_POSTGRES_CONNECTION_TIMEOUT_MILLIS: "${EAGLE_POSTGRES_CONNECTION_TIMEOUT_MILLIS:-10000}"
|
||||
EAGLE_POSTGRES_IDLE_TIMEOUT_MILLIS: "${EAGLE_POSTGRES_IDLE_TIMEOUT_MILLIS:-120000}"
|
||||
EAGLE_POSTGRES_MAX_LIFETIME_MILLIS: "${EAGLE_POSTGRES_MAX_LIFETIME_MILLIS:-1500000}"
|
||||
EAGLE_POSTGRES_KEEPALIVE_TIME_MILLIS: "${EAGLE_POSTGRES_KEEPALIVE_TIME_MILLIS:-300000}"
|
||||
EAGLE_POSTGRES_LEAK_DETECTION_THRESHOLD_MILLIS: "${EAGLE_POSTGRES_LEAK_DETECTION_THRESHOLD_MILLIS:-0}"
|
||||
# JWT public key for token validation (auth service handles signing)
|
||||
# Reads from /etc/eagle0/keys/public.pem via shared volume
|
||||
SHARDOK_AUTH_TOKEN: "${SHARDOK_AUTH_TOKEN:-}"
|
||||
@@ -220,6 +234,10 @@ services:
|
||||
- "auth:40033"
|
||||
- "--jfr-sidecar-addr"
|
||||
- "${JFR_SIDECAR_ADDR:-jfr-sidecar:8081}" # Can be switched for blue-green
|
||||
- "--shardok-profile-addr"
|
||||
- "${SHARDOK_ADDRESS}"
|
||||
- "--shardok-profile-tls"
|
||||
- "true"
|
||||
- "--http-port"
|
||||
- "8080"
|
||||
- "--maps-dir"
|
||||
@@ -233,6 +251,8 @@ services:
|
||||
DO_SPACES_SECRET_KEY: "${ADMIN_S3_SECRET_KEY:-}"
|
||||
# GitHub PAT for map editor PR creation (scoped to contents:write + pull_requests:write)
|
||||
GITHUB_TOKEN: "${GITHUB_TOKEN:-}"
|
||||
# Reuse Eagle's Shardok endpoint and auth token for admin-only CPU profiling.
|
||||
SHARDOK_AUTH_TOKEN: "${SHARDOK_AUTH_TOKEN:-}"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
# No external port - accessed via nginx at admin.eagle0.net
|
||||
|
||||
@@ -156,7 +156,7 @@ CREATE TABLE action_changed_provinces (
|
||||
support_delta REAL,
|
||||
-- round properties
|
||||
set_has_acted INTEGER, -- 0/1/NULL
|
||||
set_ruler_is_traveling INTEGER,
|
||||
set_ruler_is_visiting_town INTEGER,
|
||||
-- ruling faction changes
|
||||
clear_ruling_faction_id INTEGER NOT NULL DEFAULT 0,
|
||||
new_ruling_faction_id INTEGER,
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
# Text Client Operator Guide
|
||||
|
||||
This guide covers operating the Eagle text client against a running Eagle server. It does not cover game strategy.
|
||||
|
||||
## Authentication And Startup
|
||||
|
||||
The text client can request and refresh its own tokens. GitHub is the recommended OAuth provider:
|
||||
|
||||
Start the production client with:
|
||||
|
||||
```bash
|
||||
bazel run //src/main/scala/net/eagle0/eagle/text_client:eagle_text_client -- --tls --host prod.eagle0.net --port 443 --oauth
|
||||
```
|
||||
|
||||
On the first run, or when no usable refresh token remains, the client prints a GitHub login URL. Paste that URL to the user and wait for them to authenticate in their browser. The callback returns to a temporary loopback listener in the text client, not to the Unity client. Never open the URL or choose an account on the user's behalf.
|
||||
|
||||
The client stores the access and refresh tokens in `~/.eagle0/text-client/` with owner-only permissions and refreshes the access token automatically. For an existing token location, pass both `--bearer-token-file <path>` and `--refresh-token-file <path>`. Use `--oauth-provider <provider>` only if the account uses `google`, `discord`, `apple`, `steam`, or `twitch` instead of the recommended `github` provider.
|
||||
|
||||
At the `eagle0>` prompt, use `lobby` to list running games and available new-game leaders. To resume a game, use:
|
||||
|
||||
```text
|
||||
stream <game-id>
|
||||
```
|
||||
|
||||
Wait for `subscription: success=true` before posting a command.
|
||||
|
||||
## Inspecting State
|
||||
|
||||
Use these commands before acting:
|
||||
|
||||
```text
|
||||
status
|
||||
state
|
||||
commands
|
||||
commands-json
|
||||
```
|
||||
|
||||
`state` is the concise map and owned-province view. `state-json` prints the complete view. `commands-json` includes the exact fields, IDs, resource amounts, and command choices currently accepted by the server. Each entry also includes a `post-json` template for its corresponding selected command.
|
||||
|
||||
Generated text is stored locally during the client process. Use `text <text-id>` for a text ID without spaces, such as `text hn_105`. Province leaders in `state` are resolved through this store when their names have been received.
|
||||
|
||||
## Posting Eagle Commands
|
||||
|
||||
Use the available-command data from `commands-json` to select a command, then post a matching selected command:
|
||||
|
||||
```text
|
||||
post-json <province-id> <SelectedCommandType> <JSON>
|
||||
```
|
||||
|
||||
Templates contain the accepted JSON field names and default values. Replace zero
|
||||
IDs, `UNKNOWN` enum values, empty collections, and other placeholders with a legal
|
||||
choice from the available-command payload before posting. Where a selected command
|
||||
must echo server data, such as a ransom offer, the client copies that data into the
|
||||
template automatically.
|
||||
|
||||
For example:
|
||||
|
||||
```text
|
||||
post-json 13 ImproveSelectedCommand {"improvementType":"ECONOMY","actingHeroId":105,"lockType":true}
|
||||
post-rest 13
|
||||
post-feast 13
|
||||
```
|
||||
|
||||
The client has shortcuts for `post-rest`, `post-return`, `post-feast`, riot decisions, and battle aftermath. The authoritative selected-command schemas are defined in:
|
||||
|
||||
```text
|
||||
src/main/protobuf/net/eagle0/eagle/api/selected_command.proto
|
||||
```
|
||||
|
||||
Only post a selected command that is currently listed as available. A successful request prints `post-command: SUCCESS`; do not assume a command was accepted until that response or a later game update arrives.
|
||||
|
||||
Some actions enter a temporary subcommand state. For example, trading is operated as:
|
||||
|
||||
```text
|
||||
post-json <province-id> VisitTownSelectedCommand {}
|
||||
post-json <province-id> TradeSelectedCommand {"tradeType":"BUY_FOOD","amount":<amount>}
|
||||
post-return <province-id>
|
||||
```
|
||||
|
||||
After each step, run `commands-json` again. The server will reject a nested command, such as `TradeSelectedCommand`, if its parent state has not been entered.
|
||||
|
||||
## March Warnings
|
||||
|
||||
Marches may produce a warning before being sent. Review it, then resubmit only when intentional:
|
||||
|
||||
```text
|
||||
post-json --force <province-id> MarchSelectedCommand <JSON>
|
||||
```
|
||||
|
||||
`MarchSelectedCommand.marchingUnits` contains combat units. Each selected battalion is paired with a distinct hero in the same combat-unit entry; do not submit battalion-only entries.
|
||||
|
||||
## Shardok Battles
|
||||
|
||||
When a battle is active, inspect it with:
|
||||
|
||||
```text
|
||||
shardok-units
|
||||
shardok-commands
|
||||
```
|
||||
|
||||
Post one of the currently listed command indices with:
|
||||
|
||||
```text
|
||||
post-shardok <index>
|
||||
```
|
||||
|
||||
Omit the optional roll value during normal play. The server then generates any
|
||||
roll required by the selected command. To override that roll for deterministic
|
||||
testing, append an integer value:
|
||||
|
||||
```text
|
||||
post-shardok <index> <integer-roll>
|
||||
```
|
||||
|
||||
`roll` is a placeholder in command usage, not a literal argument. Only an
|
||||
integer is accepted, and the override has an effect only for Shardok commands
|
||||
that support a client-supplied roll.
|
||||
|
||||
For a specific active battle:
|
||||
|
||||
```text
|
||||
post-shardok <shardok-game-id> <index>
|
||||
post-shardok <shardok-game-id> <index> <integer-roll>
|
||||
```
|
||||
|
||||
The battle ID may be omitted only when exactly one outstanding battle currently
|
||||
has postable commands. If multiple battles have commands, the client refuses the
|
||||
ambiguous shorthand; use the battle ID printed by `shardok-commands`. Battles
|
||||
removed from Eagle's outstanding-battle list are no longer postable, and a
|
||||
Shardok update with no available commands clears the previous command list.
|
||||
|
||||
Use `shardok-commands-json` or `shardok-units-json` when the concise output omits a needed field.
|
||||
|
||||
The initial Eagle state includes the complete strategic province graph. Inspect it with:
|
||||
|
||||
```text
|
||||
eagle-map
|
||||
eagle-map-json
|
||||
```
|
||||
|
||||
Both forms list every province and all of its neighbors by ID and name. Use the JSON form when exact IDs or
|
||||
machine-readable relationships are needed; unlike March destinations, this is the complete map graph and is not
|
||||
filtered by the commands currently available.
|
||||
|
||||
During battle setup, `shardok-commands` prints any server-recommended unit placements in order and identifies the
|
||||
ordinary current command index for the next recommendation:
|
||||
|
||||
```text
|
||||
recommended-starting-positions (ordered):
|
||||
[0] unit=12 target=(3,4)
|
||||
[1] unit=15 target=(4,4)
|
||||
use next: post-shardok <shardok-game-id> 17
|
||||
```
|
||||
|
||||
The list is advice, not an automatic placement batch. Review it against `shardok-map`, choose whether to use the first
|
||||
recommendation, and submit that one placement with the printed `post-shardok` command. After every placement, wait for
|
||||
the updated state and run `shardok-commands` again; the legal command indices and remaining recommendations may change.
|
||||
|
||||
The client synchronizes Shardok map definitions when it connects. To inspect the
|
||||
base terrain together with current tile modifiers and occupying units, use:
|
||||
|
||||
```text
|
||||
shardok-map <shardok-game-id>
|
||||
shardok-map-json <shardok-game-id>
|
||||
```
|
||||
|
||||
The battle ID may be omitted when the client knows about exactly one Shardok
|
||||
battle. The concise map prints every coordinate's terrain plus fire, castle,
|
||||
bridge, ice, and snow state. The JSON form includes the complete base map,
|
||||
dynamic tile modifiers, and current active units.
|
||||
|
||||
## Reconnects And Shutdown
|
||||
|
||||
The client automatically recreates a failed gRPC stream and re-subscribes to the active game. It retains its in-process `ClientTextStore`, so a reconnect should announce:
|
||||
|
||||
```text
|
||||
Reconnecting to game <game-id> with saved text state.
|
||||
```
|
||||
|
||||
Followed by a successful subscription without replaying the full generated-text catalog. Do not start a second client merely because a stream reports `GOAWAY`, `UNAVAILABLE`, or `RST_STREAM`; wait for the automatic reconnect and subscription acknowledgment.
|
||||
|
||||
The text store is in memory. A newly launched process performs an initial text sync once, then retains byte offsets for reconnects during that process.
|
||||
|
||||
Use `quit` to close the client cleanly. Use `drop <game-id>` only when intentionally abandoning that game.
|
||||
@@ -0,0 +1,210 @@
|
||||
# Tutorial Playthrough Notes
|
||||
|
||||
This is a working guide for playing through the Eagle0 tutorial without losing. It is written from live text-client
|
||||
testing, so treat it as a practical checklist rather than a complete design reference.
|
||||
|
||||
## Core Rule
|
||||
|
||||
Do not risk Sadar Rakon unless the outcome is overwhelmingly favorable. In tutorial games, Sadar is the human faction
|
||||
leader; if he is captured, the server treats the human player as defeated and the game ends.
|
||||
|
||||
## Strategic Goals
|
||||
|
||||
- Survive the opening tutorial battle.
|
||||
- Rebuild Onmaa enough to stabilize support and loyalty.
|
||||
- Expand carefully from Onmaa/Nikemi with enough force to avoid leader capture.
|
||||
- Trigger the major strategic tutorial event where The Eagle appears. In code, this happens when Sadar controls at
|
||||
least four provinces or Hedrick's faction has no provinces.
|
||||
|
||||
## Tactical Safety Rules
|
||||
|
||||
- Avoid sending Sadar alone into a tactical battle.
|
||||
- If a battle turns into a long chase, reassess instead of blindly sweeping the map.
|
||||
- Prefer preserving Sadar over forcing a marginal province capture.
|
||||
- When the text client shows no attack command, do not assume the enemy is helpless; they may still be able to win by
|
||||
holding, hiding, or outlasting the attacker.
|
||||
|
||||
## Known Failure
|
||||
|
||||
In one previous Kojaria attempt, Sadar attacked with one battalion, chased Hedrick across the map, failed to force a
|
||||
decisive win, and was captured. The client did not clearly announce game over at the time; the save later showed Kojaria
|
||||
held by Hedrick and Sadar listed under `captured_heroes`.
|
||||
|
||||
## Live Run Log
|
||||
|
||||
### Fresh Tutorial Run
|
||||
|
||||
- Started: game `-3568010721410464682`.
|
||||
- Leader: Sadar Rakon.
|
||||
- Current objective: start the tutorial from the opening battle and record safe choices.
|
||||
- Opening battle setup:
|
||||
- Rakon's Loyalists at `(8,10)`.
|
||||
- Onmaa Defenders at `(7,10)`.
|
||||
- Hunters of the Steppe at `(8,11)`.
|
||||
- Rationale: keep the two infantry units together on the west-facing line and keep the longbowmen directly behind
|
||||
them. Tarn's force begins west/southwest of the defender setup area, and Elena/John arrive later, so the opening
|
||||
goal is to preserve the core army rather than overextend.
|
||||
- Opening battle, day 1: no attacks were available. Ended the turn rather than starting fires or moving forward.
|
||||
- Opening battle, day 2:
|
||||
- Used Hunters of the Steppe for an archery attack against the visible unit at `(7,9)`.
|
||||
- Used Rakon's Loyalists for an archery attack against the unit at `(6,10)`.
|
||||
- Used Onmaa Defenders for melee against the unit at `(6,10)`.
|
||||
- Avoided the offered duel from Rakon's Loyalists against the unit at `(7,9)`. If that unit is Tarn, the duel risks
|
||||
Sadar directly before reinforcements have arrived.
|
||||
- Opening battle, days 3-5:
|
||||
- On day 3, only Rakon's Loyalists and Hunters of the Steppe had commands. Used both for archery against the unit at
|
||||
`(6,10)`.
|
||||
- On days 4 and 5, only Hunters of the Steppe had commands. Used archery against the nearest non-Tarn pressure unit
|
||||
rather than retreating or entering melee.
|
||||
- This is a danger signal: if Sadar's unit disappears from the command list this early, the opening formation may have
|
||||
absorbed too much damage. Keep future attempts more defensive and consider moving Sadar back instead of using every
|
||||
available attack.
|
||||
- Elena Fyar arrived on day 6 as actor `7`, confirming the `elena_reinforcement_round_5` tutorial event. She spawned
|
||||
far southwest of the fight with Holy Wave available. Moved her toward the battle first instead of spending Holy Wave
|
||||
while isolated; the move consumed her action for the turn.
|
||||
- John Ranil arrived on day 8 as actor `8`, confirming the `ranil_reinforcement_round_7` tutorial event.
|
||||
- After John arrived:
|
||||
- Moved Hunters of the Steppe back to `(10,11)` instead of taking a weak melee attack.
|
||||
- Moved Elena to `(8,7)` to close with the enemy line.
|
||||
- Moved John to `(6,4)` to approach from the west.
|
||||
- Do not expect reinforcements to appear in immediate melee range. They need one or more turns of marching before they
|
||||
can rescue the original defender units.
|
||||
- Day 10 Holy Wave timing:
|
||||
- Moved John adjacent to Elena first.
|
||||
- Used Elena's Holy Wave only after John was adjacent, so the action could inspire more than Elena herself.
|
||||
- If Holy Wave is available immediately when Elena appears, do not spend it unless she is already adjacent to a
|
||||
friendly unit or there is a specific emergency reason.
|
||||
- Day 11 reinforcement attack:
|
||||
- Elena got melee commands against `(7,9)` and `(8,10)`. Chose `(8,10)` first because `(7,9)` had repeatedly looked
|
||||
like the Tarn/front-leader unit.
|
||||
- Moved John to `(9,9)`, which opened a charge against `(8,10)`.
|
||||
- Used John's charge and then his follow-up melee against `(8,10)`.
|
||||
- Practical rule: once Elena and John arrive, pair their attacks on the same non-Tarn unit before taking leader-risky
|
||||
engagements.
|
||||
- Probable defeat/reset signal:
|
||||
- Continued John melee attacks against `(8,10)` through day 14.
|
||||
- After the day-14 melee, the Shardok stream emitted `shardok-reset` for the same battle id
|
||||
`ce7be1272b8b4456_1_df540967`.
|
||||
- The battle token reset to `6` and commands returned to the original opening placement commands, while Eagle status
|
||||
still reported `BATTLE_IN_PROGRESS`.
|
||||
- This most likely means the battle was lost, but the text client did not surface the loss explicitly. Treat a reset
|
||||
after a long battle as a probable defeat signal unless server state proves otherwise.
|
||||
- For the live run, replay the reset battle with a more conservative setup and keep Sadar out of the first contact
|
||||
line.
|
||||
- Replay setup after reset:
|
||||
- Rakon's Loyalists at `(10,11)`.
|
||||
- Onmaa Defenders at `(8,10)`.
|
||||
- Hunters of the Steppe at `(9,11)`.
|
||||
- Rationale: put Sadar deep, use Onmaa Defenders as the first contact screen, and keep archers close enough to shoot
|
||||
without becoming the front line.
|
||||
- Replay tactical adjustment:
|
||||
- Day 2: enemy units still reached Sadar quickly. Moved Rakon's Loyalists away instead of dueling or meleeing.
|
||||
- Day 3: moved Sadar again, then moved Hunters of the Steppe out of melee contact.
|
||||
- Day 4-5: continued withdrawing Sadar north/east and pulled the archer back instead of trading melee.
|
||||
- Elena arrived with Sadar still alive and mobile. Move Elena east immediately; do not spend Holy Wave while isolated.
|
||||
- John arrived after Elena. Use Elena for contact, march John east, and let Sadar contribute only with archery while
|
||||
he remains out of melee.
|
||||
- Second probable defeat/reset:
|
||||
- The replay kept Sadar alive through both reinforcements and reached day 20.
|
||||
- After repeated John melee against the remaining contact at `(6,7)`, Shardok emitted another `shardok-reset` for the
|
||||
same battle id and returned to the original placement command list.
|
||||
- This reproduced the reset behavior from the first attempt and likely means the replay was also lost. Stop treating
|
||||
the current live game as a successful tutorial source until the loss condition is visible in the text client or
|
||||
confirmed from server state.
|
||||
- Failed castle-hold variant:
|
||||
- Onmaa castle tiles are `(6,12)`, `(8,11)`, and `(10,11)`.
|
||||
- Rakon's Loyalists at `(10,11)`.
|
||||
- Onmaa Defenders at `(8,11)`.
|
||||
- Hunters of the Steppe at `(6,12)`.
|
||||
- Early result: this was much stronger than the previous formations. When no attacks were available, ended turn
|
||||
instead of moving off castles. When longbow shots were offered, used Hunters of the Steppe from `(6,12)`. When
|
||||
enemies stepped adjacent, used ordinary castle melee, but continued to avoid duels.
|
||||
- What failed:
|
||||
- The battle still reset after day 22, returning the same battle id to token `6`.
|
||||
- The late phase degraded into too much Holy Wave/Reduce and too little direct pressure. Treat that as the wrong
|
||||
plan for this tutorial battle.
|
||||
|
||||
### Successful Opening Battle
|
||||
|
||||
- Won in game `-3568010721410464682`, battle `ce7be1272b8b4456_1_df540967`.
|
||||
- Winning setup:
|
||||
- Sadar/Rakon's Loyalists at `(10,11)`.
|
||||
- Onmaa Defenders at `(8,11)`.
|
||||
- Hunters of the Steppe at `(6,12)`.
|
||||
- Main rule: hold the three castle tiles with the starting units until the enemy gives useful attacks. Do not walk the
|
||||
starting units off the castles just because movement commands are available.
|
||||
- Early turns:
|
||||
- End the first defender turn when only movement/fire commands are available.
|
||||
- Use Hunters of the Steppe for archery whenever possible, usually into the non-Ikhaan targets at `(5,10)`,
|
||||
`(5,11)`, or `(7,10)`.
|
||||
- Use ordinary melee from castle tiles when the enemy steps adjacent.
|
||||
- Sadar may duel non-Ikhaan targets. In the winning run, actor `4` used `CHALLENGE_DUEL` against targetUnit `2`.
|
||||
- Avoid Sadar's duel against targetUnit `3` at `(9,10)`; that target behaved like Ikhaan Tarn. Ordinary melee into
|
||||
that adjacent target was still used later, but the duel was skipped every time.
|
||||
- Reinforcements:
|
||||
- Elena arrived as actor `7`. March her directly toward the castle line instead of spending Holy Wave while isolated:
|
||||
`(11,3)`, `(8,5)`, `(8,8)`, then `(8,11)`.
|
||||
- John arrived as actor `8`. Rush him toward the castles: `(6,4)`, `(10,7)`, then `(8,10)`.
|
||||
- Once John and Elena reach the line, use their melee attacks immediately. In the winning run both repeatedly attacked
|
||||
the non-Ikhaan target at `(7,10)`.
|
||||
- After that target dropped, chase the remaining left-side contact with John and Elena together. John moved to
|
||||
`(4,11)`, Elena to `(5,10)`, and John finished the battle with melee at `(5,11)`.
|
||||
- Do not make Holy Wave or Reduce the plan. The successful run won without using Holy Wave or Reduce in the decisive
|
||||
phase; direct archery, castle melee, and reinforcement melee were enough.
|
||||
- Post-battle aftermath:
|
||||
- The battle resolved to Eagle commands and offered three captured heroes: `201`, `202`, and `203`.
|
||||
- Recruit attempts were refused for all three, then each was imprisoned.
|
||||
- After the captured-hero prompts cleared, normal strategic commands returned at province `14`, starting with
|
||||
`ImproveAvailableCommand`.
|
||||
|
||||
### Strategic Play After Opening Battle
|
||||
|
||||
- Immediate recovery priorities:
|
||||
- Improve Onmaa's economy and agriculture before starting a war.
|
||||
- Use Alms sparingly if support is low, but watch food and treasury.
|
||||
- Train existing heroes/units before marching. The opening win leaves enough time to build before attacking.
|
||||
- Troop organization notes:
|
||||
- Onmaa initially exposed only `LightInfantry` and `Longbowmen` as constructible in the command surface.
|
||||
- An invalid organize attempt tried to top up preexisting heavy units and hit the server error
|
||||
`HeavyInfantry was not among constructible battalion types Vector(LightInfantry, Longbowmen)`.
|
||||
- Safe commands in the live run were:
|
||||
- Top up the existing light infantry battalion.
|
||||
- Create a new longbow battalion. The text-client payload that worked used numeric enum value `4` for Longbowmen.
|
||||
- Do not assume existing heavy battalions can be reinforced just because they already exist in the province.
|
||||
- Marching rule:
|
||||
- Do not march Sadar in the first expansion army. Leave him in Onmaa and send the non-leader army instead.
|
||||
- The live run sent Elena, John, Agamemnon, and Old Marek from Onmaa to Nikemi, then on to Kojaria.
|
||||
- Send supplies forward after the army reaches Nikemi; do not strand the field army without food.
|
||||
|
||||
### Failed Hedrick Attack
|
||||
|
||||
- Target: Kojaria, held by Hedrick the Hedge-Merchant's faction.
|
||||
- Army: four non-Sadar heroes, about 1954 troops, including the new untrained longbow battalion.
|
||||
- Strategic safety result:
|
||||
- This was better than marching Sadar alone. A reset here did not immediately imply player defeat because Sadar
|
||||
stayed home.
|
||||
- It still appears to be a losing tactical plan.
|
||||
- Tactical setup:
|
||||
- Placed the army on the west edge and advanced toward Hedrick's single 1000-troop stack.
|
||||
- A first attempt surrounded Hedrick and used repeated melee. It reset after an infantry melee, likely a loss.
|
||||
- A second attempt used more spacing:
|
||||
- Move John/engineer to the firing lane first.
|
||||
- Use archery whenever it appears.
|
||||
- Move cavalry into contact only after ranged damage is available.
|
||||
- Avoid actor 0 infantry melee and avoid making Reduce/Holy Wave the core plan.
|
||||
- What worked better:
|
||||
- The stable pattern was archery from actor 0 when available, archery from the new longbows when available, and
|
||||
cavalry melee from Old Marek.
|
||||
- Extinguishing fire with the longbows was useful when the enemy lit the cavalry contact square.
|
||||
- What failed:
|
||||
- Even with repeated archery and cavalry melee, the battle dragged to day 12 and then reset after another cavalry
|
||||
melee.
|
||||
- A single late Reduce did not visibly break the stalemate.
|
||||
- The new longbow battalion had `0.0` training and `0.0` armament in the stale strategic snapshot, so it may have
|
||||
contributed much less than its troop count suggested.
|
||||
- Practical conclusion:
|
||||
- Do not attack Hedrick immediately after creating a raw longbow battalion.
|
||||
- Build a stronger army first: train and arm the new longbows, improve the economy/agriculture enough to support
|
||||
better reinforcements, and consider taking easier provinces or waiting until the army has more developed units.
|
||||
- If attacking Hedrick, treat his 1000-stack as a boss fight. The current safe-looking four-hero army was not enough
|
||||
to finish it reliably.
|
||||
@@ -287,8 +287,8 @@ You play **Sadar Rakon**, formerly Ikhaan Tarn's most trusted lieutenant. Tarn's
|
||||
| 5 | `tutorial_support_deadline` | December nag if support < 40: have Elena give alms now |
|
||||
| 6 | `guidance_expand` (script `tutorial_expansion`) | Once support is stable: keep developing, taxes coming in January |
|
||||
| 7 | `tutorial_taxes_collected` | January: gold/food collected. Now expand — March your warlord + most heroes to a neighbor, leave 1–2 behind |
|
||||
| 8 | `tutorial_ready_to_join` | A free hero in `{provinceName}` is ready to recruit → Travel + Recruit |
|
||||
| 9 | `tutorial_in_town` | When you Travel: explains Trade / Arm Troops / Divine / Recruit / Manage Prisoners; *Return* ends the turn |
|
||||
| 8 | `tutorial_ready_to_join` | A free hero in `{provinceName}` is ready to recruit → Visit Town + Recruit |
|
||||
| 9 | `tutorial_in_town` | When you Visit Town: explains Trade / Arm Troops / Divine / Recruit / Manage Prisoners; *Return* ends the turn |
|
||||
| 10 | `tutorial_faction_appears` | **The twist**: Fracture Covenant lands at Ingia and Soria. *The Eagle* commands them. Tarn may be with them |
|
||||
| 11 | `tutorial_hero_departed` | First King's hero abandons service — "something deeper is at work" |
|
||||
| 12 | `tutorial_hero_departed_again` | Second one in a different month — "something is wrong, I can feel it" (foreshadowing) |
|
||||
|
||||
@@ -78,7 +78,7 @@ This section lists quests with direct `QuestCommandChooser` handlers in `Fulfill
|
||||
|-------|---------|-------|
|
||||
| `GrandArmyQuest` | `GrandArmyQuestCommandChooser` | Uses OrganizeTroops to top off existing battalions and hire Light Infantry. Only executes if resources are sufficient to fully meet the troop target. |
|
||||
| `BattalionDiversityQuest` | `BattalionDiversityQuestCommandChooser` | Hires one battalion of a missing type to reach 3+ distinct types. Only attempts if the province already has 2+ existing types, has sufficient development (`meetsRequirements`), and gold/food surplus. |
|
||||
| `UpgradeBattalionQuest` | `UpgradeBattalionQuestCommandChooser` | Walks a priority tree per turn: arm-completes (with optional travel and/or food sale setup) → train-completes → march in a pre-qualified neighbor battalion → organize/top-off of the target type → march in an unqualified neighbor battalion → develop Economy/Agriculture → train progress → develop Infrastructure → arm progress. Only toggles Travel when it directly enables an arm-completes finisher. |
|
||||
| `UpgradeBattalionQuest` | `UpgradeBattalionQuestCommandChooser` | Walks a priority tree per turn: arm-completes (with optional Visit Town and/or food sale setup) → train-completes → march in a pre-qualified neighbor battalion → organize/top-off of the target type → march in an unqualified neighbor battalion → develop Economy/Agriculture → train progress → develop Infrastructure → arm progress. Only chooses Visit Town when it directly enables an arm-completes finisher. |
|
||||
|
||||
### Reconnaissance Quests
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
load("@aspect_rules_js//js:defs.bzl", "js_binary", "js_run_binary")
|
||||
load("@eagle0pedia_npm//:defs.bzl", "npm_link_all_packages")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
exports_files([
|
||||
"package.json",
|
||||
"pnpm-lock.yaml",
|
||||
])
|
||||
|
||||
npm_link_all_packages()
|
||||
|
||||
filegroup(
|
||||
name = "site_sources",
|
||||
srcs = glob([
|
||||
"src/**",
|
||||
"astro.config.mjs",
|
||||
"package.json",
|
||||
"tsconfig.json",
|
||||
]),
|
||||
)
|
||||
|
||||
js_binary(
|
||||
name = "astro",
|
||||
data = [
|
||||
":node_modules",
|
||||
],
|
||||
entry_point = "tools/astro-cli.mjs",
|
||||
)
|
||||
|
||||
js_run_binary(
|
||||
name = "site",
|
||||
srcs = [
|
||||
":node_modules",
|
||||
":site_sources",
|
||||
],
|
||||
args = ["build"],
|
||||
chdir = package_name(),
|
||||
env = {
|
||||
"ASTRO_TELEMETRY_DISABLED": "1",
|
||||
},
|
||||
out_dirs = ["dist"],
|
||||
tool = ":astro",
|
||||
)
|
||||
@@ -0,0 +1,48 @@
|
||||
# Eagle0pedia Site
|
||||
|
||||
Eagle0pedia is a Starlight static site. CI validates it through Bazel, while
|
||||
DigitalOcean App Platform builds and hosts it directly as a static site.
|
||||
|
||||
## Local Build
|
||||
|
||||
```bash
|
||||
bazel build //docs/eagle0pedia:site
|
||||
```
|
||||
|
||||
The generated site is written to:
|
||||
|
||||
```text
|
||||
bazel-bin/docs/eagle0pedia/dist
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
Eagle0pedia is hosted by DigitalOcean App Platform as a static site. Use these
|
||||
component settings:
|
||||
|
||||
```text
|
||||
Resource type: Static Site
|
||||
Source directory: docs/eagle0pedia
|
||||
Build command: corepack enable && pnpm install --frozen-lockfile && pnpm build
|
||||
Output directory: dist
|
||||
HTTP route: /
|
||||
Domain: pedia.eagle0.net
|
||||
```
|
||||
|
||||
The App Platform build uses the normal Starlight package scripts:
|
||||
|
||||
```text
|
||||
npm run build
|
||||
```
|
||||
|
||||
Set this App Platform environment variable:
|
||||
|
||||
```text
|
||||
ASTRO_TELEMETRY_DISABLED=1
|
||||
```
|
||||
|
||||
CI still validates the Bazel build:
|
||||
|
||||
```text
|
||||
bazel build //docs/eagle0pedia:site
|
||||
```
|
||||
@@ -0,0 +1,56 @@
|
||||
import { defineConfig } from "astro/config";
|
||||
import starlight from "@astrojs/starlight";
|
||||
|
||||
export default defineConfig({
|
||||
site: "https://pedia.eagle0.net",
|
||||
integrations: [
|
||||
starlight({
|
||||
title: "Eagle0pedia",
|
||||
description: "Reference guide for Eagle0 commands, systems, and game concepts.",
|
||||
customCss: ["./src/styles/custom.css"],
|
||||
editLink: {
|
||||
baseUrl: "https://github.com/nolen777/eagle0/edit/main/docs/eagle0pedia/src/content/docs/",
|
||||
},
|
||||
sidebar: [
|
||||
{
|
||||
label: "Reference",
|
||||
items: [
|
||||
{ label: "Eagle0pedia", slug: "" },
|
||||
{
|
||||
label: "Concepts",
|
||||
items: [
|
||||
{ label: "Game Concepts", slug: "concepts" },
|
||||
{ label: "Victory and Defeat", slug: "concepts/victory-and-defeat" },
|
||||
{ label: "Who You Command", slug: "concepts/command-authority" },
|
||||
{ label: "Eagle Round Flow", slug: "concepts/eagle-round-flow" },
|
||||
{ label: "March and Battle Timeline", slug: "concepts/march-timeline" },
|
||||
{ label: "Information and Recon", slug: "concepts/information-and-recon" },
|
||||
{ label: "From Eagle to Shardok", slug: "concepts/eagle-to-shardok" },
|
||||
{ label: "Shardok Battles", slug: "concepts/shardok-battles" },
|
||||
{ label: "Shardok Combat Fundamentals", slug: "concepts/shardok-combat" },
|
||||
{
|
||||
label: "Shardok Victory Conditions",
|
||||
slug: "concepts/shardok-victory-conditions",
|
||||
},
|
||||
],
|
||||
},
|
||||
{ label: "Eagle Commands", slug: "commands" },
|
||||
{ label: "Shardok Commands", slug: "shardok-commands" },
|
||||
{ label: "Heroes", slug: "heroes" },
|
||||
{ label: "Battalions", slug: "battalions" },
|
||||
{ label: "Factions", slug: "factions" },
|
||||
{ label: "Provinces", slug: "provinces" },
|
||||
{ label: "Integration Notes", slug: "integration" },
|
||||
],
|
||||
},
|
||||
],
|
||||
social: [
|
||||
{
|
||||
icon: "github",
|
||||
label: "GitHub",
|
||||
href: "https://github.com/nolen777/eagle0",
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "eagle0pedia",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@9.15.9",
|
||||
"scripts": {
|
||||
"build": "astro build",
|
||||
"dev": "astro dev",
|
||||
"preview": "astro preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/starlight": "^0.36.0",
|
||||
"astro": "^5.11.0",
|
||||
"sharp": "^0.34.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.8.0"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": []
|
||||
}
|
||||
}
|
||||
Generated
+4378
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,6 @@
|
||||
import { defineCollection } from "astro:content";
|
||||
import { docsSchema } from "@astrojs/starlight/schema";
|
||||
|
||||
export const collections = {
|
||||
docs: defineCollection({ schema: docsSchema() }),
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
title: Battalions
|
||||
description: Troop count, type, training, armament, morale, heroes, and the battalion lifecycle.
|
||||
---
|
||||
|
||||
Battalions are Eagle's organized troop formations and become units in [Shardok battles](/concepts/shardok-battles/) when attached to heroes.
|
||||
|
||||
## Reading a battalion
|
||||
|
||||
- **Troops** are its current soldiers. Casualties and starvation reduce this number; reaching zero destroys the battalion.
|
||||
- **Type** determines movement, damage, battlefield limits, and whether an attached hero can cast or use stealth.
|
||||
- **TRN (training)** increases the damage the battalion delivers. It is raised with [Train](/commands/#train).
|
||||
- **ARM (armament)** reduces normal damage the battalion takes. It is raised with [Arm Troops](/commands/#arm-troops) and cannot exceed the province's effective infrastructure.
|
||||
- **Morale** affects combat performance and gates many tactical actions. It changes through battle effects.
|
||||
|
||||
## How the combat factors work
|
||||
|
||||
These values do different jobs; they are not interchangeable ratings that simply add into one combat score.
|
||||
|
||||
- **Training** scales the battalion type's melee and missile damage bonuses. Higher training therefore means more damage per troop. It also affects some command-specific odds, including the chance to stun on a [Charge](/shardok-commands/#charge).
|
||||
- **Armament** scales the battalion type's resistances and reduces incoming normal damage. Each type and damage category has its own resistance profile, so the same ARM value does not protect every battalion equally. Penetrating damage bypasses this normal armament reduction. If the unit can use [Archery](/shardok-commands/#archery), armament also determines its starting volleys: currently one volley per 10 ARM, rounded down, plus any bonus from its battalion type.
|
||||
- **Morale** changes the battalion's damage swing. Most units also need at least 10 morale to use normal tactical commands.
|
||||
- **Hero vigor** is an action constraint and current energy reserve, not battalion armor. It affects maximum AP and command access; at zero vigor, the hero-led unit is captured and removed from active battle. See [Vigor](/heroes/#vigor).
|
||||
- **Troop count** multiplies battalion damage: more surviving troops deliver more total battalion damage. It also determines how many soldiers remain available to absorb casualties.
|
||||
- **Battalion type** supplies the base damage, training bonus, resistance profile, armament benefit, movement costs, charge multiplier, and special capabilities.
|
||||
- **Hero stats** contribute separately. Charisma and bravery affect battalion combat swings, while a hero also contributes personal damage and enables stat- or profession-based commands.
|
||||
|
||||
Exact enemy training, armament, hero stats, or other details may be hidden. Use only the information your faction has revealed; the game does not promise that an unknown value is zero.
|
||||
|
||||
## Battalion types
|
||||
|
||||
### Light Infantry
|
||||
|
||||
The basic flexible foot formation. It is inexpensive, has the largest normal infantry capacity, can carry casting heroes, and allows [Ranger](/heroes/#ranger) stealth. Its protection gains from ARM are useful but lower than those of heavy formations.
|
||||
|
||||
### Heavy Infantry
|
||||
|
||||
Slower and more expensive to train and equip, with fewer troops per full battalion. Heavy Infantry starts with substantial physical resistance and generally gains more protection from ARM than light formations. It also delivers heavier melee damage, but cannot cast or hide.
|
||||
|
||||
### Light Cavalry
|
||||
|
||||
Fast on open terrain and especially effective when charging: its charge deals 1.2 times normal unit damage. A charge spends only the terrain-entry cost, so Light Cavalry may move or use another available command afterward when it retains AP.
|
||||
|
||||
### Heavy Cavalry
|
||||
|
||||
The most expensive normal formation, with the smallest capacity and highest food cost. It combines strong built-in resistance and armament scaling with the largest charge multiplier, 1.5. Like Light Cavalry, it can sometimes act again after charging if AP remains.
|
||||
|
||||
### Longbowmen
|
||||
|
||||
A ranged specialist that is always [archery](/shardok-commands/#archery)-capable and receives two bonus volleys in addition to those provided by ARM. It can also support casting and stealth-capable heroes. Longbow attacks include penetrating missile damage. Because penetrating damage bypasses normal armament reduction, trained Longbowmen can inflict heavy casualties even on well-armed troops. They are comparatively poorly protected by their own ARM.
|
||||
|
||||
### Undead
|
||||
|
||||
An exceptional formation created through necromantic effects rather than ordinary recruitment. Undead have high capacity, do not consume food or use normal morale, can cross ocean terrain, and are vulnerable to [Holy Wave](/shardok-commands/#holy-wave). Their combat and movement rules differ enough that they should not be evaluated like a normal recruitable battalion.
|
||||
|
||||
## Heroes and units
|
||||
|
||||
A Shardok unit normally combines one hero and one battalion. The hero supplies stats, [vigor](/heroes/#vigor), profession commands, personal damage, and special capabilities; the battalion supplies troops, type, training, armament, movement, morale, and its damage profile. The hero's Charisma and Bravery also affect the battalion's combat swing. A profession alone may not be enough: casting requires a casting-capable battalion, and Ranger stealth requires a stealth-capable one.
|
||||
|
||||
The two parts remain one tactical unit. If the battalion reaches zero troops, it is destroyed. If the attached hero reaches zero vigor, the hero-led unit is captured and removed from active battle; the surviving battalion does not detach and continue fighting independently.
|
||||
|
||||
## Creating and changing battalions
|
||||
|
||||
[**Organize Troops**](/commands/#organize-troops) creates, resizes, merges, transfers, or dismisses battalions. New troops cost gold and begin without training or armament. Transfers preserve weighted training and armament. Empty battalions are removed.
|
||||
|
||||
Use [**Train**](/commands/#train) to improve eligible battalions in the province and [**Arm Troops**](/commands/#arm-troops) while visiting town to buy higher armament. Province economy and agriculture affect which types can be raised; effective infrastructure limits equipment.
|
||||
|
||||
## Movement, food, and battle
|
||||
|
||||
[March](/commands/#march) moves selected battalions with their heroes and supplies. Stationed and moving battalions consume food. A shortage spends the remaining food and can reduce battalions by up to 20 percent in one consumption event.
|
||||
|
||||
Battle results return surviving troop counts and statuses to Eagle. Battalions may survive with casualties, be destroyed, flee with their heroes, be captured/secured, or become the new garrison after conquest.
|
||||
|
||||
See [Organize Troops](/commands/#organize-troops), [Train](/commands/#train), [Arm Troops](/commands/#arm-troops), and [Shardok Battles](/concepts/shardok-battles/#units).
|
||||
@@ -0,0 +1,644 @@
|
||||
---
|
||||
title: Eagle Commands
|
||||
description: Strategic command reference for Eagle command panels.
|
||||
---
|
||||
|
||||
Eagle commands are the strategic actions a faction can take from a province command panel. The server advertises available commands with `AvailableCommand`, and the client posts the matching `SelectedCommand`.
|
||||
|
||||
Most commands belong to the province currently acting this turn. Some are reaction commands, such as resolving diplomacy offers, handling captured heroes, or choosing a battle aftermath decision.
|
||||
|
||||
<span class="command-reference-marker" aria-hidden="true"></span>
|
||||
|
||||
<!--
|
||||
Source map for numeric command details:
|
||||
- Alms: src/main/resources/net/eagle0/eagle/settings.tsv fields almsSupportIncreasePerFood=0.01, almsPaladinSupportMultiplier=4, maxAlmsFood=1000; applied in src/main/scala/net/eagle0/eagle/library/actions/impl/command/AlmsCommand.scala and src/main/scala/net/eagle0/eagle/library/actions/availability/AvailableAlmsCommandFactory.scala.
|
||||
- Feast: settings fields loyaltyGainFromFeast=10, vigorGainFromFeast=10, feastGoldCostPerHero=20; applied in src/main/scala/net/eagle0/eagle/library/actions/impl/command/FeastCommand.scala and src/main/scala/net/eagle0/eagle/library/actions/availability/AvailableFeastCommandFactory.scala.
|
||||
- Hero Gift: settings fields loyaltyIncreasePerGold=0.25 and maxGiftGold=100; applied in src/main/scala/net/eagle0/eagle/library/actions/impl/command/HeroGiftCommand.scala and src/main/scala/net/eagle0/eagle/library/actions/availability/AvailableHeroGiftCommandFactory.scala.
|
||||
- Rest: settings field restVigorGain=15; applied in src/main/scala/net/eagle0/eagle/library/actions/impl/command/RestCommand.scala.
|
||||
- Vigor costs: src/main/resources/net/eagle0/eagle/settings.tsv fields actionVigorCost=15, reconVigorCost=30, apprehendOutlawVigorCost=30, controlWeatherVigorDelta=-40, startEpidemicVigorDelta=-30, vigorGainFromFeast=10, restVigorGain=15.
|
||||
- Divine: divinable heroes are unaffiliated heroes with RecruitmentInfo.NotDivined; see src/main/scala/net/eagle0/eagle/library/actions/availability/AvailableDivineCommandsFactory.scala and src/main/scala/net/eagle0/eagle/library/actions/impl/command/DivineCommand.scala.
|
||||
- Issue Orders: order availability comes from src/main/scala/net/eagle0/eagle/library/actions/availability/AvailableIssueOrdersCommandFactory.scala; vassal order behavior comes from src/main/scala/net/eagle0/eagle/library/actions/impl/action/PerformVassalCommandsPhaseAction.scala, src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers/CommandChoiceHelpers.scala, and src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers/ExpandCommandSelector.scala.
|
||||
- Send Supplies: shipment scheduling comes from src/main/scala/net/eagle0/eagle/library/actions/impl/command/SendSuppliesCommand.scala; arrival comes from src/main/scala/net/eagle0/eagle/library/actions/impl/action/PerformProvinceMoveResolutionAction.scala and src/main/scala/net/eagle0/eagle/library/actions/impl/action/ShipmentArrivedAction.scala; timing/loss settings are src/main/resources/net/eagle0/eagle/settings.tsv fields turnsToResolveShipment=1 and shipSuppliesLoss=0.1.
|
||||
- Battle Aftermath Decision: availability comes from src/main/scala/net/eagle0/eagle/library/actions/availability/AvailableBattleAftermathDecisionCommandFactory.scala; selection comes from src/main/scala/net/eagle0/eagle/library/actions/impl/command/BattleAftermathDecisionCommand.scala; finalization comes from src/main/scala/net/eagle0/eagle/library/actions/impl/action/FinalizeAftermathAction.scala.
|
||||
- Manage Prisoners: availability comes from src/main/scala/net/eagle0/eagle/library/actions/availability/AvailableManagePrisonersCommandFactory.scala; option effects come from src/main/scala/net/eagle0/eagle/library/actions/impl/command/ManagePrisonersCommand.scala; prisoner move/return timing comes from src/main/scala/net/eagle0/eagle/library/actions/impl/action/EndPlayerCommandsPhaseAction.scala; settings include prisonerEscapeChance=0.01.
|
||||
- Handle Captured Hero: availability comes from src/main/scala/net/eagle0/eagle/library/actions/availability/AvailableHandleCapturedHeroCommandFactory.scala; option effects come from src/main/scala/net/eagle0/eagle/library/actions/impl/command/HandleCapturedHeroesCommand.scala; deferred aftermath effects come from src/main/scala/net/eagle0/eagle/library/actions/impl/action/EndBattleAftermathPhaseAction.scala; settings include startingLoyalty=60 and truceMonthsFromReturningLeader=12.
|
||||
-->
|
||||
|
||||
## Normal Command Phase Commands
|
||||
|
||||
Most command phase actions spend the acting province's turn.
|
||||
|
||||
### Alms
|
||||
|
||||
<b>Hero requirements:</b> None. [Paladins](/heroes/#paladin) get a bonus.
|
||||
|
||||
<b>Vigor cost:</b> 15.
|
||||
|
||||
<b>Available when:</b> the acting province has food to give and an eligible hero with enough vigor.
|
||||
|
||||
<b>Effect:</b> Give up to 1000 food from the acting province as charity. The province gains 1 [support](/provinces/#support) per 100 food, capped by the province's maximum 100 support. Paladins multiply the support gain by 4, so a Paladin gives 4 support per 100 food. The acting hero spends vigor and gains charisma XP.
|
||||
|
||||
<b>Selection:</b> choose an amount of food and the acting hero.
|
||||
|
||||
### Apprehend Outlaw
|
||||
|
||||
<b>Hero requirements:</b> None.
|
||||
|
||||
<b>Vigor cost:</b> 30.
|
||||
|
||||
<b>Available when:</b> an [outlaw hero](/factions/#free-heroes) is present in the acting province and an eligible hero has enough vigor.
|
||||
|
||||
<b>Effect:</b> Attempt to capture the outlaw. On success, the outlaw is removed from the province's unaffiliated heroes and re-added as a prisoner with a bias against the imprisoning faction. The acting hero spends vigor, and the game creates a deferred notification/story request.
|
||||
|
||||
<b>Selection:</b> choose the outlaw, an optional battalion, and the acting hero.
|
||||
|
||||
### Control Weather
|
||||
|
||||
<b>Hero requirements:</b> [Mage](/heroes/#mage).
|
||||
|
||||
<b>Vigor cost:</b> 40.
|
||||
|
||||
<b>Available when:</b> a Mage has enough vigor and the acting province or an adjacent province has a valid weather change.
|
||||
|
||||
<b>Effect:</b> Change weather in a target province. Available options can start or end blizzards and droughts. The target province receives a deferred weather change, and the acting hero records a weather backstory event, spends or gains the configured vigor delta, and gains wisdom XP.
|
||||
|
||||
<b>Selection:</b> choose the weather control type, target province, and acting hero.
|
||||
|
||||
### Diplomacy
|
||||
|
||||
<b>Hero requirements:</b> None.
|
||||
|
||||
<b>Vigor cost:</b> 15 from the messenger hero.
|
||||
|
||||
<b>Available when:</b> a diplomatic option is valid against another faction and an eligible messenger hero can be sent.
|
||||
|
||||
<b>Effect:</b> Send a hero to another faction with a diplomatic proposal. The target faction receives the offer at the end of the current round, after any battles this round have taken place. Truce, invitation, alliance, break-alliance, and ransom options create [incoming diplomacy offers](/factions/#incoming-diplomacy-offers) for the target faction and story-generation requests. Most diplomacy options spend gold and cost the messenger vigor.
|
||||
|
||||
For truce, invitation, alliance, and break-alliance missions, the envoy leaves the acting province immediately and is unavailable for the rest of the current round, including its battles. The envoy normally returns during **Diplomacy Resolution** near the end of that same round and can act again next round. If the origin province has been lost, the envoy returns elsewhere in the faction when possible. An imprisoned envoy does not return.
|
||||
|
||||
<b>Selection:</b> choose the diplomacy option, target faction, and sent hero.
|
||||
|
||||
### Exile Vassal
|
||||
|
||||
<b>Hero requirements:</b> None.
|
||||
|
||||
<b>Vigor cost:</b> None.
|
||||
|
||||
<b>Available when:</b> this province has at least one [vassal](/heroes/#vassals).
|
||||
|
||||
<b>Effect:</b> Remove the selected vassal from your faction.
|
||||
|
||||
<b>Selection:</b> choose the hero to exile.
|
||||
|
||||
### Feast
|
||||
|
||||
<b>Hero requirements:</b> None.
|
||||
|
||||
<b>Vigor cost:</b> None. This restores up to 10 vigor for each ruling-faction hero in the province.
|
||||
|
||||
<b>Gold cost:</b> 20 gold for each ruling-faction hero in the province, multiplied by the province [price index](/provinces/#price-index) and rounded up. For example, a province with 3 ruling-faction heroes and a 1.25 price index pays 75 gold.
|
||||
|
||||
<b>Available when:</b> the acting province can pay the full feast cost.
|
||||
|
||||
<b>Effect:</b> Hold a feast in the acting province. Every ruling-faction hero in the province gains up to 10 loyalty, capped at 100, and up to 10 vigor, capped by constitution. Faction leaders do not gain loyalty, but can still gain vigor. The province spends gold, the price index may change, and feast-spending quest counters can advance.
|
||||
|
||||
<b>Selection:</b> no additional choices.
|
||||
|
||||
### Hero Gift
|
||||
|
||||
<b>Hero requirements:</b> None.
|
||||
|
||||
<b>Vigor cost:</b> None.
|
||||
|
||||
<b>Available when:</b> the province has a non-leader hero with less than 100 LOY and gold available to give.
|
||||
|
||||
<b>Effect:</b> Send up to 100 gold as a gift to the selected hero. Loyalty gain depends on the gold amount, the recipient province's price index, and the hero's current loyalty: the base gain is 0.25 loyalty per gold, divided by price index, capped by the remaining room to 100 LOY.
|
||||
|
||||
<b>Selection:</b> choose the recipient hero and amount.
|
||||
|
||||
### Improve
|
||||
|
||||
<b>Hero requirements:</b> None. [Engineers](/heroes/#engineer) get a bonus.
|
||||
|
||||
<b>Vigor cost:</b> 15.
|
||||
|
||||
<b>Available when:</b> a hero has enough vigor and the province has an available improvement or devastation repair option.
|
||||
|
||||
<b>Effect:</b> Improve a province using one of the available improvement types. [Economy, agriculture, and infrastructure](/provinces/#development-stats) rise toward 100; [devastation](/provinces/#devastation) repair reduces all three devastation tracks. The acting hero spends vigor and gains strength/agility XP, and the improvement can optionally be locked as the province's preferred type.
|
||||
|
||||
<b>Selection:</b> choose the improvement type, acting hero, and whether to lock the type.
|
||||
|
||||
### Issue Orders
|
||||
|
||||
<b>Hero requirements:</b> None.
|
||||
|
||||
<b>Vigor cost:</b> None. This does not take your turn.
|
||||
|
||||
<b>Available when:</b> a faction leader is acting and your faction controls more than one province. This command does not take your turn.
|
||||
|
||||
<b>Effect:</b> Set [standing orders](/provinces/#province-orders) for your provinces. Orders guide vassal-ruled provinces during the [Vassal Commands phase](/concepts/eagle-round-flow/#vassal-commands); provinces led directly by faction leaders are still commanded directly. Vassals may still handle urgent problems first, such as defense, riots, severe fatigue, low support, beast suppression, loyalty management, or other immediately available crisis commands.
|
||||
|
||||
<b>Selection:</b> submit the new province orders and the new focus province, or clear the focus province.
|
||||
|
||||
#### Order Meanings
|
||||
|
||||
##### Entrust
|
||||
|
||||
Use the vassal's judgment. If the province borders an enemy, the vassal tends to act like **Mobilize**. Otherwise, the vassal tends to act like **Develop**.
|
||||
|
||||
##### Develop
|
||||
|
||||
Build up the province. The vassal may send away surplus gold and food toward your focus province, if one is set, or otherwise toward the nearest faction leader. Then it prefers **Improve** actions that raise economy, agriculture, infrastructure, or repair devastation. If nothing useful is available, the province rests.
|
||||
|
||||
##### Mobilize
|
||||
|
||||
Prepare for war. The vassal may ship excess gold and food toward your focus province, if one is set, or otherwise toward the nearest faction leader. Then it may organize troops, arm troops, improve infrastructure when it limits training, train battalions, and fall back to improvement or rest.
|
||||
|
||||
##### Expand
|
||||
|
||||
Look for growth or attack opportunities. The vassal first tries to march into neutral provinces, usually leaving enough heroes behind to keep the origin province held. If expansion is not practical, it may spread heroes among your existing provinces, attack enemies when appropriate, improve the province, or rest.
|
||||
|
||||
### March
|
||||
|
||||
<b>Hero requirements:</b> None.
|
||||
|
||||
<b>Vigor cost:</b> 15 from each marching hero.
|
||||
|
||||
<b>Available when:</b> the acting province has movable heroes, battalions, gold, or food and at least one valid destination.
|
||||
|
||||
<b>Effect:</b> Move heroes, battalions, gold, and food from one province to another. March is inter-province movement and always has a destination; [Visit Town](#visit-town) is only a local visit within the current province. The selected units and supplies leave the origin province immediately, each marching hero spends vigor and gains wisdom XP, and the destination receives an [incoming army](/provinces/#incoming-armies) scheduled for the next round.
|
||||
|
||||
If the destination is already ruled by your faction when the army arrives, the heroes, battalions, gold, and food enter the province during the next round's Province Move Resolution phase, before riots, vassal commands, and player commands.
|
||||
|
||||
If the destination is neutral or hostile, the army does not simply appear in the province at Province Move Resolution. It is set up as a hostile army later in that same next round, after player commands. A neutral province can then be taken in the Uncontested Conquest phase; a defended province can lead into attack, defense, battle request, and battle resolution phases. Until one of those outcomes resolves, the marching heroes are still in the moving army rather than ruling the destination.
|
||||
|
||||
<!-- Sources:
|
||||
- March scheduling: src/main/scala/net/eagle0/eagle/library/actions/impl/command/MarchCommand.scala
|
||||
- Friendly arrival timing: src/main/scala/net/eagle0/eagle/library/actions/impl/action/PerformProvinceMoveResolutionAction.scala
|
||||
- Hostile setup and neutral conquest timing:
|
||||
src/main/scala/net/eagle0/eagle/library/actions/impl/action/PerformHostileArmySetupAction.scala
|
||||
src/main/scala/net/eagle0/eagle/library/actions/impl/action/PerformUncontestedConquestAction.scala
|
||||
- Phase order: src/main/scala/net/eagle0/eagle/model/state/RoundPhase.scala
|
||||
-->
|
||||
|
||||
<b>Selection:</b> choose origin, destination, gold, food, and marching units.
|
||||
|
||||
### Organize Troops
|
||||
|
||||
<b>Hero requirements:</b> None.
|
||||
|
||||
<b>Vigor cost:</b> None.
|
||||
|
||||
<b>Available when:</b> the acting province has battalions to manage, troops to reorganize, or gold to raise new troops.
|
||||
|
||||
<b>Effect:</b> Create, resize, merge, transfer, or dismiss battalions in the acting province. New troops cost gold and enter with no training or armament, while transferred troops preserve weighted training and armament. Empty battalions are destroyed, new battalions get generated names and initial backstory requests, and the province price index may change from the gold spend.
|
||||
|
||||
<b>Selection:</b> submit changed battalions and new battalions, including troop movements and new troop counts.
|
||||
|
||||
### Recon
|
||||
|
||||
<b>Hero requirements:</b> [Ranger](/heroes/#ranger).
|
||||
|
||||
<b>Vigor cost:</b> 30.
|
||||
|
||||
<b>Available when:</b> a Ranger has enough vigor and there is a valid target province to scout.
|
||||
|
||||
<b>Effect:</b> Send a hero to scout another province.
|
||||
|
||||
<b>Selection:</b> choose the acting hero and target province.
|
||||
|
||||
### Rest
|
||||
|
||||
<b>Hero requirements:</b> None.
|
||||
|
||||
<b>Vigor cost:</b> None. This restores up to 15 vigor for each ruling-faction hero in the province.
|
||||
|
||||
<b>Available when:</b> the acting province can spend its turn resting.
|
||||
|
||||
<b>Effect:</b> Have the acting province rest for the turn. Faction heroes in the province regain up to 15 vigor, capped by constitution, and rest-related quest counters can advance.
|
||||
|
||||
<b>Selection:</b> no additional choices.
|
||||
|
||||
### Send Supplies
|
||||
|
||||
<b>Hero requirements:</b> None.
|
||||
|
||||
<b>Vigor cost:</b> 15.
|
||||
|
||||
<b>Available when:</b> the acting province has supplies to send, an eligible hero, and a valid destination province.
|
||||
|
||||
<b>Effect:</b> Send gold and food from the acting province to another province. The origin province loses the full selected amount immediately, and the destination receives an [incoming shipment](/provinces/#incoming-shipments) scheduled for the next round.
|
||||
|
||||
Shipments arrive during the next round's Province Move Resolution phase, before riots, hero departures, vassal commands, player commands, battles, and food consumption. That means food shipped this way is available to feed the destination province later in the same round it arrives. When the shipment arrives, 10% is lost in transit and the destination receives the remaining gold and food, rounded down.
|
||||
|
||||
<b>Selection:</b> choose gold, food, acting hero, and destination province.
|
||||
|
||||
### Start Epidemic
|
||||
|
||||
<b>Hero requirements:</b> [Necromancer](/heroes/#necromancer).
|
||||
|
||||
<b>Vigor cost:</b> 30.
|
||||
|
||||
<b>Available when:</b> a Necromancer has enough vigor and the acting province or an adjacent province can receive an epidemic.
|
||||
|
||||
<b>Effect:</b> Start an epidemic in the target province.
|
||||
|
||||
<b>Selection:</b> choose the target province and acting hero.
|
||||
|
||||
### Suppress Beasts
|
||||
|
||||
<b>Hero requirements:</b> None. Heroes with any profession get a power bonus.
|
||||
|
||||
<b>Vigor cost:</b> 15 on success, plus any vigor lost to casualties. If the hero is killed, the command fails.
|
||||
|
||||
<b>Available when:</b> the acting province has a [beast event](/provinces/#province-events) and an eligible hero.
|
||||
|
||||
<b>Effect:</b> Send a hero, optionally with a battalion, to suppress beasts in the acting province. The fight rolls beast power, applies casualties to the battalion and/or hero, removes the beast event on success or failure, and creates a story notification. Success can award gold, food, support, hero XP, and battalion backstory; failure can kill the hero and destroy the battalion.
|
||||
|
||||
<b>Selection:</b> choose the hero and optional battalion.
|
||||
|
||||
### Swear Kinship
|
||||
|
||||
<b>Hero requirements:</b> The target hero must have at least 95 loyalty.
|
||||
|
||||
<b>Vigor cost:</b> None.
|
||||
|
||||
<b>Available when:</b> the faction head is in the province, your faction has fewer than 5 faction leaders, and another hero in the province has at least 95 loyalty.
|
||||
|
||||
<b>Effect:</b> Choose a hero for a kinship bond. A hero who swears kinship becomes a [faction leader](/heroes/#faction-leaders).
|
||||
|
||||
<b>Selection:</b> choose the hero to swear kinship with.
|
||||
|
||||
### Train
|
||||
|
||||
<b>Hero requirements:</b> None. [Champions](/heroes/#champion) get a bonus.
|
||||
|
||||
<b>Vigor cost:</b> 15.
|
||||
|
||||
<b>Available when:</b> a hero has enough vigor and the province has at least one battalion with less than 100 TRN.
|
||||
|
||||
<b>Effect:</b> Raise the TRN of all trainable battalions in the province. The more troops are present, the lower the amount raised. The acting hero spends vigor and gains charisma, strength, and agility XP.
|
||||
|
||||
<b>Selection:</b> choose the acting hero.
|
||||
|
||||
### Visit Town
|
||||
|
||||
<b>Hero requirements:</b> None. The acting province's ruling hero makes the visit.
|
||||
|
||||
<b>Vigor cost:</b> 15.
|
||||
|
||||
<b>Available when:</b> the acting province's ruling hero has at least 40 vigor, the hero is not already visiting town, and there is no blizzard.
|
||||
|
||||
<b>Effect:</b> Have the ruling hero leave camp to visit the town inside the acting province. Visit Town does not move any hero, battalion, or supplies to another province, so it has no destination; use [March](#march) for inter-province movement.
|
||||
|
||||
Visit Town spends the ruling hero's vigor and unlocks whichever town commands below are currently eligible. You can use as many eligible town commands as you want in the same round before using Return to go back to camp. If no town activity currently qualifies, Return may be the only command available.
|
||||
|
||||
<b>Selection:</b> no additional choices. Visit Town always uses the current province and never asks you to select a destination.
|
||||
|
||||
## Town Commands
|
||||
|
||||
You must use Visit Town before these local commands appear. The ruling hero remains in the current province; only the available command set changes from camp duties to town activities. While visiting town, you can use as many eligible town commands as you want in one round before using Return to come back to camp.
|
||||
|
||||
### Arm Troops
|
||||
|
||||
<b>Hero requirements:</b> None.
|
||||
|
||||
<b>Vigor cost:</b> None.
|
||||
|
||||
<b>Available when:</b> the ruling hero is visiting town, the province has gold, and at least one battalion can raise ARM without exceeding effective infrastructure.
|
||||
|
||||
<b>Effect:</b> Spend gold to raise armament on existing battalions. New armament must be higher than the battalion's current armament and cannot exceed the province's effective infrastructure. The province spends gold, the price index may change, and each armed battalion records a backstory event.
|
||||
|
||||
<b>Selection:</b> choose the battalions to arm and how much armament each receives.
|
||||
|
||||
### Decline Quest
|
||||
|
||||
<b>Hero requirements:</b> None.
|
||||
|
||||
<b>Vigor cost:</b> None.
|
||||
|
||||
<b>Available when:</b> the ruling hero is visiting town and an [unaffiliated hero](/heroes/#unaffiliated-heroes) quest is available in the province.
|
||||
|
||||
<b>Effect:</b> Decline one of the unaffiliated hero quests available in the acting province.
|
||||
|
||||
<b>Selection:</b> choose the hero whose quest should be declined.
|
||||
|
||||
### Divine
|
||||
|
||||
<b>Hero requirements:</b> None.
|
||||
|
||||
<b>Vigor cost:</b> None.
|
||||
|
||||
<b>Gold cost:</b> 100 gold per selected hero, multiplied by the province [price index](/provinces/#price-index) and rounded to the nearest whole gold for each hero. The total cost is that per-hero cost multiplied by the number of selected heroes. For example, divining 3 heroes at a 1.25 price index costs 375 gold.
|
||||
|
||||
<b>Available when:</b> the ruling hero is visiting town, the province can pay the cost for at least one hero, and there are free heroes who are not yet ready to join your faction but might be won over.
|
||||
|
||||
<b>Effect:</b> Spend gold to divine one or more [free heroes](/factions/#free-heroes). For each divined hero, you receive details about what could impress them enough to become recruitable. Fulfill that ask, and the hero can later appear under Recruit Heroes. The province spends gold, and the price index may change.
|
||||
|
||||
<b>Selection:</b> choose the heroes to divine.
|
||||
|
||||
### Manage Prisoners
|
||||
|
||||
<b>Hero requirements:</b> None.
|
||||
|
||||
<b>Vigor cost:</b> None.
|
||||
|
||||
<b>Available when:</b> the ruling hero is visiting town and the province has prisoner heroes with available management options.
|
||||
|
||||
<b>Effect:</b> Choose what happens to ordinary prisoners in the province. These are heroes who are already in the prisoner state, not newly captured heroes still waiting for a battle aftermath decision.
|
||||
|
||||
<b>Selection:</b> choose the prisoner and management option.
|
||||
|
||||
#### Options
|
||||
|
||||
##### Execute
|
||||
|
||||
<b>Effect:</b> Kill the prisoner and remove them from the game.
|
||||
|
||||
##### Exile
|
||||
|
||||
<b>Available when:</b> the prisoner is not a faction leader.
|
||||
|
||||
<b>Effect:</b> Turn the prisoner into an outlaw in the same province.
|
||||
|
||||
##### Release
|
||||
|
||||
<b>Available when:</b> the prisoner is not a faction leader.
|
||||
|
||||
<b>Effect:</b> Release the prisoner as a traveler in the same province. Released prisoners are no longer in your custody, but they do not automatically join another faction.
|
||||
|
||||
##### Move
|
||||
|
||||
<b>Available when:</b> you control an adjacent province.
|
||||
|
||||
<b>Effect:</b> Send the prisoner to one adjacent province ruled by your faction. The prisoner leaves the origin province now and arrives at the destination at the end of the Player Commands phase. There is a 1% chance the prisoner escapes during the move; if that happens, they become an outlaw in the destination province instead of remaining a prisoner.
|
||||
|
||||
##### Return
|
||||
|
||||
<b>Available when:</b> the prisoner is a faction leader and their faction still controls at least one province.
|
||||
|
||||
<b>Effect:</b> Return the prisoner to their faction. They leave your custody now and rejoin their faction at the end of the Player Commands phase as a ruling hero in one of that faction's provinces.
|
||||
|
||||
Ordinary prisoners cannot be returned this way. Faction leader prisoners cannot be released or exiled, but they can be executed, moved within your territory, or returned if their faction still exists.
|
||||
|
||||
### Recruit Heroes
|
||||
|
||||
<b>Hero requirements:</b> None.
|
||||
|
||||
<b>Vigor cost:</b> None.
|
||||
|
||||
<b>Available when:</b> the province is ruled by one of the acting faction's faction leaders, that ruling faction leader is visiting town, and at least one unaffiliated hero in the province currently qualifies to join that faction. The visitor may be any faction leader; it does not have to be the faction head. Vassals cannot use this command.
|
||||
|
||||
<b>Effect:</b> Recruit available unaffiliated heroes in the acting province. Recruited heroes join the faction at starting loyalty, move from the province's unaffiliated hero list into its ruling faction heroes, and receive a recruitment backstory event.
|
||||
|
||||
<b>Selection:</b> choose the heroes to recruit.
|
||||
|
||||
See [**What `WOULD_JOIN` Means**](/factions/#what-would_join-means) for the recruitment status, traveler exception, and separate **Please Recruit Me** behavior in vassal-ruled provinces.
|
||||
|
||||
### Return
|
||||
|
||||
<b>Hero requirements:</b> None.
|
||||
|
||||
<b>Vigor cost:</b> None.
|
||||
|
||||
<b>Available when:</b> the ruling hero is visiting town.
|
||||
|
||||
<b>Effect:</b> Return the ruling hero from the current province's town to camp and end that province's turn.
|
||||
|
||||
<b>Selection:</b> no additional choices.
|
||||
|
||||
### Trade
|
||||
|
||||
<b>Hero requirements:</b> None.
|
||||
|
||||
<b>Vigor cost:</b> None.
|
||||
|
||||
<b>Available when:</b> the ruling hero is visiting town and the province has enough gold or food for at least one trade.
|
||||
|
||||
<b>Effect:</b> Buy or sell food using the acting province's gold and food stores. Food buy and sell prices are based on the province price index. Buying spends gold and adds food; selling removes food and adds gold. The price index may change after the trade.
|
||||
|
||||
<b>Selection:</b> choose buy or sell and the amount of food.
|
||||
|
||||
## Special and Conditional Commands
|
||||
|
||||
These commands appear only when the game is resolving a special situation, response, or pending decision.
|
||||
|
||||
### Attack Decision
|
||||
|
||||
<b>Hero requirements:</b> None.
|
||||
|
||||
<b>Vigor cost:</b> None.
|
||||
|
||||
<b>Available when:</b> an attack decision is pending.
|
||||
|
||||
<b>Effect:</b> Choose how to respond to a pending attack opportunity or battle decision. The command shows involved armies, acting units, and the available attack decision types. **Advance** commits the army to the attack. **Withdraw** sends it back without fighting. **Demand Tribute** lets you request a specific amount of the province's gold and food, up to the displayed maximum; the demand cannot be zero. The defender later chooses whether to pay the full demand or refuse it. See [Attack and Defense Decisions](/concepts/eagle-to-shardok/#attack-and-defense-decisions) and [Resolving a tribute demand](/concepts/march-timeline/#resolving-a-tribute-demand).
|
||||
|
||||
<b>Selection:</b> choose one available attack decision.
|
||||
|
||||
### Defend
|
||||
|
||||
<b>Hero requirements:</b> None.
|
||||
|
||||
<b>Vigor cost:</b> None.
|
||||
|
||||
<b>Available when:</b> a province is under attack and can commit defenders.
|
||||
|
||||
<b>Effect:</b> Respond to an incoming attack by committing a defending army. The selected defenders become the province's defending army. If flee provinces are available, one must be selected as the army's fallback destination. See [Attack and Defense Decisions](/concepts/eagle-to-shardok/#attack-and-defense-decisions) for what happens next.
|
||||
|
||||
<b>Selection:</b> choose defending units and, when available, an optional flee province.
|
||||
|
||||
### Handle Captured Hero
|
||||
|
||||
<b>Hero requirements:</b> None.
|
||||
|
||||
<b>Vigor cost:</b> None.
|
||||
|
||||
<b>Available when:</b> a newly captured hero needs a battle aftermath decision.
|
||||
|
||||
<b>Effect:</b> Decide what happens to a hero captured during [battle aftermath](/concepts/eagle-to-shardok/#aftermath) or conquest. Captured heroes are not yet ordinary prisoners. If you imprison one, they become a normal prisoner and can later be handled with Manage Prisoners.
|
||||
|
||||
<b>Selection:</b> choose the captured hero and the handling option.
|
||||
|
||||
#### Options
|
||||
|
||||
##### Recruit
|
||||
|
||||
<b>Available when:</b> the captured hero is not a faction leader, has not already refused recruitment, and is not someone who will never join your faction.
|
||||
|
||||
<b>Effect:</b> Try to persuade the captured hero to join you immediately. The chance depends on your faction head, your faction's prestige, and the captured hero. On success, the hero joins your faction in the captured province with 60 loyalty. On failure, the hero refuses; you cannot try to recruit that captured hero again from this aftermath decision and must choose another option.
|
||||
|
||||
##### Imprison
|
||||
|
||||
<b>Effect:</b> Keep the hero as a prisoner in the captured province. This converts them into an ordinary prisoner when the aftermath resolves.
|
||||
|
||||
##### Exile
|
||||
|
||||
<b>Available when:</b> the captured hero is not a faction leader whose faction still controls a province.
|
||||
|
||||
<b>Effect:</b> Cast the hero out as an outlaw in the captured province when the aftermath resolves.
|
||||
|
||||
##### Execute
|
||||
|
||||
<b>Effect:</b> Kill the captured hero and remove them from the game when the aftermath resolves.
|
||||
|
||||
##### Return
|
||||
|
||||
<b>Available when:</b> the captured hero is a faction leader and their faction still controls at least one province.
|
||||
|
||||
<b>Effect:</b> Return the captured faction leader to the closest province ruled by their old faction when the aftermath resolves. Returning a captured leader creates a 12-month truce between your faction and theirs.
|
||||
|
||||
### Handle Riot
|
||||
|
||||
<b>Hero requirements:</b> None.
|
||||
|
||||
<b>Vigor cost:</b> Depends on the option. Crack Down can cost vigor; Give and Do Nothing do not.
|
||||
|
||||
<b>Available when:</b> a province has a riot.
|
||||
|
||||
<b>Effect:</b> Respond to a [riot](/provinces/#province-events) in the acting province. The client presents this as one command with multiple response options. The [province leader](/provinces/#ruling-faction-and-ruling-hero)'s Charisma matters, even if a different hero is selected to crack down: leader Charisma improves gift success and makes forceful crackdowns face smaller riots.
|
||||
|
||||
If you fail to stop the riot, or if the riot is still unresolved when the riot phase ends, the riot happens. The province loses 25 support, gains 25 economy devastation, gains 25 infrastructure devastation, and the riot event is removed.
|
||||
|
||||
<!-- Sources:
|
||||
- Riot options: src/main/scala/net/eagle0/eagle/library/actions/impl/command/HandleRiotGiveCommand.scala
|
||||
src/main/scala/net/eagle0/eagle/library/actions/impl/command/HandleRiotCrackDownCommand.scala
|
||||
src/main/scala/net/eagle0/eagle/library/actions/impl/command/HandleRiotDoNothingCommand.scala
|
||||
- Riot consequences: src/main/scala/net/eagle0/eagle/library/actions/impl/command/HandleRiotUtils.scala
|
||||
- Riot settings: src/main/scala/net/eagle0/eagle/library/settings/BUILD.bazel
|
||||
-->
|
||||
|
||||
#### Options
|
||||
|
||||
##### Crack Down
|
||||
|
||||
<b>Vigor cost:</b> 15 on success, plus any vigor lost to casualties. If the hero is killed, the command fails.
|
||||
|
||||
<b>Effect:</b> Try to suppress the riot by force using a hero and optionally a battalion. The riot size is random from 100 to 1500, but the province leader's Charisma, not the selected acting hero's Charisma, reduces the roll before the size is calculated. Higher leader Charisma therefore makes the riot smaller and easier to defeat.
|
||||
|
||||
Casualties are based on riot size against the power of the acting hero and any attached battalion. If a battalion is used, casualties hit the battalion first; the battalion can be destroyed, and excess casualties can injure or kill the acting hero. If no battalion is used, casualties hit the acting hero directly.
|
||||
|
||||
Crack Down succeeds if the acting hero survives. On success, the riot event is removed and the acting hero spends 15 vigor, gains Strength and Agility XP, and may take additional vigor damage from casualties. If a battalion helped and survived, its size is reduced by its casualties. A successful crackdown does not directly reduce support, add devastation, or change loyalty; the cost is the violence itself.
|
||||
|
||||
Crack Down fails if the acting hero is killed. A failed crackdown does not stop the riot; the riot will still occur and apply the normal riot damage.
|
||||
|
||||
<b>Selection:</b> choose the acting hero and an optional battalion.
|
||||
|
||||
##### Do Nothing
|
||||
|
||||
<b>Vigor cost:</b> None.
|
||||
|
||||
<b>Effect:</b> Let the riot proceed without intervention. The riot will occur, causing the normal riot damage: -25 support, +25 economy devastation, and +25 infrastructure devastation.
|
||||
|
||||
<b>Selection:</b> no additional choices.
|
||||
|
||||
##### Give
|
||||
|
||||
<b>Vigor cost:</b> None.
|
||||
|
||||
<b>Effect:</b> Offer either food or gold to address the riot. You cannot combine food and gold in the same response. The maximum useful gift is 1000 food or 500 gold, and the selected amount is spent whether the attempt succeeds or fails.
|
||||
|
||||
Gift success is probabilistic. The chance is based on the province leader's Charisma plus the fraction of the maximum gift you gave: larger gifts help, and more leader Charisma helps. A partial gift can fail. On success, the riot event is removed. On failure, the gift is still spent and the riot will still occur.
|
||||
|
||||
<b>Selection:</b> choose a food amount or a gold amount.
|
||||
|
||||
### Please Recruit Me
|
||||
|
||||
<b>Hero requirements:</b> None.
|
||||
|
||||
<b>Vigor cost:</b> None.
|
||||
|
||||
<b>Available when:</b> unaffiliated heroes ask to join.
|
||||
|
||||
<b>Effect:</b> Respond to unaffiliated heroes who are asking to join a province.
|
||||
|
||||
<b>Selection:</b> choose the province, hero, and whether to accept.
|
||||
|
||||
### Resolve Alliance Offer
|
||||
|
||||
<b>Hero requirements:</b> None.
|
||||
|
||||
<b>Vigor cost:</b> None.
|
||||
|
||||
<b>Available when:</b> another faction offers an alliance.
|
||||
|
||||
<b>Effect:</b> Accept or reject an [incoming alliance offer](/factions/#incoming-diplomacy-offers).
|
||||
|
||||
<b>Selection:</b> choose the originating faction and the resolution.
|
||||
|
||||
### Resolve Break Alliance
|
||||
|
||||
<b>Hero requirements:</b> None.
|
||||
|
||||
<b>Vigor cost:</b> None.
|
||||
|
||||
<b>Available when:</b> another faction tries to break an alliance.
|
||||
|
||||
<b>Effect:</b> Respond to a faction attempting to break an alliance. Accepting the break replaces the alliance with a 12-month truce; it does not make the factions immediately hostile. The truce remains active through its reset month and changes to **Hostile** when a later **New Round** advances the date past that month. For example, a break accepted in January has a January reset date the following year and becomes hostile when February begins.
|
||||
|
||||
Imprisoning the envoy ends the alliance without a truce and makes both relationships **Hostile**. See [Diplomacy Outcomes](/factions/#diplomacy-outcomes) for the other imprisonment consequences.
|
||||
|
||||
<b>Selection:</b> choose the originating faction and the resolution.
|
||||
|
||||
### Resolve Invitation
|
||||
|
||||
<b>Hero requirements:</b> None.
|
||||
|
||||
<b>Vigor cost:</b> None.
|
||||
|
||||
<b>Available when:</b> another faction sends an invitation.
|
||||
|
||||
<b>Effect:</b> Accept or reject an invitation from another faction.
|
||||
|
||||
<b>Selection:</b> choose the originating faction and the resolution.
|
||||
|
||||
### Resolve Ransom Offer
|
||||
|
||||
<b>Hero requirements:</b> None.
|
||||
|
||||
<b>Vigor cost:</b> None.
|
||||
|
||||
<b>Available when:</b> another faction makes a ransom offer.
|
||||
|
||||
<b>Effect:</b> Accept or reject an incoming ransom offer.
|
||||
|
||||
<b>Selection:</b> choose the offering faction, the ransom offer, and the resolution.
|
||||
|
||||
### Resolve Truce Offer
|
||||
|
||||
<b>Hero requirements:</b> None.
|
||||
|
||||
<b>Vigor cost:</b> None.
|
||||
|
||||
<b>Available when:</b> another faction offers a truce.
|
||||
|
||||
<b>Effect:</b> Accept or reject an incoming truce offer.
|
||||
|
||||
<b>Selection:</b> choose the originating faction and the resolution.
|
||||
|
||||
### Resolve Tribute
|
||||
|
||||
<b>Hero requirements:</b> None.
|
||||
|
||||
<b>Vigor cost:</b> None.
|
||||
|
||||
<b>Available when:</b> another faction demands tribute.
|
||||
|
||||
<b>Effect:</b> Decide whether to pay demanded tribute. The command shows the demanding factions, requested tribute, their rough strength, and the acting province's available food and gold. A demand must be accepted or refused in full; partial payment and counteroffers are not available. Paying requires the province to have the full requested amount, creates a 12-month truce between the factions, and turns back the demanding faction's hostile armies against the payer's provinces. Refusing transfers no resources and returns that faction's army to **Attacking**, after which normal defense selection and battle resolution continue. Neither response changes prestige directly, although a battle caused by refusal can later affect prestige. See [Resolving a tribute demand](/concepts/march-timeline/#resolving-a-tribute-demand).
|
||||
|
||||
<b>Selection:</b> choose the demanding faction and whether tribute is paid.
|
||||
|
||||
### Battle Aftermath Decision
|
||||
|
||||
<b>Hero requirements:</b> None.
|
||||
|
||||
<b>Vigor cost:</b> None.
|
||||
|
||||
<b>Available when:</b> multiple allied attacking factions won the same province and Eagle needs to decide which claimant keeps it.
|
||||
|
||||
<b>Effect:</b> Decide what your surviving claimant army does during [battle aftermath](/concepts/eagle-to-shardok/#aftermath) after a shared victory. If no ally has claimed the province yet, you may choose **Keep Province** and become the conqueror. If an ally has already claimed it, you must withdraw.
|
||||
|
||||
Keeping the province finalizes conquest: your surviving non-fled heroes become the new ruling heroes, surviving battalions become the province battalions, support is reset to 0, province orders reset to **Entrust**, and non-fled defenders or former ruling heroes can become prisoners.
|
||||
|
||||
Withdrawing sends your surviving units to an adjacent province as a moving army. You may take up to the gold and food your claimant army brought into the battle. The withdrawal army arrives next round and does not use the normal Send Supplies transit loss.
|
||||
|
||||
<b>Selection:</b> choose whether to keep the province or withdraw. If withdrawing, choose destination province and supplies to take.
|
||||
@@ -0,0 +1,62 @@
|
||||
---
|
||||
title: Who You Command
|
||||
description: Faction leaders, vassals, province turns, and direct control.
|
||||
---
|
||||
|
||||
Eagle commands are issued through provinces, but who chooses a province's action depends on its ruling hero. The key distinction is between **faction leaders**, whom the player commands directly, and **vassals**, who govern automatically.
|
||||
|
||||
## Leadership roles
|
||||
|
||||
| Role | Directly commanded? | Main purpose |
|
||||
| --- | --- | --- |
|
||||
| Faction head | Yes | Defines the faction and provides its original leadership |
|
||||
| Other faction leader | Yes | Gives the player another directly controlled leader and a successor |
|
||||
| Vassal | No | Rules a province according to standing orders and local needs |
|
||||
| Unaffiliated hero | No | May be recruited, divined, apprehended, or otherwise interacted with |
|
||||
|
||||
The faction head is also a faction leader. **Swear Kinship** can promote a loyal faction hero into an additional faction leader. A faction leader who rules a province lets the player choose that province's normal command directly.
|
||||
|
||||
## Province turns
|
||||
|
||||
Most strategic actions belong to the province currently acting. During **Player Commands**, provinces led by faction leaders present their available commands to the player. A normal command such as Train, Improve, March, or Rest spends that province's turn.
|
||||
|
||||
The hero selected for a command is its actor, but selecting a different hero does not create another province turn. Several heroes may be eligible to perform the same province action; the player chooses which hero pays vigor and receives the associated experience.
|
||||
|
||||
Some commands explicitly do not take the turn. For example, **Issue Orders** changes standing instructions without consuming the acting province's normal action. The command reference labels these exceptions.
|
||||
|
||||
## Vassal-led provinces
|
||||
|
||||
A vassal-led province acts during **Vassal Commands**, before the main Player Commands phase. The vassal chooses an action using:
|
||||
|
||||
- the province's standing order: Entrust, Develop, Mobilize, or Expand;
|
||||
- the faction's focus province;
|
||||
- urgent local needs such as defense, riots, fatigue, low support, or beasts; and
|
||||
- the commands actually available in the province.
|
||||
|
||||
Standing orders guide a vassal; they do not guarantee one exact command. A vassal may address a crisis before following a long-term development or expansion preference.
|
||||
|
||||
## Multiple leaders and provinces
|
||||
|
||||
Gaining more faction leaders does not give one province several normal actions. It expands direct control:
|
||||
|
||||
- A leader can directly rule a province instead of leaving it to a vassal.
|
||||
- Leaders in different provinces can make those provinces player-controlled.
|
||||
- Leaders provide succession if the faction head dies.
|
||||
|
||||
Heroes who are present but do not rule the province can still be selected for eligible commands. Province leadership determines who chooses the action, not which local hero must perform it.
|
||||
|
||||
## A simple example
|
||||
|
||||
Suppose your faction controls three provinces:
|
||||
|
||||
1. Your faction head rules the capital. You choose its action during Player Commands.
|
||||
2. A second faction leader rules the frontier. You choose that province's action too.
|
||||
3. A vassal rules a farming province with Develop orders. The vassal chooses its own action during Vassal Commands.
|
||||
|
||||
The result is two direct player decisions and one automatic vassal decision, assuming no riot, diplomacy offer, battle decision, or other special phase adds another required choice.
|
||||
|
||||
## Special decisions
|
||||
|
||||
Attack decisions, defense decisions, diplomacy responses, prisoner handling, riots, and battle aftermath are conditional decisions rather than extra normal province turns. They appear only when the current game state requires them and can pause the shared phase until every required faction responds.
|
||||
|
||||
See [Eagle Round Flow](/concepts/eagle-round-flow/) for phase timing and [Eagle Commands](/commands/) for which actions consume a turn.
|
||||
@@ -0,0 +1,189 @@
|
||||
---
|
||||
title: Eagle Round Flow
|
||||
description: How Eagle strategic rounds advance, including simultaneous player action.
|
||||
---
|
||||
|
||||
<!--
|
||||
Source map:
|
||||
- Phase enum: src/main/protobuf/net/eagle0/eagle/common/round_phase.proto.
|
||||
- Phase advancement: src/main/scala/net/eagle0/eagle/library/RoundPhaseAdvancer.scala.
|
||||
- Command availability by phase: src/main/scala/net/eagle0/eagle/library/actions/availability/AvailableCommandsFactory.scala.
|
||||
- Result visibility/filtering: src/main/scala/net/eagle0/eagle/library/ActionResultFilter.scala and src/main/scala/net/eagle0/eagle/library/util/view_filters/.
|
||||
-->
|
||||
|
||||
Eagle is a simultaneous-turn strategy game. During a phase, every faction gets the chance to act in parallel. The game does not fully resolve the phase just because one player chooses a command; it waits until every required player choice has been made, then advances the phase and reveals the results each faction is allowed to know.
|
||||
|
||||
That means two things matter:
|
||||
|
||||
- Your orders are not a private single-player timeline. Other factions may be choosing their own commands at the same time.
|
||||
- You do not automatically see every action in the world. You see your faction's results, your allies' visible results, and information your faction has earned through scouting, battle revelations, diplomacy, or province control.
|
||||
|
||||
## Round Shape
|
||||
|
||||
Most rounds follow this broad rhythm:
|
||||
|
||||
1. Upkeep and automatic world changes happen.
|
||||
2. Vassals and players issue strategic commands.
|
||||
3. Marching armies arrive, attacks are decided, and battles are requested.
|
||||
4. Shardok battles resolve outside the strategic map.
|
||||
5. Eagle applies battle results, aftermath choices, diplomacy, recon, and end-of-round changes.
|
||||
|
||||
Some phases are player-facing. Others are bookkeeping phases that advance automatically unless a special decision is needed.
|
||||
|
||||
## One Round Is One Month
|
||||
|
||||
The strategic date advances once per complete Eagle round, not once per command. A
|
||||
command submitted during Player Commands belongs to the current month's round. The
|
||||
server then continues through the remaining phases and starts the next round, whose
|
||||
New Round processing advances the date by one month.
|
||||
|
||||
In a one-player game, the automatic phases may finish so quickly that it looks as
|
||||
though the command itself advanced the month. If a decision or battle is pending,
|
||||
the game remains in the same round and month until that work is complete.
|
||||
|
||||
## Phase Diagram
|
||||
|
||||
This diagram groups the detailed phases into their player-visible states:
|
||||
|
||||
```text
|
||||
New Round and upkeep
|
||||
|
|
||||
v
|
||||
Province movement and events
|
||||
|
|
||||
+----> Riot or recruitment decision? ----> wait for commands
|
||||
|
|
||||
v
|
||||
Vassal Commands -> Player Commands --------------> wait for every required faction
|
||||
|
|
||||
v
|
||||
Conquest setup -> Attack Decision -> Defense Decision
|
||||
|
|
||||
v
|
||||
Battle Request and food use
|
||||
|
|
||||
v
|
||||
Battle Resolution ----------> wait for all Shardok battles
|
||||
|
|
||||
v
|
||||
Battle Aftermath -----------> wait for pending choices
|
||||
|
|
||||
v
|
||||
Diplomacy Resolution -------> wait for pending replies
|
||||
|
|
||||
v
|
||||
Recon Resolution
|
||||
|
|
||||
+-----------------> next month's New Round
|
||||
```
|
||||
|
||||
"Wait" means the phase does not advance yet. The server may be waiting for your
|
||||
choice, another faction's choice, or an external Shardok battle result.
|
||||
|
||||
## Automatic Upkeep
|
||||
|
||||
These phases usually advance without direct player input:
|
||||
|
||||
### New Round
|
||||
|
||||
The game starts a new strategic round. Provinces, heroes, factions, and long-running state are updated for the new date.
|
||||
|
||||
### Prisoner Exchange
|
||||
|
||||
The game resolves automatic prisoner exchange state.
|
||||
|
||||
### Province Events
|
||||
|
||||
Province events such as local problems or opportunities are processed.
|
||||
|
||||
### Forced Turn Back
|
||||
|
||||
Armies that must turn back do so before normal movement resolution.
|
||||
|
||||
### Province Move Resolution
|
||||
|
||||
Moving armies arrive at their destinations, supplies move, and incoming hostile groups are created where armies meet resistance.
|
||||
|
||||
### Hero Departures
|
||||
|
||||
The game checks affiliated heroes for [low-loyalty departure](/heroes/#loyalty-departure). Any hero with loyalty below 50 may leave at any time, and lower loyalty means a greater risk. A departing hero becomes an unaffiliated traveler in the same province and does not take the province's supplies.
|
||||
|
||||
### Unaffiliated Hero Actions
|
||||
|
||||
Unaffiliated heroes may move, appear, escape, or otherwise act without belonging to a faction.
|
||||
|
||||
### Hostile Army Setup
|
||||
|
||||
The game prepares hostile army groups before conquest or battle decisions.
|
||||
|
||||
### Uncontested Conquest
|
||||
|
||||
If an attacker reaches a province with no defending force, the conquest can resolve without a Shardok battle.
|
||||
|
||||
### Truce Turn Back
|
||||
|
||||
Armies affected by truces may turn back instead of fighting.
|
||||
|
||||
### Battle Request
|
||||
|
||||
Eagle creates Shardok battles for provinces where attackers and defenders must fight.
|
||||
|
||||
### Food Consumption
|
||||
|
||||
Armies consume food before or around battle handling.
|
||||
|
||||
### Battle Resolution
|
||||
|
||||
Eagle waits for outstanding Shardok battles to report results. If there are none, the phase advances automatically.
|
||||
|
||||
### Recon Resolution
|
||||
|
||||
Recon results are applied after the relevant strategic activity has completed.
|
||||
|
||||
## Player Decision Phases
|
||||
|
||||
These phases can pause until required players have submitted decisions.
|
||||
|
||||
### Handle Riot
|
||||
|
||||
If your province has a riot, you may need to decide how to respond before the round can continue.
|
||||
|
||||
### Please Recruit Me
|
||||
|
||||
Some unaffiliated heroes may ask to join a faction. The phase waits while available recruitment decisions remain.
|
||||
|
||||
### Vassal Commands
|
||||
|
||||
Vassals choose province actions for provinces they rule. Vassals lead provinces and make their own choices, so these commands can happen before the direct player command phase.
|
||||
|
||||
### Player Commands
|
||||
|
||||
This is the main strategic command phase. Each player acts for provinces and heroes they can command directly. Every player is acting within the same phase, so your command is submitted into a shared strategic moment rather than immediately revealing a global timeline.
|
||||
|
||||
See [Eagle commands](/commands/) for the command reference.
|
||||
|
||||
### Attack Decision
|
||||
|
||||
When an army reaches a hostile province, the attacking side may need to choose whether to attack.
|
||||
|
||||
### Defense Decision
|
||||
|
||||
The defending side may need to decide how to respond to an incoming attack.
|
||||
|
||||
### Battle Aftermath
|
||||
|
||||
After Shardok results return to Eagle, winners may need to decide what happens to conquered provinces, captured heroes, or multiple allied claimants.
|
||||
|
||||
### Diplomacy Resolution
|
||||
|
||||
Diplomatic offers are resolved after players choose how to answer them. Offers can be accepted, rejected, invalidated, or lead to consequences such as imprisonment.
|
||||
|
||||
## Simultaneous Information
|
||||
|
||||
For a focused explanation of public, hidden, and potentially stale observations, see [Information and Recon](/concepts/information-and-recon/).
|
||||
|
||||
The important habit is to think in phases, not in a fully visible turn order.
|
||||
|
||||
If two factions both issue orders during Player Commands, those orders belong to the same command phase. When the phase ends, each faction receives the filtered results it is allowed to observe. Your own actions are visible to you. Allied and nearby events may be visible depending on game state. Enemy actions may be hidden until scouts, battles, diplomacy, or later consequences reveal them.
|
||||
|
||||
This is why Eagle often feels less like "I move, then you move" and more like issuing orders into a living world, then discovering what happened.
|
||||
@@ -0,0 +1,135 @@
|
||||
---
|
||||
title: From Eagle to Shardok
|
||||
description: How strategic Eagle armies become tactical Shardok battles.
|
||||
---
|
||||
|
||||
<!--
|
||||
Source map:
|
||||
- Movement resolution: src/main/scala/net/eagle0/eagle/library/actions/impl/action/PerformProvinceMoveResolutionAction.scala.
|
||||
- Battle setup phase: src/main/scala/net/eagle0/eagle/library/actions/impl/action/PerformHostileArmySetupAction.scala.
|
||||
- Uncontested conquest: src/main/scala/net/eagle0/eagle/library/actions/impl/action/PerformUncontestedConquestAction.scala.
|
||||
- Battle request creation: src/main/scala/net/eagle0/eagle/library/actions/impl/action/RequestBattlesAction.scala.
|
||||
- Tactical battle resolution back into Eagle: src/main/scala/net/eagle0/eagle/library/actions/impl/action/ResolveBattleAction.scala.
|
||||
- Province held/conquered outcomes: src/main/scala/net/eagle0/eagle/library/actions/impl/action/ProvinceHeldAction.scala and src/main/scala/net/eagle0/eagle/library/actions/impl/action/ProvinceConqueredAction.scala.
|
||||
- Multi-winner aftermath: src/main/scala/net/eagle0/eagle/library/actions/impl/action/MultiVictorBattleSetupAction.scala, src/main/scala/net/eagle0/eagle/library/actions/impl/command/BattleAftermathDecisionCommand.scala, and src/main/scala/net/eagle0/eagle/library/actions/impl/action/FinalizeAftermathAction.scala.
|
||||
- Battle aftermath decisions: src/main/scala/net/eagle0/eagle/library/actions/impl/action/FinalizeAftermathAction.scala and AutoResolveBattleAftermathAction.scala.
|
||||
-->
|
||||
|
||||
Eagle is the strategic layer. Shardok is the tactical layer. The two connect when armies meet in a province and Eagle needs a battlefield result instead of a simple strategic update.
|
||||
|
||||
## Strategic Movement
|
||||
|
||||
For a compact round-by-round view of the entire process, see [March and Battle Timeline](/concepts/march-timeline/).
|
||||
|
||||
In Eagle, armies move through the March command. A moving army has an origin province, destination province, arrival round, heroes, battalions, and supplies.
|
||||
|
||||
Because Eagle is simultaneous, multiple factions can send armies during the same strategic phase. Those armies arrive later during movement resolution, after every relevant player has had the chance to submit orders for the phase.
|
||||
|
||||
## Arrival
|
||||
|
||||
When an army reaches its destination, the first question is whether it is friendly to the province's current ruler.
|
||||
|
||||
Friendly armies arrive during Province Move Resolution. Their heroes, battalions, gold, and food enter the destination province at that point.
|
||||
|
||||
Hostile armies do not immediately enter the province. They are converted into hostile army groups later in the round, after player commands, with one group per attacking faction. Each group keeps the armies, units, supplies, and approach direction from the province it marched from.
|
||||
|
||||
After hostile setup, Eagle checks for cases that do not need a tactical battle:
|
||||
|
||||
- If a hostile army reaches an unruled province, the largest army group takes it during Uncontested Conquest. Ties are broken by troop power and then a deterministic random tie-breaker.
|
||||
- If a single attacking army is attacking and there is no defending army, Eagle can resolve the attack directly as a conquest.
|
||||
- If diplomacy or truces apply, armies may turn back before fighting.
|
||||
|
||||
Otherwise, the player may see attack or defense decision phases before a battle is requested.
|
||||
|
||||
## Attack and Defense Decisions
|
||||
|
||||
Hostile army groups first wait for an attack decision. An attacking faction can choose whether to press the attack when the game asks for that decision.
|
||||
|
||||
If an attack proceeds against a ruled province, the defender may need to choose a defending army. The chosen defenders become the province's defending army. If a fallback province is available, the defender may also need to choose where the army should flee or retreat if Shardok produces that outcome.
|
||||
|
||||
## Battle Creation
|
||||
|
||||
When Eagle creates a Shardok battle, it sends:
|
||||
|
||||
- The province's Shardok map.
|
||||
- The attacking armies.
|
||||
- The defending army, if any.
|
||||
- Each side's food for the battle.
|
||||
- Victory conditions for each player.
|
||||
- The battle type and battle id.
|
||||
|
||||
Attackers in a normal province assault are trying to take the province. Defenders are trying to hold it or survive long enough for the attacker to fail.
|
||||
|
||||
The battle food sent to Shardok is limited. Attackers bring up to one month of food consumption from their carried supplies. Defenders bring up to one month of food consumption from the province's food, and that food is removed from the province when the battle is created.
|
||||
|
||||
## Tactical Resolution
|
||||
|
||||
The battle then runs in Shardok. Players place units, move, attack, use profession commands, spend Action Points, and try to satisfy their victory conditions.
|
||||
|
||||
Eagle waits in the battle resolution phase until outstanding Shardok battles report back.
|
||||
|
||||
See [Shardok battles](/concepts/shardok-battles/) for the tactical overview and [Shardok victory conditions](/concepts/shardok-victory-conditions/) for battle-ending rules.
|
||||
|
||||
## Returning To Eagle
|
||||
|
||||
When Shardok reports a result, Eagle applies battle consequences before deciding who owns the province.
|
||||
|
||||
The battle result can change:
|
||||
|
||||
- Hero locations and status.
|
||||
- Battalion size and survival.
|
||||
- Captured heroes and prisoners.
|
||||
- Supplies brought by attacking armies.
|
||||
- Province devastation.
|
||||
- Quest progress and battle history.
|
||||
- Prestige. Winners gain battle prestige; losers lose battle prestige.
|
||||
|
||||
Battles add devastation to the battle province. Surviving units may return, flee, become outlaws, be secured, or be captured depending on the tactical result.
|
||||
|
||||
Attackers' remaining gold and food are added to the battle province as part of battle resolution. If the defender holds the province, those supplies become part of the defended province. If an attacker later conquers it, the province changes hands with the supplies already present.
|
||||
|
||||
## If The Defender Wins
|
||||
|
||||
If the defending side wins, the province remains with its current ruler.
|
||||
|
||||
Attackers who did not flee, become outlaws, or get otherwise secured may be captured by the defending faction. Captured attacker battalions can be destroyed. The defender receives a Province Held result and any captured heroes become prisoners in the province.
|
||||
|
||||
## If One Attacker Wins
|
||||
|
||||
If exactly one attacking faction wins, that faction conquers the province directly.
|
||||
|
||||
Conquest does several things at once:
|
||||
|
||||
- Sets the conquering faction as the province's ruler.
|
||||
- Removes the old ruling-faction heroes from the province.
|
||||
- Adds the surviving non-fled conquering heroes as the new ruling-faction heroes.
|
||||
- Replaces the province battalions with the surviving conquering battalions.
|
||||
- Clears the defending army.
|
||||
- Sets province support to 0.
|
||||
- Resets province orders to **Entrust**.
|
||||
- Captures non-fled defenders and former ruling heroes as prisoners.
|
||||
- Clears the old owner's focus province if this province was their focus.
|
||||
- Clears recon on the province for all factions.
|
||||
|
||||
Prisoners already held in the conquered province can also be freed. Prisoners from the conquering faction join the new rulers at freed-prisoner loyalty. Prisoners from allied factions are sent toward the closest allied province when possible; if no allied province exists, they can become outlaws.
|
||||
|
||||
## If Multiple Allied Attackers Win
|
||||
|
||||
If multiple attacking factions win together, Eagle cannot automatically know who should keep the province. It creates a pending conquest decision for the surviving attacking claimants.
|
||||
|
||||
During Battle Aftermath, each claimant may be asked to choose:
|
||||
|
||||
- **Keep Province:** claim the province. Only one claimant can keep it.
|
||||
- **Withdraw:** leave for an adjacent province, taking up to the gold and food that claimant brought into the battle.
|
||||
|
||||
If an ally has already claimed the province, the remaining claimants must withdraw. If only one undecided claimant remains and nobody has claimed the province, Eagle auto-resolves that claimant as keeping it.
|
||||
|
||||
When aftermath finalizes, the keeper conquers the province through the same conquest rules as a single attacking winner. Withdrawing claimants become incoming armies to their chosen adjacent provinces, arriving next round with the gold and food they chose to take. These aftermath withdrawals do not apply shipment transit loss.
|
||||
|
||||
## Aftermath
|
||||
|
||||
Some battle results need another Eagle decision before the round can continue. This happens mainly when multiple allied attackers win the same province or when captured heroes create follow-up handling decisions.
|
||||
|
||||
Battle Aftermath happens after tactical results return but before the strategic round fully moves on. It is not a second Shardok battle; it is Eagle deciding ownership, withdrawals, supplies, and captured-hero consequences from the battle that already ended.
|
||||
|
||||
Captured heroes can create separate follow-up decisions after this. Those decisions determine what the capturing faction does with the hero now that the battle result has placed them in custody.
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
title: Game Concepts
|
||||
description: Player-facing explanations of Eagle and Shardok systems.
|
||||
---
|
||||
|
||||
These pages explain how Eagle0's major systems fit together. Use them when you want the big picture behind the command reference.
|
||||
|
||||
## Eagle
|
||||
|
||||
- [Victory and defeat](victory-and-defeat/) explains the strategic objective, leadership succession, faction destruction, and how the game ends.
|
||||
- [Who you command](command-authority/) explains faction heads, faction leaders, vassals, province turns, and direct control.
|
||||
- [Eagle round flow](eagle-round-flow/) explains how a strategic round advances, why actions are simultaneous, and when players see results.
|
||||
- [Information and recon](information-and-recon/) explains public, hidden, scouted, and potentially stale knowledge.
|
||||
- [From Eagle to Shardok](eagle-to-shardok/) explains how strategic movement turns into tactical battles and how battle results return to Eagle.
|
||||
- [March and battle timeline](march-timeline/) follows one army from order submission through arrival, battle, and aftermath.
|
||||
|
||||
## Shardok
|
||||
|
||||
- [Shardok combat fundamentals](shardok-combat/) explains turns, rounds, AP, movement, attacks, terrain, morale, and vigor.
|
||||
- [Shardok battles](shardok-battles/) explains tactical setup, turns, units, morale, vigor, terrain, and battle aftermath.
|
||||
- [Shardok victory conditions](shardok-victory-conditions/) explains how tactical battles end.
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
title: Information and Recon
|
||||
description: Public, private, stale, and scouted information in simultaneous Eagle rounds.
|
||||
---
|
||||
|
||||
Eagle does not give every faction a current view of the whole world. Results are filtered by province control, faction knowledge, scouting, diplomacy, alliances, and battle participation.
|
||||
|
||||
## Three kinds of uncertainty
|
||||
|
||||
- **Not present:** the game state confirms that something is absent.
|
||||
- **Not visible:** it may exist, but your faction has not earned that information.
|
||||
- **Known as of a date:** your faction observed it previously, but simultaneous actions may have changed it.
|
||||
|
||||
A blank enemy field can mean that the value is not visible; it does not necessarily mean the value is zero. An old observation describes the recorded date rather than guaranteeing the current state.
|
||||
|
||||
## Province information
|
||||
|
||||
Public province information can include identity, ruler, known events, known incoming armies, and an **as of** date. Full local information adds resources, caps, support, food consumption, battalions, heroes, orders, shipments, and events.
|
||||
|
||||
Controlling a province normally gives your faction its detailed state. Losing control does not mean every remembered fact remains current; the date tells you when the displayed knowledge was established.
|
||||
|
||||
## Simultaneous orders
|
||||
|
||||
Enemy factions choose during the same phase. Between an observation and a later arrival, a province may receive troops, change ruler, form a truce, or launch its own army. Observing a target does not reserve or freeze its state.
|
||||
|
||||
## Strategic Recon
|
||||
|
||||
Recon requires an eligible Ranger and targets a province not ruled by the acting faction. The command creates pending reconnaissance rather than instantly granting omniscience. Results are applied during **Recon Resolution**, after the relevant strategic activity.
|
||||
|
||||
Recon improves what your faction knows about the target at that point in the round. Later actions can make those details stale. The observation date records when that knowledge was established.
|
||||
|
||||
## Other ways information is earned
|
||||
|
||||
- Ruling a province exposes its local state.
|
||||
- Battle participants learn information revealed by the tactical encounter and its result.
|
||||
- Diplomacy reveals offers and relationship changes to the involved factions.
|
||||
- Allied or nearby events may be visible when the game state permits it.
|
||||
- Moving next to, scouting, or revealing hidden Shardok units affects tactical knowledge separately from strategic Recon.
|
||||
|
||||
Alliance does not make every hidden fact in the world public. Each faction can still have knowledge that the other does not.
|
||||
|
||||
## Example
|
||||
|
||||
In round 8, Recon reports an enemy frontier province with one battalion. You March toward it during round 9. The enemy simultaneously sends reinforcements. Your report was truthful as of round 8, but it did not promise that the round-9 defenders would be unchanged.
|
||||
|
||||
See [Eagle Round Flow](/concepts/eagle-round-flow/#simultaneous-information), [Recon](/commands/#recon), and [Provinces](/provinces/) for related rules.
|
||||
@@ -0,0 +1,62 @@
|
||||
---
|
||||
title: March and Battle Timeline
|
||||
description: Where an army is from order submission through arrival and battle aftermath.
|
||||
---
|
||||
|
||||
March is a delayed strategic order. Selected heroes, battalions, gold, and food leave the origin immediately, but they do not enter the destination until later processing.
|
||||
|
||||
## The timeline
|
||||
|
||||
| When | State | What happens |
|
||||
| --- | --- | --- |
|
||||
| Player Commands, round N | March submitted | Forces and supplies leave the origin and form a moving army scheduled for round N+1. Each marching hero spends vigor. |
|
||||
| Province Move Resolution, round N+1 | Arrival checked | An army already friendly to the destination enters it. A hostile arrival waits outside rather than entering. |
|
||||
| After Player Commands, round N+1 | Hostile Army Setup | Hostile arrivals become attacking groups, one per faction, preserving their approach direction and carried supplies. |
|
||||
| Uncontested Conquest | No tactical defense needed | An unruled destination can be claimed without Shardok. Competing groups are compared by size, power, and a deterministic tie-breaker. |
|
||||
| Attack and Defense Decisions | A ruled province is contested | Attackers decide whether to press the attack or demand tribute; defenders answer tribute demands and may choose an army and fallback destination. Truces can turn armies back. |
|
||||
| Battle Request and Resolution | Shardok battle | Eagle sends armies, map, limited battle food, and victory conditions to Shardok, then waits for a result. |
|
||||
| Battle Aftermath | Consequences applied | Eagle resolves ownership, survivors, prisoners, supplies, devastation, prestige, and any allied-claimant decisions. |
|
||||
|
||||
## Friendly arrival
|
||||
|
||||
If your faction already rules the destination during Province Move Resolution, the army's heroes, battalions, and supplies enter the province. They are then present before later phases such as player commands and food consumption.
|
||||
|
||||
## Neutral or hostile arrival
|
||||
|
||||
A marching army does not occupy a neutral or enemy province during movement resolution. It remains a moving/hostile force until conquest or battle processing decides the outcome. This distinction matters for hero location, province leadership, and food.
|
||||
|
||||
An unruled province may be conquered without a tactical battle. A ruled and defended province can require attack and defense decisions followed by Shardok.
|
||||
|
||||
## Resolving a tribute demand
|
||||
|
||||
A tribute demand pauses that attacking faction during Defense Decision. The defender resolves each demand by either paying it in full or refusing it; there is no partial-payment or counteroffer option.
|
||||
|
||||
If the defender pays, the full requested gold and food are deducted from the province. The two factions enter a 12-month truce, and the demanding faction's hostile armies against any province ruled by the payer turn back. The returning armies arrive at their origin provinces in the next round.
|
||||
|
||||
If the defender refuses, no food or gold is transferred and the demanding army returns to **Attacking** status. The province remains in Defense Decision, so an eligible defender can then choose a defending army and fallback destination. Once the remaining attack and defense decisions are resolved, the ordinary battle-request rules apply: opposed armies proceed to battle, while an undefended attack may instead become an uncontested conquest.
|
||||
|
||||
Paying or refusing tribute has no direct prestige effect. If refusal leads to battle, the eventual battle result can change prestige normally.
|
||||
|
||||
## Food and gold
|
||||
|
||||
Carried supplies travel with the army. Moving armies consume their own food while incoming. If food is insufficient, their battalions can suffer starvation losses.
|
||||
|
||||
When a normal province battle is created, attackers bring at most one month of food consumption from their carried food. Defenders receive at most one month from province food, removed when the battle is created. Remaining attacking supplies are applied to the battle province during resolution; whoever ultimately controls the province may therefore inherit them.
|
||||
|
||||
## One attacker wins
|
||||
|
||||
One victorious attacking faction conquers the province. Its surviving, non-fled heroes and battalions become the new ruling force, support becomes 0, orders reset to Entrust, and eligible defenders can become prisoners.
|
||||
|
||||
## Allied attackers win together
|
||||
|
||||
Eagle cannot automatically choose which allied winner keeps the province. Claimants enter Battle Aftermath. One may keep it; the others withdraw to adjacent provinces with permitted shares of the supplies they brought. Those withdrawals become incoming armies for the next round.
|
||||
|
||||
## Before confirming March
|
||||
|
||||
- Confirm the destination and arrival delay.
|
||||
- Leave enough heroes behind to retain the origin if necessary.
|
||||
- Carry enough food for transit and the intended battle.
|
||||
- Remember that the destination's ruler or diplomacy can change before arrival.
|
||||
- Expect an additional attack, defense, or aftermath decision when the army arrives.
|
||||
|
||||
See [March](/commands/#march) for selection rules and [From Eagle to Shardok](/concepts/eagle-to-shardok/) for detailed battle consequences.
|
||||
@@ -0,0 +1,80 @@
|
||||
---
|
||||
title: Shardok Battles
|
||||
description: How tactical Shardok battles work at a high level.
|
||||
---
|
||||
|
||||
<!--
|
||||
Source map:
|
||||
- Shardok battle model: src/main/scala/net/eagle0/eagle/model/state/shardok_battle/ShardokBattle.scala.
|
||||
- Battle creation from Eagle: src/main/scala/net/eagle0/eagle/library/actions/impl/action/RequestBattlesAction.scala.
|
||||
- Battle resolution back into Eagle: src/main/scala/net/eagle0/eagle/library/actions/impl/action/ResolveBattleAction.scala.
|
||||
- Tactical command availability: src/main/cpp/net/eagle0/shardok/library/AvailableCommandsFactory.cpp and command_factories/.
|
||||
- Victory checks: src/main/cpp/net/eagle0/shardok/library/actions/UpdateGameStatusAction.cpp.
|
||||
-->
|
||||
|
||||
Shardok is the tactical battle layer. Eagle decides that a battle should happen, sends armies into Shardok, and later receives the result back.
|
||||
|
||||
For the decision-making basics behind the command list, see [Shardok Combat Fundamentals](/concepts/shardok-combat/).
|
||||
|
||||
## Setup
|
||||
|
||||
At the start of a battle, each side places its units onto valid setup hexes. Some battles include reserve or reinforcement units that enter later. Rangers on stealth-capable units may be able to deploy hidden.
|
||||
|
||||
The map comes from the province where the battle occurs. A province's Shardok map defines terrain, castles, cities, rivers, bridges, water, and critical tiles.
|
||||
|
||||
## Units
|
||||
|
||||
See [Battalions](/battalions/) for troop, type, training, armament, morale, and lifecycle fundamentals.
|
||||
|
||||
A Shardok unit usually represents a hero attached to a battalion. The hero brings stats, vigor, profession abilities, and special command access. The battalion brings troop count, type, morale, training, armament, movement costs, damage profile, and battlefield limits.
|
||||
|
||||
Some commands require a profession. Some also require the attached battalion type to support that role. For example, many spell commands require a unit that can cast, while Ranger stealth depends on a stealth-capable battalion.
|
||||
|
||||
See [Heroes](/heroes/) for professions and [Shardok commands](/shardok-commands/) for command requirements.
|
||||
|
||||
## Action Points
|
||||
|
||||
Units act using Action Points. Moving through terrain spends AP based on the terrain and battalion type. Many major actions require a fixed AP minimum and then consume all remaining AP. The [Shardok command reference](/shardok-commands/) lists each command's AP cost and requirements.
|
||||
|
||||
Unless a command says otherwise, a unit with morale must have at least 10 morale, and a unit with an attached hero must have at least 10 vigor.
|
||||
|
||||
## Turns And Rounds
|
||||
|
||||
Shardok advances tactically while units move, attack, cast, hide, scout, flee, and reinforce. When a side ends its turn, control passes according to the tactical state. At the end of a tactical round, Shardok checks end-of-round victory conditions such as holding critical tiles or surviving to the round limit.
|
||||
|
||||
Some [victory conditions](/concepts/shardok-victory-conditions/), such as last player standing, can end the battle as soon as only one eligible side remains. Others wait until the end of a round so the opposing side has a chance to respond.
|
||||
|
||||
## Terrain
|
||||
|
||||
Terrain matters constantly:
|
||||
|
||||
- It changes movement costs.
|
||||
- It affects damage and resistance.
|
||||
- It can block line of fire.
|
||||
- Castles and cities can become critical objectives.
|
||||
- Water, bridges, ice, fire, and mountains create command-specific constraints.
|
||||
|
||||
Shardok is not just a troop-count comparison. Position, terrain, hidden information, and command timing matter.
|
||||
|
||||
## Hidden Units
|
||||
|
||||
Hidden units are not fully visible to enemies. Ranger abilities, scouting, terrain, movement, and proximity can reveal or preserve hidden information.
|
||||
|
||||
A hidden unit can matter even at battle end. If a losing hidden unit has a capable hero and can flee, it may escape rather than being resolved like an ordinary visible loser.
|
||||
|
||||
## Fleeing, Retreating, And Outlaws
|
||||
|
||||
Units can leave a battle through flee or retreat mechanics when those commands are available. A hero may also become an outlaw through battle consequences. Eagle receives these statuses when Shardok reports the final battle result.
|
||||
|
||||
## Battle Results
|
||||
|
||||
When the battle ends, Shardok reports each player's surviving units, destroyed battalions, fled units, captured or secured units, and victory or loss condition.
|
||||
|
||||
Eagle then applies the result:
|
||||
|
||||
- Defenders who win hold the province.
|
||||
- A single attacking winner can conquer the province.
|
||||
- Multiple allied attacking winners may trigger battle aftermath decisions.
|
||||
- Captured heroes and prisoners can create follow-up decisions.
|
||||
- Battle damage can devastate province economy, agriculture, and infrastructure.
|
||||
- Winning battles can advance quests.
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
title: Shardok Combat Fundamentals
|
||||
description: Turns, rounds, Action Points, movement, attacks, terrain, morale, and vigor.
|
||||
---
|
||||
|
||||
Shardok is an objective-based hex battle, not a direct comparison of troop totals. Position, terrain, heroes, hidden information, and command timing determine which force can satisfy its victory conditions.
|
||||
|
||||
## Turns, units, and rounds
|
||||
|
||||
Players act according to the tactical state. A unit spends **Action Points (AP)** to move or use commands. Ending the player turn sets that player's remaining unit AP to 0. After every required side has had its opportunity, the tactical round closes and end-of-round victory conditions are checked.
|
||||
|
||||
## Movement and position
|
||||
|
||||
Movement cost depends on terrain and battalion type. Water, mountains, bridges, ice, fire, castles, and cities can restrict paths or change their value. Enemy proximity can affect hiding and movement choices. Use the highlighted valid targets rather than assuming every adjacent hex is traversable.
|
||||
|
||||
## Attacks and special actions
|
||||
|
||||
Melee and ranged attacks require valid targets and battlefield geometry. Line of fire can be blocked. Damage and resistance depend on the units, heroes, damage profiles, and terrain involved.
|
||||
|
||||
Many major actions require a minimum AP and then consume all remaining AP. The command reference states the exact rule. Unless an entry says otherwise, a morale-using unit needs at least 10 morale, and a unit with a hero needs at least 10 [hero vigor](/heroes/#vigor). Current vigor also contributes to maximum AP. Reaching zero vigor resolves the entire hero-led unit as captured, even if troops remain.
|
||||
|
||||
## Heroes and battalions
|
||||
|
||||
The battalion provides troops, type, morale, training, armament, movement, and damage behavior. The attached hero provides stats, vigor, profession abilities, and special commands. Profession commands can also require a compatible battalion: casting and Ranger stealth are important examples.
|
||||
|
||||
## Terrain and objectives
|
||||
|
||||
Castles and cities may be defensive positions or critical objectives. Critical-tile victory normally requires a hero-bearing unit on every required tile at the end of the tactical round. Occupying an objective during your turn is therefore not always immediate victory; opponents may receive a chance to respond.
|
||||
|
||||
## Hidden units
|
||||
|
||||
Hidden units are not fully visible. Ranger scouting, adjacency, movement, and terrain can reveal them. Hidden units do not keep a player alive for Last Player Standing, although a capable hidden loser may flee when battle ends.
|
||||
|
||||
See [Shardok Commands](/shardok-commands/) for exact action requirements and [Shardok Victory Conditions](/concepts/shardok-victory-conditions/) for battle-ending checks.
|
||||
@@ -0,0 +1,80 @@
|
||||
---
|
||||
title: Shardok Victory Conditions
|
||||
description: How tactical battles decide winners and losers.
|
||||
---
|
||||
|
||||
<!--
|
||||
Source map:
|
||||
- Victory condition enum: src/main/protobuf/net/eagle0/common/victory_condition.proto.
|
||||
- Shardok victory checks: src/main/cpp/net/eagle0/shardok/library/actions/UpdateGameStatusAction.cpp.
|
||||
- Eagle assignment of battle victory conditions: src/main/scala/net/eagle0/eagle/library/actions/impl/action/RequestBattlesAction.scala.
|
||||
-->
|
||||
|
||||
Shardok battles end when one of the battle's victory conditions is satisfied. Each player has a set of conditions that apply to them.
|
||||
|
||||
In a normal Eagle province assault, attackers usually get **Hold Critical Tiles**, **Last Player Standing**, and **Last Alliance Standing**. The defender usually gets **Last Player Standing** and **Win After Max Rounds**.
|
||||
|
||||
## When conditions are checked
|
||||
|
||||
A **turn** is one player's opportunity to act. A **round** includes the required turns before end-of-round processing. This distinction determines whether the opposing side gets a response.
|
||||
|
||||
| Condition | Typical holder | Check timing |
|
||||
| --- | --- | --- |
|
||||
| Last Player Standing | Attacker and defender | As soon as only one eligible player survives |
|
||||
| Last Alliance Standing | Allied attackers | As soon as the only eligible survivors are mutually allied holders |
|
||||
| Hold Critical Tiles | Attackers | End of the tactical round |
|
||||
| Win After Max Rounds | Defender | End-of-round clock check |
|
||||
| Most Troops Standing | Special battle types | At its configured ending check |
|
||||
|
||||
The battle display's assigned conditions and maximum-round value are authoritative for that battle; the normal-assault assignments above are defaults, not promises for every scenario.
|
||||
|
||||
### Critical-tile response example
|
||||
|
||||
An attacker occupies the final critical tile during their turn. The battle does not end at that movement step. The defender receives the remaining opportunity provided by the tactical round and can retake or contest an objective. If eligible allied attackers still occupy every critical tile when the round closes, Hold Critical Tiles succeeds.
|
||||
|
||||
### Defender-clock example
|
||||
|
||||
The defender does not need to eliminate every attacker when Win After Max Rounds is assigned. The attacker must satisfy an attacking victory condition before the displayed round limit expires. Surviving without taking the objectives can therefore be a loss for the attacker.
|
||||
|
||||
## Last Player Standing
|
||||
|
||||
A player wins if they are the only surviving player and they have this victory condition.
|
||||
|
||||
A player counts as surviving when they still have a visible normal unit with troops, or a visible normal unit with a living hero. Hidden units do not keep a player alive for this check, though some hidden losing units may be able to flee when the battle ends.
|
||||
|
||||
## Last Alliance Standing
|
||||
|
||||
Multiple players can win together if all surviving players are mutually allied and all have this victory condition.
|
||||
|
||||
This is the tactical version of "the only forces left on the field are allies."
|
||||
|
||||
## Win After Max Rounds
|
||||
|
||||
A player wins if the battle lasts past the maximum number of Shardok rounds and they have this victory condition.
|
||||
|
||||
In normal province assaults, this is usually the defender's clock. The attacker has to force a result before time runs out.
|
||||
|
||||
## Hold Critical Tiles
|
||||
|
||||
A player wins by controlling every critical tile on the map. Critical tiles are usually castles or other objective tiles.
|
||||
|
||||
To count, each critical tile must be occupied by a unit belonging to the winning player, and the occupying unit must have an attached hero.
|
||||
|
||||
Allied attackers can also win this way together if allied players collectively occupy all critical tiles, all occupying players have the condition, and the occupants are mutually allied.
|
||||
|
||||
This condition is checked at the end of a tactical round, not immediately after every move, so the defender has a chance to retake objectives before the round closes.
|
||||
|
||||
## Most Troops Standing
|
||||
|
||||
This condition exists in the battle model, but normal Eagle province assaults do not currently assign it to attackers or defenders.
|
||||
|
||||
If a battle type uses it, the winner is the player with the most troops standing when the battle reaches the relevant ending check.
|
||||
|
||||
## What Happens After Victory
|
||||
|
||||
Shardok reports each player as victorious or defeated, including which condition ended the battle. Eagle then turns that tactical result into strategic consequences:
|
||||
|
||||
- A defending winner holds the province.
|
||||
- A single attacking winner conquers the province.
|
||||
- Multiple allied attacking winners may need to decide who keeps the province.
|
||||
- Captured heroes, destroyed battalions, fled units, outlaws, and secured prisoners are applied to the Eagle game state.
|
||||
@@ -0,0 +1,62 @@
|
||||
---
|
||||
title: Victory and Defeat
|
||||
description: How factions win, survive leadership losses, and leave the game.
|
||||
---
|
||||
|
||||
Eagle0 is a struggle to become the last surviving faction. The game ends when only one faction remains; that faction wins. If no factions remain, the game ends without a winner.
|
||||
|
||||
## When a faction survives
|
||||
|
||||
A faction remains in the game while it has both:
|
||||
|
||||
- at least one faction leader; and
|
||||
- at least one ruled province or moving army.
|
||||
|
||||
Losing a province is therefore serious, but it is not necessarily immediate defeat. An army already marching can keep a landless faction alive long enough to conquer somewhere else.
|
||||
|
||||
## Losing your faction head
|
||||
|
||||
The death of the faction head does not automatically destroy the faction. Another surviving faction leader succeeds them.
|
||||
|
||||
If no faction leader is available but the faction still has a province or moving army, the strongest surviving vassal can rise to become both faction leader and faction head. The faction is destroyed only if no vassal can succeed.
|
||||
|
||||
Creating additional leaders with **Swear Kinship** protects the faction against a sudden leadership loss. A candidate must belong to the faction, have at least 95 loyalty, and be with the faction head when the command is used.
|
||||
|
||||
## How a faction is destroyed
|
||||
|
||||
A faction is destroyed when either condition is true:
|
||||
|
||||
1. It rules no provinces and has no moving armies.
|
||||
2. It has no faction leaders after the succession check.
|
||||
|
||||
When a faction is destroyed:
|
||||
|
||||
- its provinces become unruled;
|
||||
- its incoming armies are removed and their supplies remain at their destinations;
|
||||
- its heroes lose their faction assignment; and
|
||||
- diplomacy and outstanding offers involving it are removed.
|
||||
|
||||
Those heroes are not necessarily killed. They may remain in the world in unaffiliated states and interact with surviving factions later.
|
||||
|
||||
## Strategic implications
|
||||
|
||||
- Keep a succession option by gaining another leader or maintaining a capable vassal.
|
||||
- Preserve at least one province or viable moving army.
|
||||
- Do not assume an enemy is eliminated merely because its last visible province falls; it may still have an army in motion.
|
||||
- Remember that conquest can release, capture, or displace heroes without destroying their former faction immediately.
|
||||
|
||||
## Quick answers
|
||||
|
||||
**Do I win a battle by killing the enemy faction head?**
|
||||
Not automatically. Leadership succession may keep the faction alive.
|
||||
|
||||
**Am I defeated as soon as I lose my last province?**
|
||||
Not if your faction still has a moving army and a leader.
|
||||
|
||||
**Can a faction with provinces survive after all its leaders die?**
|
||||
Only if a surviving vassal can rise during the succession check.
|
||||
|
||||
**How does the whole game end?**
|
||||
The final surviving faction wins. If every faction is destroyed, there is no winner.
|
||||
|
||||
For tactical battle victory rules, see [Shardok Victory Conditions](/concepts/shardok-victory-conditions/). For leadership roles, see [Heroes](/heroes/#faction-leaders) and [Factions](/factions/#leaders).
|
||||
@@ -0,0 +1,136 @@
|
||||
---
|
||||
title: Factions
|
||||
description: Reference for Eagle factions, leaders, prestige, diplomacy, and province focus.
|
||||
---
|
||||
|
||||
Factions are the political actors in Eagle. They control provinces, command heroes and battalions, negotiate diplomacy, and compete for victory.
|
||||
|
||||
<!-- Sources:
|
||||
- FactionView shape: src/main/protobuf/net/eagle0/eagle/views/faction_view.proto
|
||||
- Relationship levels: src/main/protobuf/net/eagle0/eagle/views/faction_relationship_view.proto
|
||||
- Trust storage: src/main/scala/net/eagle0/eagle/model/state/faction/FactionRelationship.scala
|
||||
- Swear Kinship availability: src/main/scala/net/eagle0/eagle/library/actions/availability/AvailableSwearBrotherhoodCommandFactory.scala
|
||||
- Swear Kinship effect: src/main/scala/net/eagle0/eagle/library/actions/impl/command/SwearBrotherhoodCommand.scala
|
||||
- Leadership and destruction: src/main/scala/net/eagle0/eagle/library/actions/impl/action/CheckForFactionChangesAction.scala
|
||||
- Free hero states and recruitment info: src/main/scala/net/eagle0/eagle/model/state/unaffiliated_hero/UnaffiliatedHeroT.scala
|
||||
- Recruitment availability and odds: src/main/scala/net/eagle0/eagle/library/actions/availability/AvailableRecruitHeroesCommandFactory.scala
|
||||
and src/main/scala/net/eagle0/eagle/library/util/recruitment_odds/RecruitmentOdds.scala
|
||||
- Prestige calculation: src/main/scala/net/eagle0/eagle/library/util/faction_utils/FactionUtils.scala
|
||||
- Truce, alliance, break-alliance, ransom, and imprisonment effects:
|
||||
src/main/scala/net/eagle0/eagle/library/actions/impl/action/diplomacy_helpers/
|
||||
- Trust constants: src/main/scala/net/eagle0/eagle/library/settings/BUILD.bazel
|
||||
-->
|
||||
|
||||
## Faction Head
|
||||
|
||||
The faction head is the hero who represents the faction. In a normal game, this is usually your starting leader: the hero whose ideals, title, and story define what your faction is about.
|
||||
|
||||
If the faction head is killed, another faction leader can take over as faction head. The faction may also be renamed around the new head if that hero has a faction name of their own.
|
||||
|
||||
## Leaders
|
||||
|
||||
[Faction leaders](/heroes/#faction-leaders) are the heroes you can command directly. They are different from [vassals](/heroes/#vassals), who rule provinces for you and make their own choices.
|
||||
|
||||
You can gain more faction leaders with [**Swear Kinship**](/commands/#swear-kinship). The command is available when your faction head is in the province, your faction has fewer than 5 leaders, and another hero in the province has at least 95 loyalty. The new kinship hero becomes a faction leader.
|
||||
|
||||
Faction leaders matter for succession. If the faction head dies, the next available faction leader becomes faction head. If no surviving leader is available to take over immediately, the strongest remaining vassal can rise to become a new faction leader and faction head.
|
||||
|
||||
## Prestige
|
||||
|
||||
Prestige is a high-level measure of faction stature. It is also the main faction-wide factor in whether free heroes are willing to join you.
|
||||
|
||||
You gain prestige by:
|
||||
|
||||
- Controlling more provinces whose support is high enough for taxes. Each supported province is worth 10 prestige.
|
||||
- Winning battles.
|
||||
- Defeating beasts.
|
||||
|
||||
Prestige is not the only recruitment factor. The faction head's charisma, the free hero's power and ambition, the hero's current state, and any personal bias toward or against your faction also matter. But when you want more heroes to take you seriously, prestige is the big strategic lever.
|
||||
|
||||
## Free Heroes
|
||||
|
||||
Free heroes are heroes in a province who do not currently belong to your faction's command structure. The Free Hero panel tells you what state they are in and whether they can be recruited, divined, or managed another way.
|
||||
|
||||
The common states are:
|
||||
|
||||
- **Resident:** A hero living in the province. Residents may be willing to join outright. If they are not ready, they may be divinable: use [**Divine**](/commands/#divine) to learn what would impress them, then fulfill that ask to make them recruitable.
|
||||
- **Traveler:** A hero passing through. You do not know whether a traveler would join you until a faction leader travels there and talks to them. Travelers are harder to recruit than residents, but they might still join if your prestige and other recruitment factors are strong enough. They are also likely to leave the province soon, so they are more time-sensitive than residents.
|
||||
- **Prisoner:** A captured hero. Prisoners start with a strong bias against the faction holding them, but desperation can build over time. Eventually a prisoner may become willing to join, especially if your prestige is high enough.
|
||||
- **Outlaw:** A hostile free hero in the province. Outlaws are not ordinary recruits and will not simply join from the Free Hero panel; you generally deal with them through commands such as [**Apprehend Outlaw**](/commands/#apprehend-outlaw).
|
||||
|
||||
Some free heroes are effectively unavailable until circumstances change. For example, a province without one of your faction leaders cannot recruit from the Free Hero panel, and a hero may refuse forever if they have a permanent reason not to join your faction.
|
||||
|
||||
### What `WOULD_JOIN` Means
|
||||
|
||||
`WOULD_JOIN` means the hero meets the normal recruitment threshold for the faction that rules their current province. It is faction-specific: another faction may not qualify to recruit the same hero. The calculation uses the ruling faction's prestige, its faction head's charisma, the free hero's power and ambition, the hero's current state, and any personal bias toward or against that faction. A hero who is permanently unwilling to join that faction does not qualify.
|
||||
|
||||
The status means **willing, not recruited**. To recruit the hero with [**Recruit Heroes**](/commands/#recruit-heroes):
|
||||
|
||||
1. The province must be ruled by one of your faction leaders.
|
||||
2. That ruling faction leader must personally use [**Visit Town**](/commands/#visit-town).
|
||||
3. Use **Recruit Heroes** while the leader is visiting town and select the hero.
|
||||
|
||||
The faction head does not have to visit personally. Any of your faction leaders can recruit in a province they rule, although the willingness calculation still uses the faction head's charisma.
|
||||
|
||||
Vassals may visit town for their own purposes, but they cannot use **Recruit Heroes**, so they do not automatically recruit a hero marked `WOULD_JOIN`. However, an eligible free hero in a vassal-ruled province with enough support to provide taxes may independently ask to join during the **Please Recruit Me** phase. That is a separate faction-level decision: a human player accepts or declines the request rather than the vassal deciding. AI-controlled factions accept such requests automatically.
|
||||
|
||||
`WOULD_JOIN` is not the only status that can lead to recruitment. Travelers continue to display `TRAVELER` even when they would accept, because their willingness remains unknown until a faction leader visits town. Once the leader visits, the authoritative **Recruit Heroes** list shows which free heroes that faction can recruit under the current circumstances.
|
||||
|
||||
## Relationships
|
||||
|
||||
Faction relationships have a visible diplomatic state and an internal trust value.
|
||||
|
||||
The visible relationship states are:
|
||||
|
||||
- **Hostile:** The default enemy state. Hostile factions can fight each other.
|
||||
- **Truce:** A temporary peace. Truce offers set a reset date; when that date passes, the relationship returns to hostile. A truce or alliance also causes hostile armies between those factions to turn back during the truce turn-back phase.
|
||||
- **Alliance:** A stronger cooperative relationship. Alliances do not have a reset date. They last until they are broken.
|
||||
|
||||
Trust is a hidden number used by AI diplomacy. Higher trust makes a faction more willing to accept diplomatic offers. Trust changes in a few ways:
|
||||
|
||||
- Trust rises by 1 each new round between every pair of factions.
|
||||
- Accepting a ransom offer raises the offering faction's trust in the faction that accepted it by 25.
|
||||
- Rejecting a ransom offer lowers the offering faction's trust in the rejecting faction by 25.
|
||||
- Imprisoning another faction's ambassador is a major trust hit: the ambassador's faction loses 150 trust toward the imprisoning faction.
|
||||
- Other factions also notice ambassador imprisonment: each neutral faction loses 40 trust toward the imprisoning faction.
|
||||
|
||||
## Incoming Diplomacy Offers
|
||||
|
||||
Incoming offers are unresolved diplomatic proposals from other factions. They become [special and conditional commands](/commands/#special-and-conditional-commands) such as resolving truce offers, alliance offers, invitations, ransom offers, or alliance breaks.
|
||||
|
||||
## Diplomacy Outcomes
|
||||
|
||||
An envoy sent with a truce, invitation, alliance, or break-alliance proposal leaves the acting province during **Player Commands**. The hero is unavailable for the rest of that round, including any battles, and normally returns during **Diplomacy Resolution** after the battles are complete. If the origin province is still controlled, the envoy returns there; otherwise the game tries another faction province and then a moving faction army. If none exists, the envoy becomes an outlaw in the origin province.
|
||||
|
||||
Accepting another faction's alliance break replaces the alliance with a truce. Its reset date is 12 months after acceptance. The truce remains in force through that reset month, then becomes **Hostile** when **New Round** advances the date beyond it. Accepting a break therefore provides a protected transition instead of permitting immediate war.
|
||||
|
||||
Imprisoning a messenger has concrete consequences:
|
||||
|
||||
- The messenger does not return. They leave their original faction and become a prisoner held in the imprisoning faction's senior faction leader province.
|
||||
- The messenger receives a -100 faction bias toward the imprisoning faction, making later recruitment much harder.
|
||||
- The messenger's original faction loses 150 trust toward the imprisoning faction.
|
||||
- Every third-party faction loses 40 trust toward the imprisoning faction.
|
||||
- The incoming offer is removed. If the message was an alliance break, the alliance ends immediately, both sides become **Hostile**, and no transition truce is created.
|
||||
|
||||
## Focus Province
|
||||
|
||||
A faction can have a focus province. The focus province helps direct [province orders](/provinces/#province-orders) and AI attention toward a particular province.
|
||||
|
||||
## Faction Destruction
|
||||
|
||||
For a player-oriented overview of winning, losing, and leadership succession, see [Victory and Defeat](/concepts/victory-and-defeat/).
|
||||
|
||||
A faction is destroyed if either of these is true:
|
||||
|
||||
- It has no ruled provinces and no moving armies.
|
||||
- It has no faction leaders left.
|
||||
|
||||
The leader case has one extra succession step. If a faction loses its available leaders but still has provinces or armies, the game first looks for the strongest surviving vassal and promotes that hero to faction leader and faction head. If no vassal can rise, the faction has no faction leaders left and is destroyed.
|
||||
|
||||
When a faction is destroyed:
|
||||
|
||||
- Its provinces lose their ruling faction.
|
||||
- Its incoming armies are removed, and their supplies are left in the province.
|
||||
- Its heroes lose their faction assignment.
|
||||
- Other factions lose relationships and incoming offers involving the destroyed faction.
|
||||
- If only one faction remains, the game ends and that faction wins. If no factions remain, the game ends with no winner.
|
||||
@@ -0,0 +1,270 @@
|
||||
---
|
||||
title: Heroes
|
||||
description: Reference for Eagle heroes, stats, professions, and battlefield roles.
|
||||
---
|
||||
|
||||
Heroes are named characters who lead factions, rule provinces, command troops, perform strategic commands, and appear as units in Shardok battles.
|
||||
|
||||
The player-facing hero model is `HeroView`. It includes identity, faction membership, profession, core stats, personality stats, vigor, loyalty, abilities, portrait path, and backstory text.
|
||||
|
||||
## Core Stats
|
||||
|
||||
- **Strength** affects physical power and contributes to combat-oriented commands.
|
||||
- **Agility** affects mobility, scouting-style competence, and some profession eligibility.
|
||||
- **Constitution** sets a hero's endurance profile and is closely tied to vigor.
|
||||
- **Charisma** affects leadership, loyalty, diplomacy-flavored actions, and some profession eligibility.
|
||||
- **Wisdom** affects knowledge, magic-flavored actions, and some profession eligibility.
|
||||
|
||||
Stats can also have XP values. Strategic commands often spend vigor and grant XP in the stats connected to that action.
|
||||
|
||||
## Personality Stats
|
||||
|
||||
- **Integrity** represents trustworthiness and moral reliability.
|
||||
- **Ambition** represents appetite for power or advancement.
|
||||
- **Gregariousness** represents sociability and ease with other heroes.
|
||||
- **Bravery** represents willingness to face danger.
|
||||
|
||||
These values help drive hero flavor, AI decisions, and story generation.
|
||||
|
||||
## Vigor
|
||||
|
||||
Vigor is a hero's current energy reserve. **Constitution is the hero's normal strategic vigor cap**; vigor and Constitution are related, but they are not the same stat.
|
||||
|
||||
### Strategic vigor
|
||||
|
||||
Many Eagle commands require an acting hero and spend vigor. The command reference lists each cost. A hero with too little vigor is not offered as an actor for that command.
|
||||
|
||||
Vigor recovers over time and through deliberate recovery:
|
||||
|
||||
- Each new month restores 5 vigor.
|
||||
- [Rest](/commands/#rest) restores up to 15 vigor to each faction hero in the province.
|
||||
- [Feast](/commands/#feast) restores up to 10 vigor to each faction hero in the province.
|
||||
- Recovery cannot raise strategic vigor above Constitution.
|
||||
|
||||
Strategic actions that spend vigor also grant Constitution XP. Constitution can rise through normal stat advancement, increasing the hero's future strategic vigor cap.
|
||||
|
||||
### Vigor in a Shardok battle
|
||||
|
||||
A hero enters battle with the vigor they currently have after strategic costs such as marching. That value becomes the hero's **battle-start vigor** and the recovery ceiling for that battle. A tired hero can therefore enter with a battle recovery ceiling below their Constitution. The ceiling does not fall as the battle proceeds; current vigor falls and rises beneath it.
|
||||
|
||||
Tactical vigor affects what a hero-led unit can do:
|
||||
|
||||
- Higher current vigor contributes to the unit's maximum Action Points. A normal unit's maximum is 5 plus rounded `vigor / 25` plus rounded `morale / 25`.
|
||||
- Spending Action Points normally spends 0.5 vigor per AP, and some profession abilities have additional vigor costs.
|
||||
- Combat damage and battlefield effects can reduce vigor.
|
||||
- Ending an activation with unused AP, [Rest](/shardok-commands/#stop-or-rest), and effects such as [Holy Wave](/shardok-commands/#holy-wave) can restore vigor, but not above battle-start vigor.
|
||||
- Below 10 vigor, most ordinary tactical commands are unavailable. A few cleanup or required commands are exceptions.
|
||||
- At zero vigor, the hero-led unit is resolved as captured. Its surviving battalion does not remain on the map as an independently acting unit.
|
||||
|
||||
While the hero remains active, their stats affect the battalion's combat swing, they add personal damage, and their profession can unlock special commands. Low vigor does not directly scale those contributions down point by point; it reduces AP and command access, with the decisive removal occurring at zero.
|
||||
|
||||
When a tactical display shows `current / battle start`, the first number is current vigor and the second is the battle-start recovery ceiling—not Constitution. Enemy details may be hidden by battlefield knowledge. An absent or hidden enemy value must not be interpreted as an actual zero.
|
||||
|
||||
## Loyalty
|
||||
|
||||
Loyalty measures a hero's attachment to their faction. Feasts, events, captures, faction bias, and story consequences can move loyalty over time.
|
||||
|
||||
Loyalty is not faction bias. Loyalty describes an affiliated hero's current attachment; bias helps determine whether an unaffiliated or imprisoned hero will join a particular faction.
|
||||
|
||||
### Loyalty Departure
|
||||
|
||||
Any hero with low loyalty (below 50) may leave at any time. The game checks for departures automatically during the **Hero Departures** phase of each strategic round, before players and vassals issue commands. Lower loyalty means a greater chance of departure.
|
||||
|
||||
When a hero leaves, they stop belonging to the faction and become an unaffiliated traveler in the same province. They do not take gold or food with them. If other faction heroes remain, the province and its supplies remain with the faction. If every affiliated hero leaves, the province becomes unowned and its stored gold and food are lost.
|
||||
|
||||
## Professions
|
||||
|
||||
Professions give heroes special command access, bonuses, and tactical identity. Some effects are Eagle strategic effects, while others apply only when the hero is attached to a Shardok battle unit.
|
||||
|
||||
Several Shardok profession commands also depend on the attached battalion type. For example, many magic commands require a battalion type that allows casting, and stealth commands require a battalion type that allows stealth.
|
||||
|
||||
### Mage
|
||||
|
||||
Eagle effects:
|
||||
|
||||
- Unlocks **Control Weather** for a mage in the acting province with enough vigor.
|
||||
- Can start or end blizzards and droughts in the acting province or a neighboring province.
|
||||
- Counts as a profession for general hero power and riot-crackdown hero power.
|
||||
- Makes the hero naturally capable of starting fires.
|
||||
|
||||
Shardok effects:
|
||||
|
||||
- Unlocks **Lightning Bolt** when attached to a casting-capable unit.
|
||||
- Unlocks the multi-round **Meteor** command chain: start, target, cast, and cancel.
|
||||
- Unlocks **Freeze Water** on adjacent water when attached to a casting-capable unit.
|
||||
- Improves **Start Fire** odds when the unit can start fires.
|
||||
- Changes hero melee damage from physical damage to magical damage: lightning, cold, fire, and aether essence.
|
||||
- Can influence AI water-crossing evaluation because Mage can freeze water.
|
||||
|
||||
Profession gain:
|
||||
|
||||
- Wisdom is the qualifying stat for Mage.
|
||||
|
||||
### Necromancer
|
||||
|
||||
Eagle effects:
|
||||
|
||||
- Unlocks **Start Epidemic** for a necromancer in the acting province with enough vigor.
|
||||
- Epidemics can target the acting province or neighboring provinces, excluding friendly provinces that already have an epidemic.
|
||||
- Counts as a profession for general hero power and riot-crackdown hero power.
|
||||
|
||||
Shardok effects:
|
||||
|
||||
- Unlocks **Raise Dead** when attached to a casting-capable unit and not already controlling a unit.
|
||||
- Unlocks **Fear** against eligible enemy units when attached to a casting-capable unit.
|
||||
- Raised dead and fear use the hero's charisma and wisdom in their odds.
|
||||
|
||||
Profession gain:
|
||||
|
||||
- Charisma is one qualifying stat for Necromancer.
|
||||
|
||||
### Engineer
|
||||
|
||||
Eagle effects:
|
||||
|
||||
- Is recommended first for **Improve** when an engineer is available.
|
||||
- Adds the configured engineer bonus to province improvements and devastation repair.
|
||||
- Counts as a profession for general hero power and riot-crackdown hero power.
|
||||
|
||||
Shardok effects:
|
||||
|
||||
- Unlocks **Build Bridge** from non-water terrain onto adjacent water.
|
||||
- Unlocks **Repair** for damaged bridges and castles.
|
||||
- Unlocks **Blow Bridge** while standing on a bridge.
|
||||
- Unlocks **Reduce** from a castle or fortified position against enemy units or castles.
|
||||
- Unlocks **Fortify** on fortifiable terrain.
|
||||
- Can influence AI water-crossing evaluation because engineers can create and destroy bridge access.
|
||||
|
||||
Profession gain:
|
||||
|
||||
- Agility is one qualifying stat for Engineer.
|
||||
|
||||
### Paladin
|
||||
|
||||
Eagle effects:
|
||||
|
||||
- Improves **Alms** by multiplying the support gained per food given.
|
||||
- Is sorted first in the **Alms** hero list when eligible.
|
||||
- Counts as a profession for general hero power and riot-crackdown hero power.
|
||||
|
||||
Shardok effects:
|
||||
|
||||
- Unlocks **Holy Wave** when the paladin has enough action points.
|
||||
|
||||
Profession gain:
|
||||
|
||||
- Charisma is one qualifying stat for Paladin.
|
||||
|
||||
### Ranger
|
||||
|
||||
Eagle effects:
|
||||
|
||||
- Unlocks **Recon** for a ranger in the acting province with enough vigor.
|
||||
- Recon can target provinces not ruled by the acting faction, as long as that faction does not already have a pending recon there.
|
||||
- Counts as a profession for general hero power and riot-crackdown hero power.
|
||||
|
||||
Shardok effects:
|
||||
|
||||
- Unlocks **Scout** when attached to a stealth-capable unit.
|
||||
- Unlocks **Hide** when attached to a stealth-capable unit, outside enemy zone of control, and with multiple hideable positions.
|
||||
- Allows defending rangers with stealth-capable units to use hidden placement during setup.
|
||||
- Gives a bonus to **Brave Water** odds when the unit can brave water.
|
||||
- Reveals adjacent hidden enemies while moving.
|
||||
- Prevents adjacent enemies from hiding or remaining hidden when the ranger has enough knowledge to be identified.
|
||||
|
||||
Profession gain:
|
||||
|
||||
- Agility is one qualifying stat for Ranger.
|
||||
|
||||
### Champion
|
||||
|
||||
Eagle effects:
|
||||
|
||||
- Is recommended first for **Train** when a champion is available.
|
||||
- Adds the configured champion bonus to training gained by trainable battalions.
|
||||
- Counts as a profession for general hero power and riot-crackdown hero power.
|
||||
|
||||
Shardok effects:
|
||||
|
||||
- Unlocks **Challenge Duel** against adjacent enemy heroes outside castles.
|
||||
- The target must have an attached hero and must either have enough known vigor to accept or be insufficiently known to rule that out.
|
||||
|
||||
Profession gain:
|
||||
|
||||
- Strength is the qualifying stat for Champion.
|
||||
|
||||
### Warden
|
||||
|
||||
Eagle effects:
|
||||
|
||||
- Improves prisoner recruitment over time. At new round processing, prisoners in a province with a ruling-faction Warden gain additional faction bias toward that province's ruler.
|
||||
- Counts as a profession for general hero power and riot-crackdown hero power.
|
||||
|
||||
Shardok effects:
|
||||
|
||||
- Unlocks **Evacuate Prisoners** when the warden's unit can flee, has enough action points, and hostile captured hero units are present.
|
||||
|
||||
Profession gain:
|
||||
|
||||
- Constitution is the qualifying stat for Warden.
|
||||
|
||||
### NoProfession
|
||||
|
||||
Heroes with no profession have no profession-specific command unlocks. They can still lead, fight, train, improve, march, use many normal strategic commands, and gain a profession later.
|
||||
|
||||
## Profession Gain
|
||||
|
||||
Only heroes with **NoProfession** can gain a profession. At the end of a round, heroes with enough XP can gain stats. If a no-profession hero gains one or more stats, the game checks for profession gain:
|
||||
|
||||
- The hero gets one profession-gain roll for each stat that increased.
|
||||
- A stat must be at least **85 after the stat gains are applied** to qualify a profession.
|
||||
- Eligible professions are based on all post-gain stats at or above 85, not only the stats that increased that round.
|
||||
- Strength qualifies **Champion**.
|
||||
- Agility qualifies **Engineer** and **Ranger**.
|
||||
- Wisdom qualifies **Mage**.
|
||||
- Charisma qualifies **Necromancer** and **Paladin**.
|
||||
- Constitution qualifies **Warden**.
|
||||
|
||||
### Progression example
|
||||
|
||||
A NoProfession hero earns Agility-related XP from strategic actions. During end-of-round progression, enough XP may increase Agility. If the hero's Agility is at least 85 after that increase, Engineer and Ranger are eligible. The hero receives profession-gain rolls for stats that increased; the player does not directly choose between eligible professions.
|
||||
|
||||
If several stats increase together, each increase can create a roll and all post-gain qualifying stats participate in eligibility. A hero who already has a profession does not roll for a replacement.
|
||||
|
||||
### Planning progression
|
||||
|
||||
- Commands award XP in stats associated with the action.
|
||||
- Stat increases and profession checks happen during end-of-round processing, not when XP is awarded.
|
||||
- Reaching 85 in a qualifying stat makes its profession possible but does not guarantee a particular random result.
|
||||
- Choosing which eligible hero performs a command influences who receives its XP.
|
||||
|
||||
## Faction Leaders
|
||||
|
||||
For a side-by-side explanation of direct control, province turns, and vassal automation, see [Who You Command](/concepts/command-authority/).
|
||||
|
||||
Every faction starts with a **faction head**. The faction head is the hero who defines what the faction represents: its political identity, its public story, and the starting point for its leadership.
|
||||
|
||||
Factions can gain additional faction leaders through **Swear Kinship** with another hero. The candidate must already belong to the faction, must have at least 95 loyalty, and must be in a province with the faction head. When the command succeeds, that hero becomes one of the faction's leaders.
|
||||
|
||||
Faction leaders are directly commandable. Vassals, by contrast, lead provinces and make their own choices when their province acts. Promoting a loyal vassal into leadership gives the player more direct control over that hero.
|
||||
|
||||
Faction leaders also provide succession. If the faction head is killed, another available faction leader can take over as the new faction head.
|
||||
|
||||
## Vassals
|
||||
|
||||
A **vassal** is a hero who belongs to your faction and rules one of its provinces, but is not a faction leader. You cannot command vassals directly. Instead, their provinces act during the Vassal Commands phase, when each vassal chooses an action based on the province's standing orders and immediate needs.
|
||||
|
||||
Vassals can be given standing orders with **Issue Orders**, promoted to faction leaders with **Swear Kinship**, or removed from the faction with **Exile Vassal**.
|
||||
|
||||
## Unaffiliated Heroes
|
||||
|
||||
Unaffiliated heroes can appear in provinces before joining a faction. Their province entry includes name, profession, status, leader flag, and recruitment information.
|
||||
|
||||
Common interactions include recruiting them, divining more information about them, declining their quests, or responding when they ask to join.
|
||||
|
||||
## Battle Capabilities
|
||||
|
||||
Hero views expose whether a hero is archery-capable or start-fire-capable. Those capabilities affect Shardok command availability when the hero is represented in battle.
|
||||
|
||||
- **Archery-capable**: the hero needs at least 75 agility.
|
||||
- **Start-fire-capable**: the hero is always capable if they are a Mage. Otherwise, they need at least 70 agility and at least 70 wisdom.
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
title: Eagle0pedia
|
||||
description: Reference companion for Eagle0 commands, systems, and game concepts.
|
||||
---
|
||||
|
||||
Eagle0pedia is the in-game reference companion for Eagle0. The tutorial answers "what should I do next?"; these pages answer "what does this mean?"
|
||||
|
||||
Eagle0 is a strategy game about factions, heroes, provinces, armies, diplomacy, and tactical battles. The broad strategic layer is **Eagle**: provinces produce food and gold, heroes lead factions or rule as vassals, armies march across the map, and diplomacy shapes the war. When armies clash, battles are resolved in **Shardok**, the tactical hex-based combat system.
|
||||
|
||||
Your strategic goal is to become the last surviving faction. Start with [Victory and Defeat](concepts/victory-and-defeat/) to learn how factions win, survive leadership losses, and leave the game.
|
||||
|
||||
Use these pages when you want a quick reference for commands, hero professions, faction leadership, province stats, or tactical battle options.
|
||||
|
||||
## Sections
|
||||
|
||||
- [Game concepts](concepts/) explains the flow of Eagle rounds, the handoff from Eagle to Shardok, tactical battles, and victory conditions.
|
||||
- [Eagle round flow](concepts/eagle-round-flow/) explains strategic phases and simultaneous player action.
|
||||
- [From Eagle to Shardok](concepts/eagle-to-shardok/) explains how strategic armies become tactical battles.
|
||||
- [Shardok battles](concepts/shardok-battles/) explains tactical battle structure, units, terrain, and aftermath.
|
||||
- [Shardok victory conditions](concepts/shardok-victory-conditions/) explains how tactical battles end.
|
||||
- [Eagle commands](commands/) explains the strategic commands available from province command panels.
|
||||
- [Shardok commands](shardok-commands/) explains tactical battle commands and profession-gated battlefield actions.
|
||||
- [Heroes](heroes/) covers stats, vigor, loyalty, professions, faction leaders, and battle capabilities.
|
||||
- [Battalions](battalions/) covers troop count, type, training, armament, morale, heroes, and battlefield outcomes.
|
||||
- [Factions](factions/) covers faction heads, leaders, prestige, diplomacy, and focus provinces.
|
||||
- [Provinces](provinces/) covers province stats, supplies, orders, armies, maps, castles, and unaffiliated heroes.
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
title: Integration Notes
|
||||
description: Technical notes for Eagle0pedia links, deployment, and review.
|
||||
---
|
||||
|
||||
Eagle0pedia lives close to the game data and can be reviewed with code changes before being deployed to `pedia.eagle0.net`.
|
||||
|
||||
## Link Targets
|
||||
|
||||
Unity info buttons should link to stable concept URLs. For the command panel, start with anchors on the command reference page:
|
||||
|
||||
- `https://pedia.eagle0.net/commands/#train`
|
||||
- `https://pedia.eagle0.net/commands/#march`
|
||||
- `https://pedia.eagle0.net/commands/#diplomacy`
|
||||
- `https://pedia.eagle0.net/shardok-commands/#melee`
|
||||
- `https://pedia.eagle0.net/heroes/#professions`
|
||||
- `https://pedia.eagle0.net/provinces/#province-stats`
|
||||
|
||||
When individual command pages are split out, keep redirects or aliases for these anchors so older clients still land somewhere useful.
|
||||
@@ -0,0 +1,229 @@
|
||||
---
|
||||
title: Provinces
|
||||
description: Reference for Eagle provinces, resources, infrastructure, orders, and tactical maps.
|
||||
---
|
||||
|
||||
Provinces are the strategic map locations in Eagle. They hold resources, heroes, battalions, province orders, incoming armies, local events, and the Shardok map used when battles occur there.
|
||||
|
||||
The player-facing province model is `ProvinceView`. Public information includes id, name, ruling faction, known events, known incoming armies, and an `as_of` date. When you have full information, `FullProvinceInfo` adds the detailed local state: gold, food, caps, support, food consumption, battalions, heroes, orders, incoming shipments, and local events.
|
||||
|
||||
<!--
|
||||
Source map:
|
||||
- Province view fields: src/main/protobuf/net/eagle0/eagle/views/province_view.proto and src/main/scala/net/eagle0/eagle/library/util/view_filters/ProvinceViewFilter.scala.
|
||||
- Province utilities: src/main/scala/net/eagle0/eagle/library/util/province/ProvinceUtils.scala.
|
||||
- New year production and yearly degradation: src/main/scala/net/eagle0/eagle/library/actions/impl/action/NewYearAction.scala.
|
||||
- New round caps, price-index drift, empty-province devastation repair, and hero-cap loyalty pressure: src/main/scala/net/eagle0/eagle/library/actions/impl/action/NewRoundAction.scala.
|
||||
- Food consumption and starvation: src/main/scala/net/eagle0/eagle/library/actions/impl/action/PerformFoodConsumptionPhaseAction.scala.
|
||||
- Shipment arrival: src/main/scala/net/eagle0/eagle/library/actions/impl/action/ShipmentArrivedAction.scala and src/main/scala/net/eagle0/eagle/library/actions/impl/action/PerformProvinceMoveResolutionAction.scala.
|
||||
- Price index: src/main/scala/net/eagle0/eagle/library/util/PriceIndexUtils.scala.
|
||||
- Settings: src/main/resources/net/eagle0/eagle/settings.tsv.
|
||||
-->
|
||||
|
||||
## Strategic Role
|
||||
|
||||
A province is both a resource base and a staging point. It can:
|
||||
|
||||
- Produce gold and food at the new year.
|
||||
- Store gold and food between commands.
|
||||
- Feed stationed battalions each round.
|
||||
- Hold heroes, battalions, prisoners, unaffiliated heroes, and events.
|
||||
- Launch March, Send Supplies, Train, Improve, Arm Troops, Trade, and other province commands.
|
||||
- Become a Shardok battlefield when hostile armies meet there.
|
||||
|
||||
The important strategic question is usually not just "how high are the stats?" It is "can this province keep support above 40, feed its troops, store enough supplies, and stage the actions I need before another faction interferes?"
|
||||
|
||||
## Development Stats
|
||||
|
||||
The three main development stats are **economy**, **agriculture**, and **infrastructure**.
|
||||
|
||||
- **Economy** drives annual gold production. A supported province produces 35 gold per effective economy at the new year.
|
||||
- **Agriculture** drives annual food production. A supported province produces 50 food per effective agriculture at the new year.
|
||||
- **Infrastructure** increases gold and food storage caps, affects improvement quality, and caps how far battalions can be armed.
|
||||
|
||||
Each stat has a matching devastation track. The effective value is the stat minus its devastation, never below 0. For example, 80 agriculture with 25 agriculture devastation behaves like 55 effective agriculture for production and other effective-stat checks.
|
||||
|
||||
Improve can raise economy, agriculture, or infrastructure. It can also repair devastation across all three devastation tracks. Engineers get a bonus when improving.
|
||||
|
||||
## Support
|
||||
|
||||
Support is local backing for the ruling faction. It ranges from 0 to 100 and is one of the most important province numbers.
|
||||
|
||||
A province needs at least **40 support** to provide annual gold and food production. If support is below 40, the province does not produce taxes at the new year. That also matters for prestige: supported provinces are worth prestige to the ruling faction.
|
||||
|
||||
Support changes through several commands and events:
|
||||
|
||||
- **Alms** spends food to raise support.
|
||||
- **Improve** raises support as a side effect.
|
||||
- **Riot** damage reduces support by 25 if the riot is not stopped.
|
||||
- **Suppress Beasts** can raise support on success.
|
||||
- Province events can raise or lower support.
|
||||
|
||||
Support also affects risk. Low support makes the province less useful economically and affects riot timing.
|
||||
|
||||
## Gold
|
||||
|
||||
Gold pays for many strategic actions. Feast, Organize Troops, Arm Troops, Divine, Hero Gift, diplomacy, Trade buys, and some other commands spend gold.
|
||||
|
||||
At the new year, supported provinces gain annual gold equal to:
|
||||
|
||||
`effective economy * 35`
|
||||
|
||||
Gold is stored in the province until spent, traded, shipped, marched away, captured through conquest, or lost because the province is over its storage cap.
|
||||
|
||||
Feast costs gold, not food: 20 gold per ruling-faction hero in the province, multiplied by the province price index.
|
||||
|
||||
## Food
|
||||
|
||||
Food supports armies and can also be spent strategically.
|
||||
|
||||
At the new year, supported provinces gain annual food equal to:
|
||||
|
||||
`effective agriculture * 50`
|
||||
|
||||
Food is consumed during the Food Consumption phase by battalions stationed in the province and by moving armies. The province view exposes monthly food consumption so you can judge how long the province can feed its current troops.
|
||||
|
||||
Food is also used by:
|
||||
|
||||
- **Alms**, which spends food to raise support.
|
||||
- **Trade**, which buys or sells food for gold.
|
||||
- **Send Supplies**, which can ship food to another province.
|
||||
- **March**, which can carry food with moving armies.
|
||||
- Riot gifts, if you choose to give food.
|
||||
|
||||
## Food Consumption and Starvation
|
||||
|
||||
Food consumption happens during the Food Consumption phase, after Province Move Resolution, player commands, hostile army setup, and battle requests, but before Battle Resolution.
|
||||
|
||||
This timing matters:
|
||||
|
||||
- A Send Supplies shipment that arrives during Province Move Resolution is available before the destination province consumes food later that same round.
|
||||
- A friendly marching army that arrives during Province Move Resolution can also bring food before that round's food consumption.
|
||||
- Moving armies consume food from their own carried supplies while they are still incoming.
|
||||
|
||||
If a province cannot feed its battalions, it spends all remaining province food and the battalions are reduced. The maximum battalion reduction from a food shortage is 20% in a single food-consumption event.
|
||||
|
||||
The same starvation rule applies to moving armies: if the army's carried food is not enough, the moving battalions are reduced and the army's food drops to 0.
|
||||
|
||||
Low-food warnings look ahead to incoming food, upcoming taxes, support, and expected production. If the province has enough food arriving before the next consumption, the warning can account for that.
|
||||
|
||||
## Storage Caps
|
||||
|
||||
Provinces have separate gold and food caps. These caps are based on effective development and infrastructure:
|
||||
|
||||
- Gold cap uses effective economy plus effective infrastructure.
|
||||
- Food cap uses effective agriculture plus effective infrastructure.
|
||||
- Both start from a base storage limit of 5000.
|
||||
- Each point of relevant development or infrastructure adds 50 storage.
|
||||
- A faction's focus province gets 1.5x storage.
|
||||
|
||||
Being over cap is not an immediate hard cutoff. During New Round processing, the province loses 20% of the amount above each cap, rounded up. Excess supplies therefore decay over time unless you spend them, ship them, trade them, or move them.
|
||||
|
||||
## Price Index
|
||||
|
||||
Price index is local price pressure. It changes costs and trade prices in the province.
|
||||
|
||||
The normal range is 0.75 to 1.5. A price index above 1 makes local purchases more expensive; below 1 makes them cheaper.
|
||||
|
||||
Gold spending can push the price index upward. Selling food counts as negative gold spending and can push it downward. Each gold spent changes the index by 0.0001, clamped to the normal range.
|
||||
|
||||
The price index also drifts each new round toward a local steady state based on development and devastation. It moves 10% of the distance toward that steady state per round.
|
||||
|
||||
Commands affected by price index include:
|
||||
|
||||
- **Feast** gold cost.
|
||||
- **Organize Troops** troop costs.
|
||||
- **Arm Troops** armament costs.
|
||||
- **Trade** buy and sell prices.
|
||||
- **Divine** cost.
|
||||
- **Hero Gift** loyalty gained per gold.
|
||||
|
||||
## Devastation
|
||||
|
||||
Devastation is damage to province systems. Economy, agriculture, and infrastructure each have their own devastation value.
|
||||
|
||||
Devastation reduces the corresponding effective stat. That means:
|
||||
|
||||
- Economy devastation reduces gold production and gold storage.
|
||||
- Agriculture devastation reduces food production and food storage.
|
||||
- Infrastructure devastation reduces storage, armament ceiling, and infrastructure-dependent checks.
|
||||
|
||||
Battles and province events can add devastation. Riots add economy and infrastructure devastation. Blizzards, droughts, floods, epidemics, and battle results can damage different province systems.
|
||||
|
||||
Improve with the Devastation option repairs all three devastation tracks. Empty provinces also recover devastation slowly during new round processing.
|
||||
|
||||
## Infrastructure and Troops
|
||||
|
||||
Infrastructure matters militarily because it caps armament. Arm Troops cannot raise a battalion's ARM above the province's effective infrastructure.
|
||||
|
||||
Economy and agriculture also affect which battalion types can be raised in the province. Battalion type availability checks the province's effective economy and effective agriculture against the battalion type's minimums.
|
||||
|
||||
Training is handled by [**Train**](/commands/#train). Armament is handled by [**Arm Troops**](/commands/#arm-troops). Organizing troop counts, creating battalions, resizing battalions, and dismissing battalions are handled by [**Organize Troops**](/commands/#organize-troops).
|
||||
|
||||
## Heroes and Hero Cap
|
||||
|
||||
A province has a hero cap. The default map data sets hero cap from castle count: each castle gives 5 hero capacity.
|
||||
|
||||
Going over the cap is allowed, but it creates loyalty pressure. During new round processing, if the province has too many established ruling-faction heroes, those heroes lose loyalty. Newly joined heroes are exempt for their first few rounds.
|
||||
|
||||
This is why moving heroes around is not just about command access. Keeping too many heroes stacked in one province can create long-term loyalty damage.
|
||||
|
||||
## Ruling Faction and Ruling Hero
|
||||
|
||||
A province can have a ruling faction and a ruling hero. The ruling hero is the local leader for the province.
|
||||
|
||||
If a faction leader rules the province, the player can command that leader directly. If a [vassal](/heroes/#vassals) rules it, the province acts through vassal behavior during the [Vassal Commands phase](/concepts/eagle-round-flow/#vassal-commands) unless another special phase interrupts it.
|
||||
|
||||
Province leadership also matters for riots. The province leader's Charisma affects riot size and gift success, even if another hero is selected to crack down.
|
||||
|
||||
## Province Orders
|
||||
|
||||
Province orders are standing instructions for vassal-led provinces. The [**Issue Orders**](/commands/#issue-orders) command changes them and can also set or clear the faction focus province.
|
||||
|
||||
The main orders are:
|
||||
|
||||
- **Entrust:** let the vassal judge the situation.
|
||||
- **Develop:** build up and improve the province.
|
||||
- **Mobilize:** prepare military strength and supplies.
|
||||
- **Expand:** look for growth, movement, and attack opportunities.
|
||||
|
||||
The focus province also affects logistics. Vassals may ship excess gold and food toward the focus province if one is set, or toward the nearest faction leader if not.
|
||||
|
||||
## Incoming Armies
|
||||
|
||||
Incoming armies represent forces that are moving toward the province but have not arrived or resolved yet.
|
||||
|
||||
Friendly incoming armies arrive during [Province Move Resolution](/concepts/eagle-round-flow/#province-move-resolution) if the destination is already ruled by their faction. Hostile arrivals do not simply enter the province; they can become hostile army groups and later trigger conquest, attack decisions, defense decisions, battle requests, and [Shardok battles](/concepts/eagle-to-shardok/).
|
||||
|
||||
Incoming armies can carry gold and food. If supplies arrive with a friendly movement, transit loss can reduce the amount received.
|
||||
|
||||
## Incoming Shipments
|
||||
|
||||
Incoming shipments are supplies sent by [**Send Supplies**](/commands/#send-supplies).
|
||||
|
||||
The origin province loses the selected gold and food immediately. The destination receives an incoming shipment scheduled for the next round. During the next round's Province Move Resolution phase, the shipment arrives before food consumption. 10% is lost in transit, rounded down after loss.
|
||||
|
||||
Because shipments arrive before food consumption, shipping food can prevent starvation in the destination province during the arrival round.
|
||||
|
||||
## Province Events
|
||||
|
||||
Province events are temporary or local conditions that affect the province.
|
||||
|
||||
Important event types include:
|
||||
|
||||
- **Riot:** requires a decision if it reaches Handle Riot. If not stopped, it causes support loss and devastation.
|
||||
- **Beasts:** can damage support over time and can be suppressed for rewards and prestige.
|
||||
- **Blizzard:** damages province systems and affects supplies.
|
||||
- **Drought:** damages agriculture.
|
||||
- **Flood:** damages infrastructure and agriculture.
|
||||
- **Epidemic:** damages economy, battalions, and hero vigor, and can spread or end over time.
|
||||
- **Festival:** improves support and prestige while active.
|
||||
|
||||
Some events can be created or changed by commands. Mages can use [**Control Weather**](/commands/#control-weather) to start or end blizzards and droughts. Necromancers can use [**Start Epidemic**](/commands/#start-epidemic).
|
||||
|
||||
## Tactical Map and Castles
|
||||
|
||||
Each province has a `hex_map_name`. When a battle occurs there, Eagle uses that name to choose the Shardok tactical map.
|
||||
|
||||
`castle_count` records the province's fortification count. Castles matter for hero capacity, defensive setup, tactical objectives, and some battle behavior.
|
||||
|
||||
The source map data in `province_map.tsv` defines province ids, names, neighbor ids, neighbor positions, tactical map names, neighbor names, and castle counts.
|
||||
@@ -0,0 +1,452 @@
|
||||
---
|
||||
title: Shardok Commands
|
||||
description: Tactical command reference for Shardok battles.
|
||||
---
|
||||
|
||||
<span class="command-reference-marker" aria-hidden="true"></span>
|
||||
<span class="command-reference-five-row-marker" aria-hidden="true"></span>
|
||||
|
||||
<!--
|
||||
Source map for Shardok command requirements/costs:
|
||||
- Shared morale/vigor filters: src/main/resources/net/eagle0/shardok/settings.tsv fields minimumMoraleToAct=10 and minimumVigorToAct=10; applied in src/main/cpp/net/eagle0/shardok/library/AvailableCommandsFactory.cpp.
|
||||
- Fixed AP setting values: src/main/resources/net/eagle0/shardok/settings.tsv. Most command factories call SettingsGetter::ActionCostFor(...) in src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.cpp, which returns ActionCost::usesAll; see src/main/cpp/net/eagle0/shardok/library/ActionCost.hpp and src/main/cpp/net/eagle0/shardok/library/unit/Unit.cpp.
|
||||
- Melee: meleeActionPointCost=5; src/main/cpp/net/eagle0/shardok/library/command_factories/melee_command_factory/MeleeCommandFactory.cpp.
|
||||
- Archery: archeryActionPointCost=5 and longbowWindBonusRangeMinSpeed=20; hero archery threshold is agility >= 75 in src/main/cpp/net/eagle0/shardok/ai_battle_simulator/AiBattleSimulator.cpp; normal targets come from TwoAwayTilesUnblockedByMountains in src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.cpp; longbow castle-adjacent and wind-assisted range-3 exceptions are in src/main/cpp/net/eagle0/shardok/library/command_factories/archery_command_factory/ArcheryCommandFactory.cpp; volley spend is in src/main/cpp/net/eagle0/shardok/library/commands/ArcheryCommand.cpp.
|
||||
- Charge: terrain-entry cost from BattalionType::GetCostToEnterTerrain(...); allowsMoveAfterCharge is true for Light Cavalry and Heavy Cavalry in src/main/resources/net/eagle0/shardok/battalionTypes.tsv; stun settings are baseStunChanceOnCharge and trainingDifferenceMultiplierForStunChanceOnCharge in the same file; src/main/cpp/net/eagle0/shardok/library/command_factories/ChargeCommandFactory.cpp.
|
||||
- Move: per-hex terrain-entry costs, plus extraCostForZocMovement=1; src/main/cpp/net/eagle0/shardok/library/command_factories/MoveCommandFactory.cpp and src/main/cpp/net/eagle0/shardok/library/commands/MoveCommand.cpp.
|
||||
- Lightning Bolt: lightningActionPointCost=5 and lightningRange=3; src/main/cpp/net/eagle0/shardok/library/command_factories/LightningBoltCommandFactory.cpp and src/main/cpp/net/eagle0/shardok/library/commands/LightningBoltCommand.cpp.
|
||||
- Meteor: minimumVigorForMeteor=30, meteorStartActionPointCost=5, meteorTargetActionPointCost=0, meteorCancelActionPointCost=0, meteorCastActionPointCost=1, meteorCastVigorCost=20, meteorRange=3, meteorSplashFactor=0.4, and meteorCastWisdomXp=5; src/main/cpp/net/eagle0/shardok/library/command_factories/MeteorStartCommandFactory.cpp, MeteorTargetCommandFactory.cpp, src/main/cpp/net/eagle0/shardok/library/util/MeteorRangeCoords.cpp, src/main/cpp/net/eagle0/shardok/library/commands/MeteorStartCommand.cpp, MeteorTargetCommand.cpp, MeteorCancelCommand.cpp, and src/main/cpp/net/eagle0/shardok/library/actions/MeteorCastAction.cpp.
|
||||
- Start Fire: startFireActionPointCost=5; hero start-fire thresholds are Mage or agility >= 70 and wisdom >= 70 in src/main/cpp/net/eagle0/shardok/ai_battle_simulator/AiBattleSimulator.cpp; src/main/cpp/net/eagle0/shardok/library/command_factories/StartFireCommandFactory.cpp and src/main/cpp/net/eagle0/shardok/library/commands/StartFireCommand.cpp.
|
||||
- Extinguish Fire: extinguishActionPointCost=5; src/main/cpp/net/eagle0/shardok/library/command_factories/ExtinguishFireCommandFactory.cpp and src/main/cpp/net/eagle0/shardok/library/commands/ExtinguishFireCommand.cpp.
|
||||
- Freeze Water: freezeWaterActionPointCost=5; src/main/cpp/net/eagle0/shardok/library/command_factories/FreezeWaterCommandFactory.cpp.
|
||||
- Build Bridge: buildBridgeActionPointCost=5; src/main/cpp/net/eagle0/shardok/library/command_factories/BuildBridgeCommandFactory.cpp.
|
||||
- Blow Bridge: blowBridgeActionPointCost=5; src/main/cpp/net/eagle0/shardok/library/command_factories/BlowBridgeCommandFactory.cpp.
|
||||
- Placement: no AP cost in setup commands; src/main/cpp/net/eagle0/shardok/library/actions/PlaceUnitCommand.cpp and PlaceHiddenUnitCommand.cpp.
|
||||
- Scout: scoutActionPointCost=5 and scoutRange=4; src/main/cpp/net/eagle0/shardok/library/command_factories/ScoutCommandFactory.cpp.
|
||||
- Raise Dead: raiseDeadActionPointCost=5, raiseDeadRange=2, raiseDeadStartingSize=100, raiseDeadWisdomXp=1, and raiseDeadCharismaXp=2; src/main/cpp/net/eagle0/shardok/library/command_factories/RaiseDeadCommandFactory.cpp and src/main/cpp/net/eagle0/shardok/library/commands/RaiseDeadCommand.cpp.
|
||||
- Dismiss Unit: dismissBaseOdds=60 and requires remainingActionPoints > 0 in src/main/cpp/net/eagle0/shardok/library/command_factories/ControlCommandFactory.cpp; execution zeroes both units in src/main/cpp/net/eagle0/shardok/library/commands/DismissUnitCommand.cpp.
|
||||
- Holy Wave: holyWaveActionPointCost=5, holyWaveVigorCost=10, holyWaveInspireBonus=25, maxInspireOverBase=10, holyWaveVigorBuf=5, holyWaveDamagePercentage=0.7, and holyWaveCharismaXp=5; src/main/cpp/net/eagle0/shardok/library/command_factories/HolyWaveCommandFactory.cpp and src/main/cpp/net/eagle0/shardok/library/commands/HolyWaveCommand.cpp.
|
||||
- Stop and Rest: stop posts no AP cost; actionCostToVigorCost=0.5 and rest zeroes AP in src/main/cpp/net/eagle0/shardok/library/commands/UnitRestCommand.cpp; availability and stop implementation are in ChargeCommandFactory.cpp and UnitStopCommand.cpp.
|
||||
- Challenge Duel: challengeDuelActionPointCost=5, minimumVigorToAcceptDuel=30, duelDeclinedMoraleAdjustment=-15, duelWonMoraleBoost=15, and duelWinnerCharismaXp=20; src/main/cpp/net/eagle0/shardok/library/command_factories/ChallengeDuelCommandFactory.cpp and src/main/cpp/net/eagle0/shardok/library/commands/challenge_duel_command/ChallengeDuelCommand.cpp.
|
||||
- Fear: fearActionPointCost=5, fearRange=3, fearDebuf=-25, fearFailedBuf=10, and fearCharismaXp=2; src/main/cpp/net/eagle0/shardok/library/command_factories/FearCommandFactory.cpp and src/main/cpp/net/eagle0/shardok/library/commands/FearCommand.cpp.
|
||||
- Repair: repairActionPointCost=5 and repairBuf=25; src/main/cpp/net/eagle0/shardok/library/command_factories/RepairCommandFactory.cpp and src/main/cpp/net/eagle0/shardok/library/commands/RepairCommand.cpp.
|
||||
- Reduce: reduceActionPointCost=5, reduceRange=3, and reduceForestBonus=50; src/main/cpp/net/eagle0/shardok/library/command_factories/ReduceCommandFactory.cpp and src/main/cpp/net/eagle0/shardok/library/commands/ReduceCommand.cpp.
|
||||
- Flee and Retreat: fleeActionPointCost=5 and retreatActionPointCost=5; src/main/cpp/net/eagle0/shardok/library/command_factories/FleeCommandFactory.cpp and RetreatCommandFactory.cpp.
|
||||
- Control: requires remainingActionPoints > 0 in ControlCommandFactory.cpp; execution zeroes acting unit in src/main/cpp/net/eagle0/shardok/library/commands/ControlCommand.cpp.
|
||||
- Reinforce: reinforceActionPointCost=5; src/main/cpp/net/eagle0/shardok/library/command_factories/ReinforceCommandFactory.cpp.
|
||||
- Brave Water: braveWaterActionPointCost=8, braveWaterRangerBonus=20, and braveWaterMaxLoss=0.2; src/main/cpp/net/eagle0/shardok/library/command_factories/BraveWaterCommandFactory.cpp and src/main/cpp/net/eagle0/shardok/library/commands/BraveWaterCommand.cpp.
|
||||
- Release Unit: requires remainingActionPoints > 0 in ControlCommandFactory.cpp; execution zeroes both units in src/main/cpp/net/eagle0/shardok/library/commands/ReleaseUnitCommand.cpp.
|
||||
- End Turn and Setup: no command AP cost; end turn zeroes player unit AP in src/main/cpp/net/eagle0/shardok/library/commands/EndTurnCommand.cpp; setup end in src/main/cpp/net/eagle0/shardok/library/actions/EndPlayerSetupCommand.cpp.
|
||||
- Hide: hideActionPointCost=5; src/main/cpp/net/eagle0/shardok/library/command_factories/HideCommandFactory.cpp.
|
||||
- Fortify: fortifyActionPointCost=5 and fortifyDamageTakenMultiplier=0.9; src/main/cpp/net/eagle0/shardok/library/command_factories/FortifyCommandFactory.cpp, src/main/cpp/net/eagle0/shardok/library/commands/FortifyCommand.cpp, and the commands/actions that clear fortified status.
|
||||
- Become Outlaw: uses fleeActionPointCost=5 through src/main/cpp/net/eagle0/shardok/library/command_factories/FleeCommandFactory.cpp and src/main/cpp/net/eagle0/shardok/library/commands/BecomeOutlawCommand.cpp.
|
||||
- Evacuate Prisoners: evacuatePrisonersActionPointCost=5; src/main/cpp/net/eagle0/shardok/library/command_factories/EvacuatePrisonersCommandFactory.cpp.
|
||||
-->
|
||||
|
||||
## Shared Requirements
|
||||
|
||||
Unless noted otherwise: **10 morale**, **10 hero vigor**, and the listed **Action Point cost**.
|
||||
|
||||
## Activations and follow-ups
|
||||
|
||||
A unit can keep acting while it has Action Points (AP) and at least one available command. Read each command's AP line as follows:
|
||||
|
||||
- **Consumes all remaining AP** or **sets the unit to 0 AP** means **ends activation**. The unit cannot act again this turn.
|
||||
- A command with only an exact AP cost spends that amount. If the unit still has AP afterward, its activation can continue with whatever commands are then available.
|
||||
- A command may also end the unit's useful activation because no legal commands remain, even if the display still shows AP.
|
||||
|
||||
Movement is the most important case. Each destination shows its movement cost and may show **follow-up available** icons. Those follow-ups are actions predicted to be legal *after reaching that destination*, such as Charge, Scout, Start Fire, Repair, Reduce, or Reinforce.
|
||||
|
||||
The follow-up list is a planning aid, not a complete list of everything the unit might do next. It does not include another Move, and battlefield information can change when the move resolves. A destination with no follow-up icons does not itself mean “ends activation”: compare the movement cost with the unit's remaining AP and check the commands offered after arrival.
|
||||
|
||||
## Tactical Commands
|
||||
|
||||
### Melee
|
||||
|
||||
<b>Unit requirements:</b> any unit.
|
||||
|
||||
<b>Action Point cost:</b> 5 AP; consumes all remaining AP.
|
||||
|
||||
<b>Available when:</b> an enemy target is adjacent and the acting unit can still act.
|
||||
|
||||
<b>Effect:</b> Attack the target in close combat. The UI may show combat odds before you choose the attack.
|
||||
|
||||
<b>Selection:</b> choose the attacking unit and adjacent target.
|
||||
|
||||
### Archery
|
||||
|
||||
<b>Unit requirements:</b> Longbowmen, or a unit with an attached hero who has at least 75 AGI. The unit must have at least one volley remaining.
|
||||
|
||||
<b>Action Point cost:</b> 5 AP; consumes all remaining AP.
|
||||
|
||||
<b>Available when:</b> a known enemy is exactly 2 hexes away, and the line between the attacker and target is not blocked by mountains.
|
||||
|
||||
<b>Effect:</b> Make a ranged attack without requiring adjacency, spending one volley. Longbowmen can also fire at adjacent hexes while standing in a castle, and can fire 3 hexes when the wind is at least 20 mph and blowing toward the target.
|
||||
|
||||
<b>Selection:</b> choose the firing unit and target unit or hex.
|
||||
|
||||
### Charge
|
||||
|
||||
<b>Unit requirements:</b> any unit.
|
||||
|
||||
<b>Action Point cost:</b> the cost to enter the target's terrain. Knights and Dragoons spend only that terrain cost and may be able to move again afterward; other units spend all remaining AP.
|
||||
|
||||
<b>Activation:</b> ends activation for units that spend all remaining AP. Knights and Dragoons can continue when they retain AP and another command is available.
|
||||
|
||||
<b>Available when:</b> a valid path reaches a target that can be attacked at the end of the move.
|
||||
|
||||
<b>Effect:</b> Move into an attack as one action. Charge attacks have a chance to stun the target unit.
|
||||
|
||||
<b>Selection:</b> choose the charging unit, path, and target.
|
||||
|
||||
### Move
|
||||
|
||||
<b>Unit requirements:</b> any unit.
|
||||
|
||||
<b>Action Point cost:</b> the terrain cost for each hex in the path, plus 1 extra AP for steps that start in enemy zone of control.
|
||||
|
||||
<b>Activation:</b> does not inherently end activation. The unit can continue if it has AP and a legal command after arriving. Follow-up icons on a destination preview eligible non-move actions there.
|
||||
|
||||
<b>Available when:</b> one or more reachable destination hexes are valid for the acting unit.
|
||||
|
||||
<b>Effect:</b> Move the unit along the chosen path to a reachable hex, paying terrain costs as it goes. Hidden units may reveal themselves if they move into exposed terrain or near an enemy Ranger.
|
||||
|
||||
<b>Selection:</b> choose the unit and destination path.
|
||||
|
||||
### Lightning Bolt
|
||||
|
||||
<b>Unit requirements:</b> [Mage](/heroes/#mage) attached to Light Infantry, Light Cavalry, or Longbowmen. Heavy units and Undead cannot cast.
|
||||
|
||||
<b>Action Point cost:</b> 5 AP; consumes all remaining AP.
|
||||
|
||||
<b>Available when:</b> a known enemy is within 3 hexes of the caster.
|
||||
|
||||
<b>Effect:</b> Cast a lightning attack at the target. The descriptor may include odds or roll information when the client can display expected results.
|
||||
|
||||
<b>Selection:</b> choose the casting unit and target.
|
||||
|
||||
### Meteor
|
||||
|
||||
<b>Unit requirements:</b> [Mage](/heroes/#mage) attached to Light Infantry, Light Cavalry, or Longbowmen, with at least 30 vigor. Heavy units and Undead cannot cast.
|
||||
|
||||
<b>Action Point cost:</b> start costs 5 AP and consumes all remaining AP; target and cancel cost 0 AP; the automatic cast step is configured as 1 AP but resolves by setting the caster to 0 AP.
|
||||
|
||||
<b>Available when:</b> the caster can begin the spell, choose or change a target within 3 hexes, cast the pending meteor, or cancel the pending meteor.
|
||||
|
||||
<b>Effect:</b> Begin a delayed meteor spell, then choose a target within 3 hexes on the following round. When the meteor resolves, a unit on the target hex takes a fire attack based on the Mage's Wisdom. Units on the six adjacent hexes take 40% as much. The impact damages castles, bridges, ice, and snow; may start fires on the target and adjacent hexes; and removes fortification from units it hits. Destroying supporting ice or a bridge can also drop a unit into the water. The Mage spends 20 vigor, gains 5 Wisdom XP, and is stunned for 2 rounds after the strike. You can cancel while the meteor is still pending.
|
||||
|
||||
<b>Selection:</b> choose the caster, spell target, or cancel option depending on the current meteor step.
|
||||
|
||||
### Start Fire
|
||||
|
||||
<b>Unit requirements:</b> a unit with an attached [Mage](/heroes/#mage), or an attached hero with at least 70 AGI and 70 WIS. Mages improve the odds.
|
||||
|
||||
<b>Action Point cost:</b> 5 AP; consumes all remaining AP.
|
||||
|
||||
<b>Available when:</b> a target hex can be set on fire.
|
||||
|
||||
<b>Effect:</b> Attempt to start a fire on the target hex.
|
||||
|
||||
<b>Selection:</b> choose the acting unit and target hex.
|
||||
|
||||
### Extinguish Fire
|
||||
|
||||
<b>Unit requirements:</b> any unit with an attached hero.
|
||||
|
||||
<b>Action Point cost:</b> 5 AP; consumes all remaining AP.
|
||||
|
||||
<b>Available when:</b> a target hex contains fire that can be reduced or removed.
|
||||
|
||||
<b>Effect:</b> Remove or reduce fire on the target hex.
|
||||
|
||||
<b>Selection:</b> choose the acting unit and burning target hex.
|
||||
|
||||
### Freeze Water
|
||||
|
||||
<b>Unit requirements:</b> [Mage](/heroes/#mage) attached to Light Infantry, Light Cavalry, or Longbowmen. Heavy units and Undead cannot cast.
|
||||
|
||||
<b>Action Point cost:</b> 5 AP; consumes all remaining AP.
|
||||
|
||||
<b>Available when:</b> a valid water hex can be frozen.
|
||||
|
||||
<b>Effect:</b> Turn water into traversable frozen terrain where the command is valid.
|
||||
|
||||
<b>Selection:</b> choose the caster and water hex.
|
||||
|
||||
### Build Bridge
|
||||
|
||||
<b>Unit requirements:</b> [Engineer](/heroes/#engineer).
|
||||
|
||||
<b>Action Point cost:</b> 5 AP; consumes all remaining AP.
|
||||
|
||||
<b>Available when:</b> the engineer has a valid water or gap target where a bridge can be built.
|
||||
|
||||
<b>Effect:</b> Create a bridge across the target.
|
||||
|
||||
<b>Selection:</b> choose the engineer unit and bridge target.
|
||||
|
||||
### Blow Bridge
|
||||
|
||||
<b>Unit requirements:</b> [Engineer](/heroes/#engineer) standing on a bridge.
|
||||
|
||||
<b>Action Point cost:</b> 5 AP; consumes all remaining AP.
|
||||
|
||||
<b>Available when:</b> the bridge can be destroyed.
|
||||
|
||||
<b>Effect:</b> Destroy the existing bridge target.
|
||||
|
||||
<b>Selection:</b> choose the engineer unit and bridge.
|
||||
|
||||
### Placement
|
||||
|
||||
<b>Unit requirements:</b> a unit available for setup placement. Hidden placement requires a defending [Ranger](/heroes/#ranger) attached to a stealth-capable unit.
|
||||
|
||||
<b>Action Point cost:</b> no AP cost during setup.
|
||||
|
||||
<b>Available when:</b> the battle is in setup and a valid placement hex is available.
|
||||
|
||||
<b>Effect:</b> Put a reserve unit onto the battlefield. Rangers on stealth-capable units can be placed hidden during defensive setup.
|
||||
|
||||
<b>Selection:</b> choose the unit and setup hex.
|
||||
|
||||
### Scout
|
||||
|
||||
<b>Unit requirements:</b> [Ranger](/heroes/#ranger) attached to a stealth-capable unit.
|
||||
|
||||
<b>Action Point cost:</b> 5 AP; consumes all remaining AP.
|
||||
|
||||
<b>Available when:</b> the ranger can scout a hex within 4 hexes.
|
||||
|
||||
<b>Effect:</b> Reveal information about the target hex and nearby terrain, including possible hidden-unit information.
|
||||
|
||||
<b>Selection:</b> choose the scouting unit and target.
|
||||
|
||||
### Raise Dead
|
||||
|
||||
<b>Unit requirements:</b> [Necromancer](/heroes/#necromancer) attached to Light Infantry, Light Cavalry, or Longbowmen, and the hero must not already control a unit. Heavy units and Undead cannot cast.
|
||||
|
||||
<b>Action Point cost:</b> 5 AP; consumes all remaining AP.
|
||||
|
||||
<b>Available when:</b> there is an empty, undead-traversable target hex within 2 hexes.
|
||||
|
||||
<b>Effect:</b> Make the displayed success roll. On success, create a battalion of 100 Undead on the target hex, controlled by the Necromancer. The new battalion starts with 50 morale, 0 training, and 0 armament. On failure, no unit is created. Either result spends the action; the Necromancer gains 1 Wisdom XP and 2 Charisma XP.
|
||||
|
||||
<b>Selection:</b> choose the necromancer unit and target.
|
||||
|
||||
### Dismiss Unit
|
||||
|
||||
<b>Unit requirements:</b> a hero who currently controls a summoned unit, such as Undead raised by a Necromancer.
|
||||
|
||||
<b>Action Point cost:</b> requires the controlling unit to have more than 0 AP; execution sets both the controlling unit and dismissed unit to 0 AP.
|
||||
|
||||
<b>Available when:</b> the controlling hero has more than 0 AP and still has a controlled unit.
|
||||
|
||||
<b>Effect:</b> Make the displayed success roll, based on the controlling hero's Charisma and Wisdom. Success disbands the controlled unit. Failure still ends the control relationship, but the unit remains on the battlefield uncontrolled and targets its former controller.
|
||||
|
||||
<b>Selection:</b> choose Dismiss Unit on the controlling hero; the controlled unit is the target.
|
||||
|
||||
### Holy Wave
|
||||
|
||||
<b>Unit requirements:</b> [Paladin](/heroes/#paladin).
|
||||
|
||||
<b>Action Point cost:</b> 5 AP; consumes all remaining AP.
|
||||
|
||||
<b>Vigor cost:</b> 10 from the Paladin under normal circumstances.
|
||||
|
||||
<b>Available when:</b> the Paladin unit has enough AP and meets the shared morale and hero-vigor requirements.
|
||||
|
||||
<b>Effect:</b> Emit a wave centered on the Paladin. The Paladin and every adjacent non-Undead unit controlled by the same player gain 25 morale, capped at 10 above base morale and at 100 overall; attached heroes regain 5 vigor up to starting vigor; and stun is removed. Every adjacent Undead unit, regardless of allegiance, loses 70% of its troops, becomes visible, and targets the Paladin. The Paladin gains 5 Charisma XP. The effect does not make a success roll.
|
||||
|
||||
<b>Selection:</b> choose the Paladin unit. Holy Wave does not require a target.
|
||||
|
||||
### Stop and Rest
|
||||
|
||||
<b>Unit requirements:</b> a unit with an available charge.
|
||||
|
||||
<b>Action Point cost:</b> Stop costs 0 AP; Rest consumes all remaining AP.
|
||||
|
||||
<b>Available when:</b> Stop is offered outside an enemy zone of control; Rest is offered inside an enemy zone of control.
|
||||
|
||||
<b>Effect:</b> Stop declines the available charge without spending the unit's remaining AP. Rest declines the charge, ends the activation, and restores an attached hero's vigor by half of the AP given up. For example, resting with 4 AP restores 2 vigor.
|
||||
|
||||
<b>Selection:</b> choose stop or rest for the acting unit.
|
||||
|
||||
### Challenge Duel
|
||||
|
||||
<b>Unit requirements:</b> [Champion](/heroes/#champion) challenging an adjacent known enemy unit with an attached hero. A challenge cannot enter a castle.
|
||||
|
||||
<b>Action Point cost:</b> 5 AP; consumes all remaining AP.
|
||||
|
||||
<b>Available when:</b> the target hero has at least 30 vigor, or the Champion does not know enough about the target to know otherwise.
|
||||
|
||||
<b>Effect:</b> The target's chance to accept depends on Bravery and its assessment of the two battalions and heroes. A target that declines normally loses 15 morale; a target known to have less than 30 vigor declines without that penalty. If accepted, the heroes exchange attacks until one falls or 30 exchanges pass. A winner's battalion gains 15 morale and the winning hero gains 20 Charisma XP. Participating heroes can also gain Strength and Agility XP.
|
||||
|
||||
<b>Selection:</b> choose the champion unit and duel target.
|
||||
|
||||
### Fear
|
||||
|
||||
<b>Unit requirements:</b> [Necromancer](/heroes/#necromancer) attached to Light Infantry, Light Cavalry, or Longbowmen. Heavy units and Undead cannot cast.
|
||||
|
||||
<b>Action Point cost:</b> 5 AP; consumes all remaining AP.
|
||||
|
||||
<b>Available when:</b> a known enemy that uses morale is within 3 hexes.
|
||||
|
||||
<b>Effect:</b> Make the displayed success roll. The Necromancer's Charisma and Wisdom improve the chance, while a target hero's Charisma resists it. Success reduces the target battalion's morale by 25 and stuns it for 1 round. Failure instead increases its morale by 10. The Necromancer gains 2 Charisma XP either way.
|
||||
|
||||
<b>Selection:</b> choose the necromancer unit and target.
|
||||
|
||||
### Repair
|
||||
|
||||
<b>Unit requirements:</b> [Engineer](/heroes/#engineer).
|
||||
|
||||
<b>Action Point cost:</b> 5 AP; consumes all remaining AP.
|
||||
|
||||
<b>Available when:</b> a damaged friendly or unoccupied bridge or castle is on the Engineer's hex or an adjacent hex. The Engineer cannot repair from water or repair a structure occupied by a known enemy.
|
||||
|
||||
<b>Effect:</b> Restore 25 integrity to the bridge or castle, up to its maximum of 100. Repair does not restore troops or hero vigor.
|
||||
|
||||
<b>Selection:</b> choose the Engineer unit and the bridge or castle to repair.
|
||||
|
||||
### Reduce
|
||||
|
||||
<b>Unit requirements:</b> [Engineer](/heroes/#engineer) in a castle or fortified position.
|
||||
|
||||
<b>Action Point cost:</b> 5 AP; consumes all remaining AP.
|
||||
|
||||
<b>Available when:</b> a known enemy unit or a castle with remaining integrity is within 3 hexes and the line is not blocked by mountains. Friendly-occupied hexes and the Engineer's own hex cannot be targeted.
|
||||
|
||||
<b>Effect:</b> Bombard the target. The damage roll improves with the Engineer's Agility, Strength, and Wisdom, and gains a bonus when friendly forces have access to a forest. The result reduces castle or bridge integrity on the hex, damages any occupying enemy unit, and removes that unit's fortified status.
|
||||
|
||||
<b>Selection:</b> choose the engineer unit and target.
|
||||
|
||||
### Flee
|
||||
|
||||
<b>Unit requirements:</b> a unit with an attached hero that is allowed to flee.
|
||||
|
||||
<b>Action Point cost:</b> 5 AP; consumes all remaining AP.
|
||||
|
||||
<b>Available when:</b> the hero can leave the battle and has enough AP.
|
||||
|
||||
<b>Effect:</b> Attempt to withdraw from the province entirely. Make the displayed success roll: nearby visible friendly units improve the chance to escape, while nearby visible enemies and impassable terrain reduce it. Units 2 hexes away have half as much effect as adjacent units. VIP heroes escape automatically. On success, the unit leaves both the battle and the province and will escape even if the province is later captured. On failure, the unit is captured during the attempt.
|
||||
|
||||
<b>Selection:</b> choose the fleeing unit.
|
||||
|
||||
### Retreat
|
||||
|
||||
<b>Unit requirements:</b> a defending unit with an attached hero.
|
||||
|
||||
<b>Action Point cost:</b> 5 AP; consumes all remaining AP.
|
||||
|
||||
<b>Available when:</b> the defending hero has enough AP. Attackers cannot use Retreat.
|
||||
|
||||
<b>Effect:</b> Withdraw the unit from the battle while remaining in the province. Retreat succeeds automatically and does not make the immediate capture roll used by Flee, but the hero will still be captured if their side loses the battle and the province is captured.
|
||||
|
||||
<b>Selection:</b> choose the retreating unit.
|
||||
|
||||
### Control
|
||||
|
||||
<b>Unit requirements:</b> a hero who already has a controlled summoned unit, such as Undead raised by a Necromancer.
|
||||
|
||||
<b>Action Point cost:</b> requires more than 0 AP; execution sets the acting unit to 0 AP.
|
||||
|
||||
<b>Available when:</b> the controlling hero has more than 0 AP and still has a controlled unit.
|
||||
|
||||
<b>Effect:</b> Spend the controlling hero's remaining AP to command the linked unit this round. Control does not create a new controlled unit; Raise Dead establishes that relationship.
|
||||
|
||||
<b>Selection:</b> choose Control on the controlling hero; the linked unit is the target.
|
||||
|
||||
### Reinforce
|
||||
|
||||
<b>Unit requirements:</b> an on-map unit with enough AP and a friendly reserve unit available to enter the battle.
|
||||
|
||||
<b>Action Point cost:</b> 5 AP; consumes all remaining AP.
|
||||
|
||||
<b>Available when:</b> fewer than 10 friendly on-map units have attached heroes, and the reserve unit has an empty, non-burning hex in its designated starting area.
|
||||
|
||||
<b>Effect:</b> Spend the acting unit's activation and move the selected reserve unit onto the chosen entry hex as a normal active unit.
|
||||
|
||||
<b>Selection:</b> choose the reinforcement option and entry placement.
|
||||
|
||||
### Brave Water
|
||||
|
||||
<b>Unit requirements:</b> a unit with an attached hero whose battalion type permits Brave Water. The hero needs at least 10 vigor. [Ranger](/heroes/#ranger) is not required, but Rangers receive a 20-point odds bonus.
|
||||
|
||||
<b>Action Point cost:</b> 8 AP; consumes all remaining AP.
|
||||
|
||||
<b>Available when:</b> the unit is on land and can reach a believed-empty, traversable land hex across water.
|
||||
|
||||
<b>Effect:</b> Make the displayed crossing roll. Agility and Wisdom improve the chance; armament and bad weather reduce it. Success moves the unit across the water, unless a hidden occupant triggers an ambush on arrival. Failure leaves the unit in place. The attempt always stuns the unit for 1 round and can cost up to 20% of its troops, armament, and hero vigor; safer crossings reduce the possible losses.
|
||||
|
||||
<b>Selection:</b> choose the unit and water movement target.
|
||||
|
||||
### Release Unit
|
||||
|
||||
<b>Unit requirements:</b> a hero who currently controls a summoned unit, such as Undead raised by a Necromancer.
|
||||
|
||||
<b>Action Point cost:</b> requires more than 0 AP; execution sets both the controlling unit and released unit to 0 AP.
|
||||
|
||||
<b>Available when:</b> the controlling hero has more than 0 AP and still has a controlled unit.
|
||||
|
||||
<b>Effect:</b> End the control relationship without a roll. The released unit becomes uncontrolled, targets its former controller, and remains on the battlefield. Both units end their activations.
|
||||
|
||||
<b>Selection:</b> choose Release Unit on the controlling hero; the controlled unit is the target.
|
||||
|
||||
### End Turn and Setup
|
||||
|
||||
<b>Unit requirements:</b> None.
|
||||
|
||||
<b>Action Point cost:</b> no AP cost to post the command; End Turn then sets that player's remaining unit AP to 0.
|
||||
|
||||
<b>Available when:</b> the active player can end the tactical turn or finish setup.
|
||||
|
||||
<b>Effect:</b> `END_TURN_COMMAND` ends the active tactical turn. `END_PLAYER_SETUP_COMMAND` ends that player's setup phase.
|
||||
|
||||
<b>Selection:</b> choose the end-turn or end-setup command.
|
||||
|
||||
### Hide
|
||||
|
||||
<b>Unit requirements:</b> [Ranger](/heroes/#ranger) attached to a stealth-capable unit.
|
||||
|
||||
<b>Action Point cost:</b> 5 AP; consumes all remaining AP.
|
||||
|
||||
<b>Available when:</b> the unit can enter or remain in hidden status.
|
||||
|
||||
<b>Effect:</b> Hide the unit.
|
||||
|
||||
<b>Selection:</b> choose the ranger unit.
|
||||
|
||||
### Fortify
|
||||
|
||||
<b>Unit requirements:</b> [Engineer](/heroes/#engineer).
|
||||
|
||||
<b>Action Point cost:</b> 5 AP; consumes all remaining AP.
|
||||
|
||||
<b>Available when:</b> the Engineer is not already fortified, is standing on fortifiable terrain, and has enough AP.
|
||||
|
||||
<b>Effect:</b> Mark the unit as fortified, reducing damage taken by 10%. Moving removes fortification; being hit by Reduce or Meteor also removes it, as does taking fire damage at the end of the unit's turn.
|
||||
|
||||
<b>Selection:</b> choose the engineer unit.
|
||||
|
||||
### Become Outlaw
|
||||
|
||||
<b>Unit requirements:</b> a unit with an attached hero that is not allowed to flee normally.
|
||||
|
||||
<b>Action Point cost:</b> 5 AP through the flee/outlaw action.
|
||||
|
||||
<b>Available when:</b> the hero has enough AP, cannot flee, and the battle permits the outlaw outcome.
|
||||
|
||||
<b>Effect:</b> Use the same displayed escape roll as Flee. Success removes the unit from battle and records the hero as an outlaw. Failure removes the unit as captured.
|
||||
|
||||
<b>Selection:</b> choose the outlaw command when offered.
|
||||
|
||||
### Evacuate Prisoners
|
||||
|
||||
<b>Unit requirements:</b> [Warden](/heroes/#warden) on a unit that can flee.
|
||||
|
||||
<b>Action Point cost:</b> 5 AP; consumes all remaining AP.
|
||||
|
||||
<b>Available when:</b> at least one hostile hero has already been captured, and the Warden is allowed to flee and has enough AP.
|
||||
|
||||
<b>Effect:</b> Remove the Warden's unit from battle as evacuated and secure every captured hostile hero for the Warden's player. The evacuation succeeds automatically.
|
||||
|
||||
<b>Selection:</b> choose the warden unit and evacuation command.
|
||||
@@ -0,0 +1,258 @@
|
||||
.sl-markdown-content:has(.command-reference-marker) {
|
||||
--command-border: color-mix(in srgb, var(--sl-color-gray-5), transparent 18%);
|
||||
--command-surface: color-mix(in srgb, var(--sl-color-bg), var(--sl-color-gray-6) 22%);
|
||||
--command-surface-strong: color-mix(in srgb, var(--sl-color-bg), var(--sl-color-accent-low) 28%);
|
||||
--command-muted: var(--sl-color-gray-2);
|
||||
--command-label: var(--sl-color-accent-high);
|
||||
}
|
||||
|
||||
.command-reference-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.command-reference-five-row-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sl-markdown-content:has(.command-reference-marker) > p:has(.command-reference-marker) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sl-markdown-content:has(.command-reference-five-row-marker) > p:has(.command-reference-five-row-marker) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sl-markdown-content:has(.command-reference-marker) > p {
|
||||
max-width: 68rem;
|
||||
}
|
||||
|
||||
.sl-markdown-content:has(.command-reference-marker) .sl-heading-wrapper.level-h2 {
|
||||
margin-top: 3rem;
|
||||
padding: 1rem 1.125rem;
|
||||
border: 1px solid var(--command-border);
|
||||
border-radius: 0.5rem;
|
||||
background:
|
||||
linear-gradient(90deg, var(--sl-color-accent-low), transparent 72%),
|
||||
var(--command-surface);
|
||||
}
|
||||
|
||||
.sl-markdown-content:has(.command-reference-marker) .sl-heading-wrapper.level-h2 h2 {
|
||||
font-size: clamp(1.35rem, 2.4vw, 1.7rem);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.sl-markdown-content:has(.command-reference-marker) .sl-heading-wrapper.level-h2 + p {
|
||||
margin-top: 0.75rem;
|
||||
color: var(--command-muted);
|
||||
}
|
||||
|
||||
.sl-markdown-content:has(.command-reference-marker) .sl-heading-wrapper.level-h3 {
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0;
|
||||
padding: 1rem 1.125rem 0.875rem;
|
||||
border: 1px solid var(--command-border);
|
||||
border-bottom: 0;
|
||||
border-radius: 0.5rem 0.5rem 0 0;
|
||||
background: var(--command-surface-strong);
|
||||
font-size: 1.18rem;
|
||||
}
|
||||
|
||||
.sl-markdown-content:has(.command-reference-marker) .sl-heading-wrapper.level-h3 h3 {
|
||||
font-size: 1.15rem;
|
||||
}
|
||||
|
||||
.sl-markdown-content:has(.command-reference-marker) .sl-heading-wrapper.level-h3 + p,
|
||||
.sl-markdown-content:has(.command-reference-marker) .sl-heading-wrapper.level-h3 + p + p,
|
||||
.sl-markdown-content:has(.command-reference-marker) .sl-heading-wrapper.level-h3 + p + p + p,
|
||||
.sl-markdown-content:has(.command-reference-marker) .sl-heading-wrapper.level-h3 + p + p + p + p {
|
||||
margin: 0;
|
||||
padding: 0.75rem 1.125rem;
|
||||
border-right: 1px solid var(--command-border);
|
||||
border-left: 1px solid var(--command-border);
|
||||
background: var(--sl-color-bg);
|
||||
position: relative;
|
||||
padding-left: 13.625rem;
|
||||
}
|
||||
|
||||
.sl-markdown-content:has(.command-reference-five-row-marker)
|
||||
.sl-heading-wrapper.level-h3
|
||||
+ p
|
||||
+ p
|
||||
+ p
|
||||
+ p
|
||||
+ p {
|
||||
margin: 0;
|
||||
padding: 0.75rem 1.125rem;
|
||||
border-right: 1px solid var(--command-border);
|
||||
border-left: 1px solid var(--command-border);
|
||||
background: var(--sl-color-bg);
|
||||
position: relative;
|
||||
padding-left: 13.625rem;
|
||||
}
|
||||
|
||||
.sl-markdown-content:has(.command-reference-marker) .sl-heading-wrapper.level-h3 + p {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.sl-markdown-content:has(.command-reference-marker) .sl-heading-wrapper.level-h3 + p + p + p + p {
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid var(--command-border);
|
||||
border-radius: 0 0 0.5rem 0.5rem;
|
||||
box-shadow: 0 0.7rem 1.6rem color-mix(in srgb, black, transparent 92%);
|
||||
}
|
||||
|
||||
.sl-markdown-content:has(.command-reference-five-row-marker)
|
||||
.sl-heading-wrapper.level-h3
|
||||
+ p
|
||||
+ p
|
||||
+ p
|
||||
+ p {
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 0;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.sl-markdown-content:has(.command-reference-five-row-marker)
|
||||
.sl-heading-wrapper.level-h3
|
||||
+ p
|
||||
+ p
|
||||
+ p
|
||||
+ p
|
||||
+ p {
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid var(--command-border);
|
||||
border-radius: 0 0 0.5rem 0.5rem;
|
||||
box-shadow: 0 0.7rem 1.6rem color-mix(in srgb, black, transparent 92%);
|
||||
}
|
||||
|
||||
.sl-markdown-content:has(.command-reference-marker) p > b:first-child {
|
||||
color: var(--command-label);
|
||||
font-size: 0.82rem;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sl-markdown-content:has(.command-reference-marker) .sl-heading-wrapper.level-h3 + p > b:first-child,
|
||||
.sl-markdown-content:has(.command-reference-marker) .sl-heading-wrapper.level-h3 + p + p > b:first-child,
|
||||
.sl-markdown-content:has(.command-reference-marker) .sl-heading-wrapper.level-h3 + p + p + p > b:first-child,
|
||||
.sl-markdown-content:has(.command-reference-marker) .sl-heading-wrapper.level-h3 + p + p + p + p > b:first-child,
|
||||
.sl-markdown-content:has(.command-reference-five-row-marker)
|
||||
.sl-heading-wrapper.level-h3
|
||||
+ p
|
||||
+ p
|
||||
+ p
|
||||
+ p
|
||||
+ p
|
||||
> b:first-child {
|
||||
position: absolute;
|
||||
left: 1.125rem;
|
||||
width: 11.5rem;
|
||||
}
|
||||
|
||||
.sl-markdown-content:has(.command-reference-marker) .sl-heading-wrapper.level-h4 {
|
||||
margin-top: 1rem;
|
||||
padding: 0.65rem 1rem;
|
||||
border: 1px solid var(--command-border);
|
||||
border-radius: 0.45rem;
|
||||
background: color-mix(in srgb, var(--sl-color-gray-6), transparent 55%);
|
||||
color: var(--sl-color-white);
|
||||
font-size: 0.92rem;
|
||||
letter-spacing: 0.02em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.sl-markdown-content:has(.command-reference-marker) .sl-heading-wrapper.level-h5 {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0;
|
||||
padding: 0.75rem 1rem 0.25rem;
|
||||
border-top: 1px solid var(--command-border);
|
||||
color: var(--sl-color-accent-high);
|
||||
}
|
||||
|
||||
.sl-markdown-content:has(.command-reference-marker) .sl-heading-wrapper.level-h5 + p,
|
||||
.sl-markdown-content:has(.command-reference-marker) .sl-heading-wrapper.level-h5 + p + p {
|
||||
margin: 0;
|
||||
padding: 0.4rem 1rem;
|
||||
border-right: 1px solid var(--command-border);
|
||||
border-left: 1px solid var(--command-border);
|
||||
position: relative;
|
||||
padding-left: 7.5rem;
|
||||
}
|
||||
|
||||
.sl-markdown-content:has(.command-reference-marker) .sl-heading-wrapper.level-h5 + p > b:first-child,
|
||||
.sl-markdown-content:has(.command-reference-marker) .sl-heading-wrapper.level-h5 + p + p > b:first-child {
|
||||
position: absolute;
|
||||
left: 1rem;
|
||||
width: 5.5rem;
|
||||
}
|
||||
|
||||
.sl-markdown-content:has(.command-reference-marker) .sl-heading-wrapper.level-h5 + p + p {
|
||||
padding-bottom: 0.8rem;
|
||||
border-bottom: 1px solid var(--command-border);
|
||||
border-radius: 0 0 0.45rem 0.45rem;
|
||||
}
|
||||
|
||||
@media (max-width: 50rem) {
|
||||
.sl-markdown-content:has(.command-reference-marker) .sl-heading-wrapper.level-h2,
|
||||
.sl-markdown-content:has(.command-reference-marker) .sl-heading-wrapper.level-h3 {
|
||||
padding-right: 0.875rem;
|
||||
padding-left: 0.875rem;
|
||||
}
|
||||
|
||||
.sl-markdown-content:has(.command-reference-marker) .sl-heading-wrapper.level-h3 + p,
|
||||
.sl-markdown-content:has(.command-reference-marker) .sl-heading-wrapper.level-h3 + p + p,
|
||||
.sl-markdown-content:has(.command-reference-marker) .sl-heading-wrapper.level-h3 + p + p + p,
|
||||
.sl-markdown-content:has(.command-reference-marker) .sl-heading-wrapper.level-h3 + p + p + p + p,
|
||||
.sl-markdown-content:has(.command-reference-five-row-marker)
|
||||
.sl-heading-wrapper.level-h3
|
||||
+ p
|
||||
+ p
|
||||
+ p
|
||||
+ p
|
||||
+ p {
|
||||
padding-right: 0.875rem;
|
||||
padding-left: 0.875rem;
|
||||
}
|
||||
|
||||
.sl-markdown-content:has(.command-reference-marker) p > b:first-child {
|
||||
display: block;
|
||||
margin-bottom: 0.2rem;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.sl-markdown-content:has(.command-reference-marker) .sl-heading-wrapper.level-h3 + p,
|
||||
.sl-markdown-content:has(.command-reference-marker) .sl-heading-wrapper.level-h3 + p + p,
|
||||
.sl-markdown-content:has(.command-reference-marker) .sl-heading-wrapper.level-h3 + p + p + p,
|
||||
.sl-markdown-content:has(.command-reference-marker) .sl-heading-wrapper.level-h3 + p + p + p + p,
|
||||
.sl-markdown-content:has(.command-reference-five-row-marker)
|
||||
.sl-heading-wrapper.level-h3
|
||||
+ p
|
||||
+ p
|
||||
+ p
|
||||
+ p
|
||||
+ p,
|
||||
.sl-markdown-content:has(.command-reference-marker) .sl-heading-wrapper.level-h5 + p,
|
||||
.sl-markdown-content:has(.command-reference-marker) .sl-heading-wrapper.level-h5 + p + p {
|
||||
padding-left: 0.875rem;
|
||||
}
|
||||
|
||||
.sl-markdown-content:has(.command-reference-marker) .sl-heading-wrapper.level-h3 + p > b:first-child,
|
||||
.sl-markdown-content:has(.command-reference-marker) .sl-heading-wrapper.level-h3 + p + p > b:first-child,
|
||||
.sl-markdown-content:has(.command-reference-marker) .sl-heading-wrapper.level-h3 + p + p + p > b:first-child,
|
||||
.sl-markdown-content:has(.command-reference-marker) .sl-heading-wrapper.level-h3 + p + p + p + p > b:first-child,
|
||||
.sl-markdown-content:has(.command-reference-five-row-marker)
|
||||
.sl-heading-wrapper.level-h3
|
||||
+ p
|
||||
+ p
|
||||
+ p
|
||||
+ p
|
||||
+ p
|
||||
> b:first-child,
|
||||
.sl-markdown-content:has(.command-reference-marker) .sl-heading-wrapper.level-h5 + p > b:first-child,
|
||||
.sl-markdown-content:has(.command-reference-marker) .sl-heading-wrapper.level-h5 + p + p > b:first-child {
|
||||
position: static;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
import "../node_modules/astro/astro.js";
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict"
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
# Gitea offsite backups
|
||||
|
||||
Eagle0's Gitea mirror keeps Git and Git LFS data on `zog` for fast local
|
||||
access. The `Gitea Offsite Backup` GitHub Actions workflow copies the
|
||||
recoverable state to an encrypted Restic repository in the private
|
||||
`eagle0-gitea-backups` DigitalOcean Space.
|
||||
|
||||
## Backup contents
|
||||
|
||||
Each snapshot contains:
|
||||
|
||||
- a transactionally consistent SQLite backup of the Gitea database;
|
||||
- Gitea configuration and persistent application data;
|
||||
- every locally stored Git LFS object;
|
||||
- a fresh mirror clone of `admin/eagle0`;
|
||||
- the Gitea version and captured `main` commit in `manifest.txt`.
|
||||
|
||||
Regenerable indexes, sessions, queues, repository archives, logs, and the
|
||||
OAuth2 JWT signing key are excluded. Gitea creates a new JWT signing key when
|
||||
none exists, which invalidates pre-restoration OAuth2 tokens. Container-managed
|
||||
SSH host keys are not readable by the backup account and must also be
|
||||
regenerated after a complete NAS loss.
|
||||
|
||||
## Schedule and retention
|
||||
|
||||
The backup runs daily at 10:23 UTC on a `halfdan` self-hosted runner. Each
|
||||
run validates the SQLite snapshot, runs `git fsck`, uploads an encrypted
|
||||
snapshot, and reads a rotating 5% sample of the Restic repository.
|
||||
|
||||
Weekly maintenance runs Sunday at 11:53 UTC. It retains 14 daily, 8 weekly,
|
||||
12 monthly, and 3 yearly snapshots, prunes unreferenced Restic packs, and
|
||||
reads a rotating 10% sample.
|
||||
|
||||
Use the workflow's `full_restore_verify` option after storage or credential
|
||||
changes. It downloads the latest snapshot, validates the restored SQLite
|
||||
database, and runs `git fsck` against the restored repository.
|
||||
|
||||
## Credentials
|
||||
|
||||
GitHub Actions stores:
|
||||
|
||||
- `GITEA_DO_SPACES_ACCESS_KEY`
|
||||
- `GITEA_DO_SPACES_SECRET_KEY`
|
||||
- `DO_SPACES_ENDPOINT`
|
||||
- `GITEA_BACKUP_SSH_KEY`
|
||||
- `GITEA_RESTIC_PASSWORD`
|
||||
|
||||
`GITEA_BACKUP_SSH_KEY` is a dedicated key accepted by the NAS account used for
|
||||
backup reads. It is not a copy of a developer's personal SSH key.
|
||||
|
||||
The Restic recovery password also has an offline recovery copy in the
|
||||
workstation's macOS Keychain under service `eagle0-gitea-restic`. Never put
|
||||
that password in the repository, workflow logs, or a support ticket.
|
||||
|
||||
## Restore
|
||||
|
||||
Install `restic`, then set the DigitalOcean credentials, Restic repository,
|
||||
and Restic password in the shell performing the restore. Restore into an
|
||||
empty directory:
|
||||
|
||||
```bash
|
||||
restic snapshots --host zog --tag gitea
|
||||
restic restore latest --host zog --tag gitea --target /path/to/restore
|
||||
```
|
||||
|
||||
The snapshot preserves the absolute staging path below the restore target.
|
||||
Locate `manifest.txt`; its containing directory is the restored backup root.
|
||||
|
||||
Before changing Gitea:
|
||||
|
||||
```bash
|
||||
sqlite3 /path/to/restored/root/gitea/gitea.db "PRAGMA integrity_check;"
|
||||
git --git-dir=/path/to/restored/root/repositories/admin/eagle0.git fsck --full
|
||||
```
|
||||
|
||||
To recover a complete installation:
|
||||
|
||||
1. Recreate the same Gitea version shown in `manifest.txt`.
|
||||
2. Stop Gitea.
|
||||
3. Restore `config`, `gitea`, `lfs`, and the bare repository into the
|
||||
matching paths under `/volume2/docker/gitea`.
|
||||
4. Preserve the restored file ownership expected by the container.
|
||||
5. Allow Gitea to regenerate indexes, sessions, queues, archives, and SSH
|
||||
host keys.
|
||||
6. Start Gitea and verify a historical LFS checkout before enabling writes.
|
||||
|
||||
Do not overwrite a running installation during a restore.
|
||||
@@ -2,18 +2,18 @@ module github.com/nolen777/eagle0
|
||||
|
||||
go 1.25.0
|
||||
|
||||
toolchain go1.26.4
|
||||
toolchain go1.26.5
|
||||
|
||||
require (
|
||||
github.com/aws/aws-sdk-go-v2 v1.42.0
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.25
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.24
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.103.3
|
||||
github.com/aws/aws-sdk-go-v2 v1.43.0
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.31
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.30
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.106.0
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6
|
||||
golang.org/x/sys v0.46.0
|
||||
google.golang.org/grpc v1.81.1
|
||||
golang.org/x/sys v0.47.0
|
||||
google.golang.org/grpc v1.82.1
|
||||
google.golang.org/protobuf v1.36.11
|
||||
)
|
||||
|
||||
|
||||
@@ -4,34 +4,66 @@ cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdB
|
||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 h1:rIkQfkCOVKc1OiRCNcSDD8ml5RJlZbH/Xsq7lbpynwc=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0/go.mod h1:RD2SsorTmYhF6HkTmDw7KmPYQk8OBYwTkuasChwv7R4=
|
||||
github.com/aws/aws-sdk-go-v2 v1.32.8 h1:cZV+NUS/eGxKXMtmyhtYPJ7Z4YLoI/V8bkTdRZfYhGo=
|
||||
github.com/aws/aws-sdk-go-v2 v1.32.8/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U=
|
||||
github.com/aws/aws-sdk-go-v2 v1.42.0 h1:XvXMJTkFQtpBKIWZnmr9ZEOc2InWM2yldjXEJ/bymhA=
|
||||
github.com/aws/aws-sdk-go-v2 v1.42.0/go.mod h1:27+ACypSLljLAEKsCYOmrjKh83vuTRkuAe9Uv/3A4bg=
|
||||
github.com/aws/aws-sdk-go-v2 v1.42.1 h1:9eOTgu1z/dVtYpNZ3/8/XbbaX0x/BqE3HUzAzs6K0ek=
|
||||
github.com/aws/aws-sdk-go-v2 v1.42.1/go.mod h1:5pKeft2eJj+gElQ38Jqg4ibCqh+/AK33/0X3hip7IjM=
|
||||
github.com/aws/aws-sdk-go-v2 v1.43.0 h1:fharf/WhbRAVZ1du0QL7roNFxZ6T/sWr+4Ni617bwSI=
|
||||
github.com/aws/aws-sdk-go-v2 v1.43.0/go.mod h1:5pKeft2eJj+gElQ38Jqg4ibCqh+/AK33/0X3hip7IjM=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 h1:lL7IfaFzngfx0ZwUGOZdsFFnQ5uLvR0hWqqhyE7Q9M8=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7/go.mod h1:QraP0UcVlQJsmHfioCrveWOC1nbiWUl3ej08h4mXWoc=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13 h1:p1BBrg/Hhp6uK7zpejeI8QFXHJeC/mynzi04Sl03k9g=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13/go.mod h1:8cIfkE9MDhkRZGpQ22aV6/lkYeYSozpz16Smrs5x4Ls=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.14 h1:3IZY0XAJquT3aHzbkHfPzy4ACPcEjVG0x87KOwtpqGY=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.14/go.mod h1:zwM6veDkhGgQFqkBy+uT28AAYpLu+uFMlPl+rCg/73E=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.28.10 h1:fKODZHfqQu06pCzR69KJ3GuttraRJkhlC8g80RZ0Dfg=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.28.10/go.mod h1:PvdxRYZ5Um9QMq9PQ0zHHNdtKK+he2NHtFCUFMXWXeg=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.25 h1:ACCejvStYoilgwrfegSt5ZntCbPrk52qfwyNcnl3omM=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.25/go.mod h1:LJyU8sDRbXUxFn8xMJIGP+v9QYYwveNLI8a/giAOiAs=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.29 h1:BcMHHnpiWKogf+gGfpj3K1w+Sktz29XDo/cPSAPO3FU=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.29/go.mod h1:+Kbhn8Es4kPUph3F/0W7avykytc+Jh2Ld9/msv9ljV4=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.30 h1:XwsEzpTJfQYJbFicz/QMLwAZdyeNVVoOEkbF7R3gPJk=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.30/go.mod h1:Ud32SuMc+/9BGxfpSVld7HrE2o05JwKmXY4M3jOQNZU=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.31 h1:n4nY9O3QKoHIkL85EX+V8RcMFtOhlpTFhGArg915PXk=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.31/go.mod h1:PN0NYDCCoOpGGsZ2+elDUidmHfQBPyYzN2GCgl8HEBs=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.51 h1:F/9Sm6Y6k4LqDesZDPJCLxQGXNNHd/ZtJiWd0lCZKRk=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.51/go.mod h1:TKbzCHm43AoPyA+iLGGcruXd4AFhF8tOmLex2R9jWNQ=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.24 h1:2hQqYCV9yqyePQ9o6dCrZc/zO8U3TwPr9mIKlZnPu/I=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.24/go.mod h1:IDwpACtwqHLISdzfwUUNq4P9DsB/h5BLg4FwJPNfqFY=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.28 h1:zTXJSsNcoO91/mTXsZoYf0AK8dvNPiA58/VtyGXR+wM=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.28/go.mod h1:Kd9E0JzDBW/q1xbsHFrev/GnbAf5J0Ng8xoyc7HZ91Q=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.29 h1:WHZGssHH887cO0ox07SIQZsFx3MKD4ps6w0xUEmnKYQ=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.29/go.mod h1:Mhl0xR6zjguiuj00XRx2wMx22sAltk7oya39sT7fdg8=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.30 h1:TTCvvzFU6gXa4iJecNG/0F/B0oYTiazoRECr2XyLHrY=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.30/go.mod h1:jKxAp2AEncnliinzpgOSZDFv6+VjvWhjw/AtbfsWT9U=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.23 h1:IBAoD/1d8A8/1aA8g4MBVtTRHhXRiNAgwdbo/xRM2DI=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.23/go.mod h1:vfENuCM7dofkgKpYzuzf1VT1UKkA/YL3qanfBn7HCaA=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29 h1:r6qZHbT+wxgWO/e9vYNUEtg7lv5+UN3pRqKhLXvnArg=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29/go.mod h1:QRnaRcTVGKPGRy8w78HMQtKUGRYcnMZAANATkeVA6Mo=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.30 h1:/hi1JADLEW9YYryEz1w4GQu0EtP23pP553Cf9KgsDV4=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.30/go.mod h1:/3AOgy4K17Dm4ucMZVC/MJkzy5kmfKUcINRHZyo0koQ=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.31 h1:kfVL5wAunCJycL6MOQ6aNh6PlAYEymflcjuKmrWUA0o=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.31/go.mod h1:nWfRNDAppujCQgOUd43lKT4yeLv9z3nJ3bw1G3BgQKo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.27 h1:jSJjSBzw8VDIbWv+mmvBSP8ezsztMYJGH+eKqi9AmNs=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.27/go.mod h1:/DAhLbFRgwhmvJdOfSm+WwikZrCuUJiA4WgJG0fTNSw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 h1:f3vKqSo13fhTYb+JEcXwXefZQE26I1FB5eTSniU67ko=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29/go.mod h1:MzoLFUArKGpGD+ukmPiTPG1X5x4o6M2kq4v2dr1FiEc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.30 h1:xM/Is9cKMHa8Jj8zkvWhvrFkZsXJV9E+BB4g0HW0duQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.30/go.mod h1:WueJeNDZvK1fMYEWJIkcivBfEzUkTpBhzlrUKKY8EuA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.31 h1:Z8F3hfCY33IGpJjFAnv0wvtv1FIKj1GHmRDEYqy64tw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.31/go.mod h1:aVyUoytEyOViR6jhq6jula0xkc5NfBE2hgeF6BvOrao=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.27 h1:l+X4K77Dui85pIj5foXDhPlnqcNRG2QUyvca300lXh8=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.27/go.mod h1:KvZXSFEXm6x84yE8qffKvT3x8J5clWnVFXphpohhzJ8=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 h1:RdwIf/CuUsvJX3RgJagbOyotl/cxoLY4xviKuE7p2GY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29/go.mod h1:71wt8W2EgswdZy9Mf9KNnzxZ3TiZlv4caKghPktDOkA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.30 h1:jn46zC9LdsVR/ZpMIJqMqb8hHv31BlLx3ulVqNspUOk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.30/go.mod h1:1hTMsAgbdS/AtUi4bw8+gUuh1pceo+eXRLfpSuSQj3M=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.31 h1:hyOxUyXdh3AyjE93gBgsfziJag9ACwcs+ZpDBLzi8mw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.31/go.mod h1:OERqI9k0draSLB8O8woxY3q25ZWTELRK4RRoLMuMZFo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw=
|
||||
@@ -40,46 +72,98 @@ github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.27 h1:AmB5QxnD+fBFrg9LcqzkgF/CaYv
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.27/go.mod h1:Sai7P3xTiyv9ZUYO3IFxMnmiIP759/67iQbU4kdmkyU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30 h1:VTGy885W5DKBxWRUJbym9hytNaYzsyaPkCHGRRMAOhU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30/go.mod h1:AS0HycUvJRFvTt613AYDOgO2jzw+00cVSMny8XB3yMY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.31 h1:3GUprIsfmGcC5SACIyB0e7E0BM1O1b3Erl5CePYIAeQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.31/go.mod h1:7PuV1yl5e2xnUbm+RqvVg5i2iBM8EyijZNoI9wsOoOc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.32 h1:0MrUL35H/Y4kdFfItoR5jCgtDQ4Z/8LudAoIHRfA4hE=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.32/go.mod h1:2tNZkuWz54arj8mHVf+8Y7cKkcD8Wr/fBpENgEXpjLc=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 h1:iXtILhvDxB6kPvEXgsDhGaZCSC6LQET5ZHSdJozeI0Y=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12 h1:ZD2+BSw9vFsNlKYIasSNt3uDbjqqXIBcM13UJv/Lx2k=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12/go.mod h1:Ms4zlcVBbXbiP7EVLhl+lgjvA/a7YphqQ3Ih3174EmI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.13 h1:mbRIur/BiHK6SKPjoBIXSE/hJ6g6JGRLuxQy1jGjlN4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.13/go.mod h1:ITg9em2KbJx1s0y4aqRX5OYWG6HBZ5TVR//OdpEZ2CQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.8 h1:iwYS40JnrBeA9e9aI5S6KKN4EB2zR4iUVYN0nwVivz4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.8/go.mod h1:Fm9Mi+ApqmFiknZtGpohVcBGvpTu542VC4XO9YudRi0=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.22 h1:V51LGlOq/1VsDsHUdoklAQi7rMmx4qQubvFYAlP2254=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.22/go.mod h1:4Pzhyz8hJOm2bepgl+NjvRx8vlUFAIIvJnZ/MkcNPpU=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.23 h1:9Fjh6fi/U5JEStVZijmaMpUwE/gvBJj7x2B/PjbO9To=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.23/go.mod h1:iMoT2f1tClxrWAAnKCXjZQ6LOmfLrMG14wmnWpM+F14=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.24 h1:mdPwDQPqxlw9Sc62Nt15yjEcARaDbPXkjRYtXsUripo=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.24/go.mod h1:ls5ytnwLTcQaUu32fMYXFI3MjpKuTwL840PAm9iqyEg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.8 h1:cWno7lefSH6Pp+mSznagKCgfDGeZRin66UvYUqAkyeA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.8/go.mod h1:tPD+VjU3ABTBoEJ3nctu5Nyg4P4yjqSH5bJGGkY4+XE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29 h1:DRebniUGZ2MqiiIVmQJ04vIXr918hubdHMnarSLEWyU=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29/go.mod h1:LfRkPCD8YHDM2E5eTkos2UpwYeZnBcVarTa8L59bJHA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.30 h1:/Z5jmNrKsSD7EmDjzAPsm/3L9IuOkzaynklJZ1qX7S4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.30/go.mod h1:lEzEZnOosE7zi8Z6royW1cFJTD9fpab4Ul1SBrllewk=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.31 h1:w2SIhW92DZPFrSL4ksVCr8IYff5OZwIcxg8+95tzvAI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.31/go.mod h1:wAhpCQbkov+IcvjozJbd2xRCoZybUEHNkcFunssNACg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.8 h1:/Mn7gTedG86nbpjT4QEKsN1D/fThiYe1qvq7WsBGNHg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.8/go.mod h1:Ae3va9LPmvjj231ukHB6UeT8nS7wTPfC3tMZSZMwNYg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.29 h1:hiME6pBzC7OTl9LMtlyTWBuEl1f4QBcUmFDKC7MLXtc=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.29/go.mod h1:G7RP+uhagpKtKhd1BM9N6JQqjCcGEU47K5lBVZQyRQw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.31 h1:uao4A3QZ5UmB326V6KF+qRpv9Tjz7IlnlnTbbANntlU=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.31/go.mod h1:I/1+z0VwL1GhQyLgkoHDlygpUZ+iTAwOQ/NsftiUL2I=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.32 h1:jWXtZdCnhXa9sGFixRaU2AxT4DIVse9HS4E2f+/KwV0=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.32/go.mod h1:9JS1UpfVvyD/ZPX8GsKb/Pq8scEM+7GP5fqh9SwH7po=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.72.2 h1:a7aQ3RW+ug4IbhoQp29NZdc7vqrzKZZfWZSaQAXOZvQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.72.2/go.mod h1:xMekrnhmJ5aqmyxtmALs7mlvXw5xRh+eYjOjvrIIFJ4=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.103.3 h1:JRseEu/vIDMaWis4bSw0QbXL+cvIGc1XnX076H5ZXLE=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.103.3/go.mod h1:77ZAgynvx1txMvDG8gGWoWkO1augYDxkp9JElWFgjQU=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.105.0 h1:XptwLL+UHXgafYMIHTy59IRovLbhz3znkxY2uS/pbXU=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.105.0/go.mod h1:zdmCoFO/dSI7GlrwsPqFJI+WlFnSU4Tc8TJnlXrM1Do=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.105.1 h1:LkBKxAOE5WXjlFuFZqPG1rREnl6I6QCMElcXFDEidos=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.105.1/go.mod h1:zdmCoFO/dSI7GlrwsPqFJI+WlFnSU4Tc8TJnlXrM1Do=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.105.2 h1:5C00eQYpTrgQXnp6V3P6P7zPElna3AXvlukbANE6nJI=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.105.2/go.mod h1:zdmCoFO/dSI7GlrwsPqFJI+WlFnSU4Tc8TJnlXrM1Do=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.106.0 h1:7QZWVJZWzHivHWIa+5TELLaBBkbuoj0GPwQtMlJ0sqk=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.106.0/go.mod h1:fcvq5L7dK+5cQFicEJwpI6e6Wn8NY2i6yT5wRLYVc7s=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.2.0 h1:3nXpRcFwRCW8n7HgO2QGy0Dc20eQNfBuUemGQhpF8m8=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.2.0/go.mod h1:LxYujSTLPRlp2vTtcUO/+1ilrew8ytt6SvQyOgejzFQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.4.0 h1:sLzmJGCMv+C8KqiJgEqDLB6vxaJGmobRh4rr//ZpA3w=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.4.0/go.mod h1:mxC0nT/C8wMMS97DemZPzvUZxvIt+2Iq+eS3JdFZGgg=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.4.1 h1:V7ZZ300WPXGjvkyore5DGe0ljVPOxCXie/thWdtSBXE=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.4.1/go.mod h1:mxC0nT/C8wMMS97DemZPzvUZxvIt+2Iq+eS3JdFZGgg=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.5.0 h1:OHH5iTQvVGmfHjX/5Q+vFuA/Rf2x6/95aJ/75QCQSm4=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.5.0/go.mod h1:mCF3AK9PpL49oOrhniUXWAfhVBVQ/XbytoE5eccZUIs=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.24.9 h1:YqtxripbjWb2QLyzRK9pByfEDvgg95gpC2AyDq4hFE8=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.24.9/go.mod h1:lV8iQpg6OLOfBnqbGMBKYjilBlf633qwHnBEiMSPoHY=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.31.3 h1:ey1XLTYXb9PcLt4535632o5kCGXNXEhNb620Dqwuylo=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.31.3/go.mod h1:Lk7PlmoTYryQmyBG0EXqj5BcUbj3whXdU2s3yGI3EAc=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.32.0 h1:qjMmry/cBDee1E/2gyvel0uRYCi3mwRZ2hf6N+GAodo=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.32.0/go.mod h1:u8af9Nqkmqnr96f7v9nHqzZT9XBwbXEkTiqT4ROuJSE=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.32.1 h1:gYFYh4iLLcAOJRLNPY2aD2g9DIhKn4eof8UkIrr1rTk=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.32.1/go.mod h1:u8af9Nqkmqnr96f7v9nHqzZT9XBwbXEkTiqT4ROuJSE=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.33.0 h1:CaJyYhxBE0M/HJX/YvSaSmQlsI91VHB0lKU8LtLxL3A=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.33.0/go.mod h1:+e6BMRMPjBQoCw/WovYR9GLy2IU0z4Q77smOB1DraSg=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.8 h1:6dBT1Lz8fK11m22R+AqfRsFn8320K0T5DTGxxOQBSMw=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.8/go.mod h1:/kiBvRQXBc6xeJTYzhSdGvJ5vm1tjaDEjH+MSeRJnlY=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6 h1:yLr03zQE/5Eu5l3QU0Si+xMbLMbSDF2YXsigqXngs6g=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6/go.mod h1:Q5N6icH+KJZDLh+ESNwzdv6cZ6vLFF/egy3IOxWhmz4=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.37.0 h1:fpOlDPI55HdszaxapEGk6HsGosOUaM2YPWJpjMgp8UI=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.37.0/go.mod h1:DMPWJBjYs6+3+f/qhBFEFPPlQ6NlhWjai3dJNvipJ84=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.37.1 h1:arjT9Cm3/WYbGmD5TUZHk4UQn4Lle1fUNZs5FC6CtF0=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.37.1/go.mod h1:DMPWJBjYs6+3+f/qhBFEFPPlQ6NlhWjai3dJNvipJ84=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.0 h1:tC323YV77QdafeBr6LUhLDTsboyuyHLNRwAyCP44kGU=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.0/go.mod h1:SfLK1sgviHmbI+MozR9iDwDjL4cdCVZtahsjoR+z7wg=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.6 h1:VwhTrsTuVn52an4mXx29PqRzs2Dvu921NpGk7y43tAM=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.6/go.mod h1:+8h7PZb3yY5ftmVLD7ocEoE98hdc8PoKS0H3wfx1dlc=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.43.3 h1:VrIhKRCSK1umelSgB9RghvA9RTUYeQffyAS5ApXehNI=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.43.3/go.mod h1:r8wkDOuLaaMFqFiYAb8dGY2A3gJCOujMc6CFOVC4Zhc=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.44.0 h1:bLZ0PolJ8J+HkJHztcXORUpHXBye2U8298lCEMi6ZCU=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.44.0/go.mod h1:9gdl4RrflIdpDb2TlXshWgR1F9TeCkvqDx77Vpr4Z/Q=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.44.1 h1:RvfHDg+xvAeZ+5741vUEjpOVtYSIm93W2zhx10Xtydw=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.44.1/go.mod h1:9gdl4RrflIdpDb2TlXshWgR1F9TeCkvqDx77Vpr4Z/Q=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.45.0 h1:Pd6PNlp4t8PTXxqzstICl52Wsy78vpjFZ7PRUj44mJc=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.45.0/go.mod h1:rmQ0TnHzuLPmabgjPcsywhsSOmaBDgzR4zvDxSPsGdg=
|
||||
github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro=
|
||||
github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
|
||||
github.com/aws/smithy-go v1.27.1 h1:4T340VFndXtADGF52gYa1POyL7s9E4Z1OeZ1hCscIw8=
|
||||
github.com/aws/smithy-go v1.27.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||
github.com/aws/smithy-go v1.27.2 h1:y9NPmSE6am6LjEFPfqHqG/jJk7AauQvhCJONKh7kpzk=
|
||||
github.com/aws/smithy-go v1.27.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||
github.com/aws/smithy-go v1.27.3 h1:F3Zb497UhhskkfpJmfkXswyo+t0sh9OTBnIHjogWbVY=
|
||||
github.com/aws/smithy-go v1.27.3/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik=
|
||||
@@ -121,6 +205,8 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.42.0 h1:kpt2PEJuOuqYkPcktfJqWWDjTEd/FNgrxcniL7kQrXQ=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.42.0/go.mod h1:W9zQ439utxymRrXsUOzZbFX4JhLxXU4+ZnCt8GG7yA8=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.43.0 h1:62yY3dT7/ShwOxzA0RsKRgshBmfElKI4d/Myu2OxDFU=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.43.0/go.mod h1:RyaZMFY7yi1kAs45S6mbFGz8O8rqB0dTY14uzvG4LCs=
|
||||
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||
@@ -149,6 +235,8 @@ golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
|
||||
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
@@ -174,6 +262,10 @@ google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
|
||||
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
|
||||
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
|
||||
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
|
||||
google.golang.org/grpc v1.82.0 h1:vguDnZUPjE26w09A63VoxZPnvPjB5Riyc0mkXPFmAIU=
|
||||
google.golang.org/grpc v1.82.0/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
|
||||
google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE=
|
||||
google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
|
||||
google.golang.org/protobuf v1.26.0-rc.1 h1:7QnIQpGRHE5RnLKnESfDoxm2dTapTZua5a0kS0A+VXQ=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
|
||||
|
||||
+325
-154
@@ -4,9 +4,9 @@
|
||||
"com.amazonaws:aws-lambda-java-core": -179547809,
|
||||
"com.amazonaws:aws-lambda-java-events": -1998571449,
|
||||
"com.google.android:annotations": 1910180409,
|
||||
"com.google.api.grpc:proto-google-common-protos": -2106826238,
|
||||
"com.google.auth:google-auth-library-credentials": 1149797254,
|
||||
"com.google.auth:google-auth-library-oauth2-http": -2102758232,
|
||||
"com.google.api.grpc:proto-google-common-protos": 391189699,
|
||||
"com.google.auth:google-auth-library-credentials": 43407369,
|
||||
"com.google.auth:google-auth-library-oauth2-http": 1085819179,
|
||||
"com.google.auto.value:auto-value": 1146221104,
|
||||
"com.google.auto.value:auto-value-annotations": -1101276935,
|
||||
"com.google.code.findbugs:jsr305": 495355163,
|
||||
@@ -25,10 +25,11 @@
|
||||
"com.squareup.okio:okio": 795722926,
|
||||
"com.thesamet.scalapb:compilerplugin_3": 603908795,
|
||||
"com.thesamet.scalapb:lenses_3": -1873974815,
|
||||
"com.thesamet.scalapb:protoc-bridge_3": -1260006911,
|
||||
"com.thesamet.scalapb:protoc-bridge_3": -1482619205,
|
||||
"com.thesamet.scalapb:scalapb-json4s_3": -772830803,
|
||||
"com.thesamet.scalapb:scalapb-runtime-grpc_3": -2026114331,
|
||||
"com.thesamet.scalapb:scalapb-runtime_3": -1281968352,
|
||||
"com.zaxxer:HikariCP": 1991920386,
|
||||
"io.netty:netty-buffer": 732437843,
|
||||
"io.netty:netty-codec": -1199307927,
|
||||
"io.netty:netty-codec-http": -1456497900,
|
||||
@@ -38,30 +39,31 @@
|
||||
"io.netty:netty-handler": -1970529483,
|
||||
"io.netty:netty-handler-proxy": -195328652,
|
||||
"io.netty:netty-resolver": -548277043,
|
||||
"io.netty:netty-tcnative-boringssl-static": 2033070843,
|
||||
"io.netty:netty-tcnative-classes": 2023773715,
|
||||
"io.netty:netty-tcnative-boringssl-static": -754344644,
|
||||
"io.netty:netty-tcnative-classes": -763641772,
|
||||
"io.netty:netty-transport": -1962486730,
|
||||
"io.netty:netty-transport-native-epoll": -1215800119,
|
||||
"io.netty:netty-transport-native-unix-common": -818773192,
|
||||
"io.netty:netty-transport-native-epoll": 833393060,
|
||||
"io.netty:netty-transport-native-unix-common": 1230419987,
|
||||
"io.opencensus:opencensus-api": 2042364617,
|
||||
"io.opencensus:opencensus-contrib-grpc-metrics": 317094357,
|
||||
"io.perfmark:perfmark-api": 1019157794,
|
||||
"io.sentry:sentry": 563160305,
|
||||
"io.sentry:sentry": -699383028,
|
||||
"javax.xml.bind:jaxb-api": -1735500680,
|
||||
"junit:junit": -744267592,
|
||||
"org.checkerframework:checker-qual": -819031361,
|
||||
"org.codehaus.mojo:animal-sniffer-annotations": -1634689353,
|
||||
"org.codehaus.mojo:animal-sniffer-annotations": 852823480,
|
||||
"org.json4s:json4s-ast_3": 687176866,
|
||||
"org.json4s:json4s-core_3": 977579605,
|
||||
"org.json4s:json4s-native_3": -123383795,
|
||||
"org.postgresql:postgresql": -1963352047,
|
||||
"org.mockito:mockito-core": -1925148636,
|
||||
"org.postgresql:postgresql": -1283293677,
|
||||
"org.reactivestreams:reactive-streams": 509083534,
|
||||
"org.scala-lang:scala3-staging_3": -360398695,
|
||||
"org.scalameta:scalafmt-cli_2.13": 243143182,
|
||||
"org.scalameta:scalafmt-cli_2.13": 1603259922,
|
||||
"org.scalamock:scalamock_3": -1047317697,
|
||||
"org.slf4j:slf4j-api": -2005163028,
|
||||
"org.slf4j:slf4j-simple": 1999565066,
|
||||
"org.xerial:sqlite-jdbc": 1165654128,
|
||||
"org.xerial:sqlite-jdbc": -641800335,
|
||||
"repositories": 514854214,
|
||||
"software.amazon.awssdk:aws-core": 578934204,
|
||||
"software.amazon.awssdk:http-client-spi": 1408046502,
|
||||
@@ -84,8 +86,8 @@
|
||||
"com.google.android:annotations": 769933135,
|
||||
"com.google.api.grpc:proto-google-common-protos": 1506674904,
|
||||
"com.google.api:api-common": 1729001290,
|
||||
"com.google.auth:google-auth-library-credentials": 563614471,
|
||||
"com.google.auth:google-auth-library-oauth2-http": -1252786723,
|
||||
"com.google.auth:google-auth-library-credentials": -426926107,
|
||||
"com.google.auth:google-auth-library-oauth2-http": -307063036,
|
||||
"com.google.auto.value:auto-value": -1399392164,
|
||||
"com.google.auto.value:auto-value-annotations": 1092025564,
|
||||
"com.google.code.findbugs:jsr305": -1038659426,
|
||||
@@ -94,8 +96,8 @@
|
||||
"com.google.guava:failureaccess": -56291903,
|
||||
"com.google.guava:guava": 2119270017,
|
||||
"com.google.guava:listenablefuture": 1588902908,
|
||||
"com.google.http-client:google-http-client": 1756875461,
|
||||
"com.google.http-client:google-http-client-gson": 1267916085,
|
||||
"com.google.http-client:google-http-client": -174274814,
|
||||
"com.google.http-client:google-http-client-gson": 1658743026,
|
||||
"com.google.j2objc:j2objc-annotations": -2074422376,
|
||||
"com.google.protobuf:protobuf-java": -1065767575,
|
||||
"com.google.re2j:re2j": -461460138,
|
||||
@@ -109,14 +111,15 @@
|
||||
"com.squareup.okhttp:okhttp": -1936180231,
|
||||
"com.squareup.okio:okio": 1978550531,
|
||||
"com.squareup.okio:okio-jvm": -1730532943,
|
||||
"com.thesamet.scalapb:compilerplugin_3": -1073075945,
|
||||
"com.thesamet.scalapb:lenses_3": 791427023,
|
||||
"com.thesamet.scalapb:protoc-bridge_3": -1984150046,
|
||||
"com.thesamet.scalapb:protoc-gen_3": 1860775213,
|
||||
"com.thesamet.scalapb:scalapb-json4s_3": 1579843487,
|
||||
"com.thesamet.scalapb:scalapb-runtime-grpc_3": 97373115,
|
||||
"com.thesamet.scalapb:scalapb-runtime_3": 818778125,
|
||||
"com.typesafe:config": -1791448454,
|
||||
"com.thesamet.scalapb:compilerplugin_3": -986481869,
|
||||
"com.thesamet.scalapb:lenses_3": 1632980089,
|
||||
"com.thesamet.scalapb:protoc-bridge_3": -1510160103,
|
||||
"com.thesamet.scalapb:protoc-gen_3": 36781197,
|
||||
"com.thesamet.scalapb:scalapb-json4s_3": 1449885529,
|
||||
"com.thesamet.scalapb:scalapb-runtime-grpc_3": 1266714255,
|
||||
"com.thesamet.scalapb:scalapb-runtime_3": 318411326,
|
||||
"com.typesafe:config": 1779132647,
|
||||
"com.zaxxer:HikariCP": 337938184,
|
||||
"commons-codec:commons-codec": 1377992676,
|
||||
"commons-io:commons-io": 669567296,
|
||||
"dev.dirs:directories": 1716880629,
|
||||
@@ -147,28 +150,30 @@
|
||||
"io.netty:netty-handler": 1617793819,
|
||||
"io.netty:netty-handler-proxy": 1503431664,
|
||||
"io.netty:netty-resolver": -533324791,
|
||||
"io.netty:netty-tcnative-boringssl-static": -1922162184,
|
||||
"io.netty:netty-tcnative-boringssl-static:jar:linux-aarch_64": -310918884,
|
||||
"io.netty:netty-tcnative-boringssl-static:jar:linux-x86_64": 1582797105,
|
||||
"io.netty:netty-tcnative-boringssl-static:jar:osx-aarch_64": -1315559501,
|
||||
"io.netty:netty-tcnative-boringssl-static:jar:osx-x86_64": -95251834,
|
||||
"io.netty:netty-tcnative-boringssl-static:jar:windows-x86_64": -1202154110,
|
||||
"io.netty:netty-tcnative-classes": -1020773440,
|
||||
"io.netty:netty-tcnative-boringssl-static": 1274713091,
|
||||
"io.netty:netty-tcnative-boringssl-static:jar:linux-aarch_64": 423479165,
|
||||
"io.netty:netty-tcnative-boringssl-static:jar:linux-x86_64": 1509601219,
|
||||
"io.netty:netty-tcnative-boringssl-static:jar:osx-aarch_64": -1452923369,
|
||||
"io.netty:netty-tcnative-boringssl-static:jar:osx-x86_64": -1682600003,
|
||||
"io.netty:netty-tcnative-boringssl-static:jar:windows-x86_64": 741676114,
|
||||
"io.netty:netty-tcnative-classes": -2096215842,
|
||||
"io.netty:netty-transport": -1390736500,
|
||||
"io.netty:netty-transport-classes-epoll": 704589083,
|
||||
"io.netty:netty-transport-native-epoll:jar:linux-x86_64": -2050990961,
|
||||
"io.netty:netty-transport-native-epoll:jar:linux-x86_64": -933296886,
|
||||
"io.netty:netty-transport-native-unix-common": -2090924105,
|
||||
"io.opencensus:opencensus-api": -1002511030,
|
||||
"io.opencensus:opencensus-contrib-grpc-metrics": -1797408624,
|
||||
"io.opencensus:opencensus-contrib-http-util": -541858674,
|
||||
"io.perfmark:perfmark-api": -752483303,
|
||||
"io.sentry:sentry": 1564972879,
|
||||
"io.sentry:sentry": 1646897141,
|
||||
"javax.activation:javax.activation-api": -217704356,
|
||||
"javax.annotation:javax.annotation-api": 193132517,
|
||||
"javax.inject:javax.inject": -1284481446,
|
||||
"javax.xml.bind:jaxb-api": 1600326587,
|
||||
"joda-time:joda-time": 230482511,
|
||||
"junit:junit": 712711302,
|
||||
"net.bytebuddy:byte-buddy": 105401824,
|
||||
"net.bytebuddy:byte-buddy-agent": -1761476423,
|
||||
"net.java.dev.jna:jna": 943627808,
|
||||
"net.java.dev.jna:jna-platform": 1004830334,
|
||||
"org.apache.commons:commons-compress": -308222379,
|
||||
@@ -179,7 +184,7 @@
|
||||
"org.apache.httpcomponents:httpclient": -504016800,
|
||||
"org.apache.httpcomponents:httpcore": -353060362,
|
||||
"org.apache.xbean:xbean-reflect": -1764161877,
|
||||
"org.checkerframework:checker-qual": -53559553,
|
||||
"org.checkerframework:checker-qual": 1032417002,
|
||||
"org.codehaus.mojo:animal-sniffer-annotations": -1570976252,
|
||||
"org.codehaus.plexus:plexus-archiver": 766678324,
|
||||
"org.codehaus.plexus:plexus-classworlds": -543968237,
|
||||
@@ -192,52 +197,54 @@
|
||||
"org.jetbrains.kotlin:kotlin-stdlib-common": 11605,
|
||||
"org.jetbrains:annotations": 116138949,
|
||||
"org.jline:jline:jar:jdk8": -1133032957,
|
||||
"org.json4s:json4s-ast_3": 689482233,
|
||||
"org.json4s:json4s-core_3": 481951064,
|
||||
"org.json4s:json4s-jackson-core_3": 813804735,
|
||||
"org.json4s:json4s-native-core_3": -289663551,
|
||||
"org.json4s:json4s-native_3": -2093359247,
|
||||
"org.json4s:json4s-ast_3": 218697247,
|
||||
"org.json4s:json4s-core_3": 888852031,
|
||||
"org.json4s:json4s-jackson-core_3": -1708586056,
|
||||
"org.json4s:json4s-native-core_3": -1556364554,
|
||||
"org.json4s:json4s-native_3": 1810593063,
|
||||
"org.jspecify:jspecify": -1402924792,
|
||||
"org.mockito:mockito-core": 534163387,
|
||||
"org.objenesis:objenesis": 684097820,
|
||||
"org.ow2.asm:asm": 1177470613,
|
||||
"org.postgresql:postgresql": -570711868,
|
||||
"org.postgresql:postgresql": -1200274402,
|
||||
"org.reactivestreams:reactive-streams": -1996658890,
|
||||
"org.scala-lang.modules:scala-collection-compat_2.13": -2012838353,
|
||||
"org.scala-lang.modules:scala-collection-compat_3": -915569507,
|
||||
"org.scala-lang.modules:scala-collection-compat_3": 2001700859,
|
||||
"org.scala-lang.modules:scala-xml_2.13": -1867291781,
|
||||
"org.scala-lang:scala-compiler": 993782407,
|
||||
"org.scala-lang:scala-library": -479379026,
|
||||
"org.scala-lang:scala-reflect": 2030778053,
|
||||
"org.scala-lang:scala3-library_3": -340570223,
|
||||
"org.scala-lang:scala3-library_3": -1714704820,
|
||||
"org.scala-lang:scala3-staging_3": 1767551804,
|
||||
"org.scalameta:common2_2.13": 183869992,
|
||||
"org.scalameta:common_2.13": 1613859331,
|
||||
"org.scalameta:io_2.13": 1078868274,
|
||||
"org.scalameta:mdoc-parser_2.13": 1998129208,
|
||||
"org.scalameta:metaconfig-core_2.13": 1560822216,
|
||||
"org.scalameta:metaconfig-pprint_2.13": -13676725,
|
||||
"org.scalameta:metaconfig-typesafe-config_2.13": -1923428474,
|
||||
"org.scalameta:munit-diff_2.13": 612923316,
|
||||
"org.scalameta:parsers_2.13": -2012847908,
|
||||
"org.scalameta:scalafmt-cli_2.13": -788023559,
|
||||
"org.scalameta:scalafmt-config_2.13": 761717294,
|
||||
"org.scalameta:scalafmt-core_2.13": 1878102965,
|
||||
"org.scalameta:scalafmt-dynamic-core_2.13": -1900839476,
|
||||
"org.scalameta:scalafmt-dynamic_2.13": 860328364,
|
||||
"org.scalameta:scalafmt-interfaces": 651108583,
|
||||
"org.scalameta:scalafmt-macros_2.13": -1771055183,
|
||||
"org.scalameta:scalafmt-sysops_2.13": -1263777532,
|
||||
"org.scalameta:scalameta_2.13": -428808011,
|
||||
"org.scalameta:common2_2.13": 1796949606,
|
||||
"org.scalameta:common_2.13": 1892326143,
|
||||
"org.scalameta:io_2.13": -1056988288,
|
||||
"org.scalameta:mdoc-parser_2.13": -377476829,
|
||||
"org.scalameta:metaconfig-core_2.13": -557456764,
|
||||
"org.scalameta:metaconfig-pprint_2.13": 268613438,
|
||||
"org.scalameta:metaconfig-typesafe-config_2.13": 618711541,
|
||||
"org.scalameta:munit-diff_2.13": 667211464,
|
||||
"org.scalameta:parsers_2.13": 970620234,
|
||||
"org.scalameta:scalafmt-cli_2.13": 1904050715,
|
||||
"org.scalameta:scalafmt-config_2.13": 1077807261,
|
||||
"org.scalameta:scalafmt-core_2.13": -542782848,
|
||||
"org.scalameta:scalafmt-dynamic-core_2.13": -565068427,
|
||||
"org.scalameta:scalafmt-dynamic_2.13": -133643165,
|
||||
"org.scalameta:scalafmt-interfaces": -1699622215,
|
||||
"org.scalameta:scalafmt-macros_2.13": -1322813645,
|
||||
"org.scalameta:scalafmt-sysops_2.13": -1994112303,
|
||||
"org.scalameta:scalameta_2.13": 625605450,
|
||||
"org.scalameta:svm-subs": -181782616,
|
||||
"org.scalameta:trees2_2.13": -1248113756,
|
||||
"org.scalameta:trees_2.13": -1233441989,
|
||||
"org.scalamock:scalamock_3": -9538025,
|
||||
"org.scalameta:trees2_2.13": 1553624450,
|
||||
"org.scalameta:trees_2.13": 1539326839,
|
||||
"org.scalamock:scalamock_3": 23905729,
|
||||
"org.slf4j:slf4j-api": -1249720338,
|
||||
"org.slf4j:slf4j-simple": -1378006603,
|
||||
"org.tukaani:xz": -1726944826,
|
||||
"org.typelevel:paiges-core_2.13": 1005692255,
|
||||
"org.virtuslab.scala-cli:config_2.13": -113493958,
|
||||
"org.virtuslab.scala-cli:specification-level_2.13": -176416420,
|
||||
"org.xerial:sqlite-jdbc": 532995836,
|
||||
"org.xerial:sqlite-jdbc": -2058325677,
|
||||
"software.amazon.awssdk:annotations": 998747622,
|
||||
"software.amazon.awssdk:apache5-client": -1074889369,
|
||||
"software.amazon.awssdk:arns": -343721679,
|
||||
@@ -272,12 +279,11 @@
|
||||
"software.amazon.eventstream:eventstream": 1951806649
|
||||
},
|
||||
"conflict_resolution": {
|
||||
"com.google.api.grpc:proto-google-common-protos:2.63.1": "com.google.api.grpc:proto-google-common-protos:2.64.1",
|
||||
"com.google.guava:failureaccess:1.0.1": "com.google.guava:failureaccess:1.0.3",
|
||||
"com.google.j2objc:j2objc-annotations:2.8": "com.google.j2objc:j2objc-annotations:3.1",
|
||||
"io.netty:netty-transport-native-unix-common:4.1.127.Final": "io.netty:netty-transport-native-unix-common:4.1.135.Final",
|
||||
"io.netty:netty-transport-native-unix-common:4.1.133.Final": "io.netty:netty-transport-native-unix-common:4.1.135.Final",
|
||||
"io.opencensus:opencensus-api:0.31.0": "io.opencensus:opencensus-api:0.31.1",
|
||||
"org.codehaus.mojo:animal-sniffer-annotations:1.26": "org.codehaus.mojo:animal-sniffer-annotations:1.27"
|
||||
"org.checkerframework:checker-qual:3.49.5": "org.checkerframework:checker-qual:3.55.1"
|
||||
},
|
||||
"artifacts": {
|
||||
"com.amazonaws:aws-lambda-java-core": {
|
||||
@@ -354,15 +360,15 @@
|
||||
},
|
||||
"com.google.auth:google-auth-library-credentials": {
|
||||
"shasums": {
|
||||
"jar": "dd7e39b8f5c366b09f8c2d400b7b63c925499df813b0f0d78044abd72098e159"
|
||||
"jar": "d29934610c3e9eb6c60e95ad05ca34ce486c3867c2f3689548b4aa2c2dd6ec52"
|
||||
},
|
||||
"version": "1.40.0"
|
||||
"version": "1.42.1"
|
||||
},
|
||||
"com.google.auth:google-auth-library-oauth2-http": {
|
||||
"shasums": {
|
||||
"jar": "67fe2c9df9780d8725c1dff61d2ffbbcb93fdcf15e91c44a5e610a4a69afd151"
|
||||
"jar": "fb54ada3c0223ffb4470fb6d8fbd55f764588dbd244dc7c05caaf4ab62977898"
|
||||
},
|
||||
"version": "1.40.0"
|
||||
"version": "1.42.1"
|
||||
},
|
||||
"com.google.auto.value:auto-value": {
|
||||
"shasums": {
|
||||
@@ -414,15 +420,15 @@
|
||||
},
|
||||
"com.google.http-client:google-http-client": {
|
||||
"shasums": {
|
||||
"jar": "97a0666340956dbc96893bc8cf843a5b57367543f218feddad3ff433fbfdaa68"
|
||||
"jar": "20aa0948f6ceb5cfa9b2b3adddd9b7e990ab40230e084081642cbb81235d3eef"
|
||||
},
|
||||
"version": "2.0.2"
|
||||
"version": "2.1.0"
|
||||
},
|
||||
"com.google.http-client:google-http-client-gson": {
|
||||
"shasums": {
|
||||
"jar": "37c8fd5c7869bc4abf3b3219c7e79cb566afa745559772e969c21595199600cd"
|
||||
"jar": "3cf72c7b88375a34bf721c664f42edc8e096e2370e4126c9b8933c3c92e591f3"
|
||||
},
|
||||
"version": "2.0.2"
|
||||
"version": "2.1.0"
|
||||
},
|
||||
"com.google.j2objc:j2objc-annotations": {
|
||||
"shasums": {
|
||||
@@ -516,9 +522,9 @@
|
||||
},
|
||||
"com.thesamet.scalapb:protoc-bridge_3": {
|
||||
"shasums": {
|
||||
"jar": "e7e2f1862f54076b6870bd034a7c16aae7b88cfee3d00b69dbb6b1175108560c"
|
||||
"jar": "019cee53ec9e4eb6be0f8b72be4c9a97b56ddd780da5128a4db97af4af32e186"
|
||||
},
|
||||
"version": "0.9.9"
|
||||
"version": "0.9.10"
|
||||
},
|
||||
"com.thesamet.scalapb:protoc-gen_3": {
|
||||
"shasums": {
|
||||
@@ -546,9 +552,15 @@
|
||||
},
|
||||
"com.typesafe:config": {
|
||||
"shasums": {
|
||||
"jar": "dd6b9fb5da5bc49d31b581703f57050e1e7d403814f62f3832181a7226b36256"
|
||||
"jar": "47d13e3196c84f2a8ccd014ae563738c3da5b7406b38e03505ec0601a2f4fc37"
|
||||
},
|
||||
"version": "1.4.6"
|
||||
"version": "1.4.9"
|
||||
},
|
||||
"com.zaxxer:HikariCP": {
|
||||
"shasums": {
|
||||
"jar": "2be45c641b841e81df87292e181f2c203500c5f0e9a1676f135927387f3cf40b"
|
||||
},
|
||||
"version": "7.1.0"
|
||||
},
|
||||
"commons-codec:commons-codec": {
|
||||
"shasums": {
|
||||
@@ -732,20 +744,20 @@
|
||||
},
|
||||
"io.netty:netty-tcnative-boringssl-static": {
|
||||
"shasums": {
|
||||
"jar": "ec3b14ceff74c5d7e24a378e64074744cc5e7035c49cf6ca0a4e23dff9713d1f",
|
||||
"linux-aarch_64": "11dfee82cfcb5c4f596271fe1d56a70fe77b4034e54162d0bd211623b540857f",
|
||||
"linux-x86_64": "39ac6b1eb4ffc18d5fe9412cc6253a8954bafc8e3ca10109bc370490f73a3673",
|
||||
"osx-aarch_64": "eef3fc7cdc5e2dc8c5cb50bae11443052bf1ee2f6e8b64378565746d1529f05e",
|
||||
"osx-x86_64": "e3beaa3ebdb136569528c59aa826d771efadc0ac9db484c6c0e457fa5efd0f07",
|
||||
"windows-x86_64": "5458b4b502763d49cd2e16c434312687178afac9c164cabf75c4a8bcecf7ac50"
|
||||
"jar": "ffb1fd42e5952863159f10a370e8c2dcfa3f40615ec4caefaa9ac8b1b1790233",
|
||||
"linux-aarch_64": "cba3868793eb7ad32963275f32eb3e8932a3d50192557af8abd2516dab8b8956",
|
||||
"linux-x86_64": "8e4bf375ae8b82fe657f54d142beb35d115d8294ec5d23a9c09afc8a93dd4928",
|
||||
"osx-aarch_64": "10c6dd7eee84c099ea9e40ccbd933b501c665738d610627c5390d729fc978a47",
|
||||
"osx-x86_64": "b5a5048cff2a64641cbbe9fcfffbf032ef506b4d2836c8ba9e689b3c461b24fc",
|
||||
"windows-x86_64": "dee808e0dd5b4c14b4e8bd8d116dff347163b32574b2fb11a2ade476f7d03628"
|
||||
},
|
||||
"version": "2.0.74.Final"
|
||||
"version": "2.0.75.Final"
|
||||
},
|
||||
"io.netty:netty-tcnative-classes": {
|
||||
"shasums": {
|
||||
"jar": "194874cf723794dd409fd1e728cd91ffbcff0584d73b4d8a1ad0d69d04acf9b3"
|
||||
"jar": "2d5a6a8be78bba7782eec385ba178b12f5bfc6837771fd3bb8c1d7cd18b6911c"
|
||||
},
|
||||
"version": "2.0.74.Final"
|
||||
"version": "2.0.75.Final"
|
||||
},
|
||||
"io.netty:netty-transport": {
|
||||
"shasums": {
|
||||
@@ -761,9 +773,9 @@
|
||||
},
|
||||
"io.netty:netty-transport-native-epoll": {
|
||||
"shasums": {
|
||||
"linux-x86_64": "3d03f27eea1cd23357a56a96e1eeedfeb3d74fa0ba4d5c36a862bd035056275b"
|
||||
"linux-x86_64": "fdc76fe46e7218ef54547c1002aff69c4597291cc3ef34322d6f1a2f3090c880"
|
||||
},
|
||||
"version": "4.1.127.Final"
|
||||
"version": "4.1.133.Final"
|
||||
},
|
||||
"io.netty:netty-transport-native-unix-common": {
|
||||
"shasums": {
|
||||
@@ -797,9 +809,9 @@
|
||||
},
|
||||
"io.sentry:sentry": {
|
||||
"shasums": {
|
||||
"jar": "8f3d16c54f013694fd987689a2b37fa6ed7767b95de79d1949a0e24a1d62cc1a"
|
||||
"jar": "0bac4f4330e94ca19eb91640618d22a89b47ce45ffb79aa3d55e41f9b4f33517"
|
||||
},
|
||||
"version": "8.43.2"
|
||||
"version": "8.50.1"
|
||||
},
|
||||
"javax.activation:javax.activation-api": {
|
||||
"shasums": {
|
||||
@@ -837,6 +849,18 @@
|
||||
},
|
||||
"version": "4.13.2"
|
||||
},
|
||||
"net.bytebuddy:byte-buddy": {
|
||||
"shasums": {
|
||||
"jar": "42ba43dcccd8d9d77bfe8776a83e72b67f1fa52c4038a98629e7d288b648da4e"
|
||||
},
|
||||
"version": "1.12.8"
|
||||
},
|
||||
"net.bytebuddy:byte-buddy-agent": {
|
||||
"shasums": {
|
||||
"jar": "18faf4f33893c3e883a3220adfdab25d6ceff9e46b1ad760a5b6f83df9373307"
|
||||
},
|
||||
"version": "1.12.8"
|
||||
},
|
||||
"net.java.dev.jna:jna": {
|
||||
"shasums": {
|
||||
"jar": "0c8eb7acf67261656d79005191debaba3b6bf5dd60a43735a245429381dbecff"
|
||||
@@ -899,9 +923,9 @@
|
||||
},
|
||||
"org.checkerframework:checker-qual": {
|
||||
"shasums": {
|
||||
"jar": "508c83c62c344f6f7ee28f47b88a8797d6116d043bfd1ca0576c828dd1df2880"
|
||||
"jar": "857658c8bdcbff794949a8d9922f88d63ee3751c203c7cd7d21ea97f9e745288"
|
||||
},
|
||||
"version": "3.49.5"
|
||||
"version": "3.55.1"
|
||||
},
|
||||
"org.codehaus.mojo:animal-sniffer-annotations": {
|
||||
"shasums": {
|
||||
@@ -1011,6 +1035,18 @@
|
||||
},
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"org.mockito:mockito-core": {
|
||||
"shasums": {
|
||||
"jar": "9a5f59ad06d0bb7d0fd2d4a74eb4d66d561ee0327f0febacb7f9cfb5be4abbfb"
|
||||
},
|
||||
"version": "4.4.0"
|
||||
},
|
||||
"org.objenesis:objenesis": {
|
||||
"shasums": {
|
||||
"jar": "03d960bd5aef03c653eb000413ada15eb77cdd2b8e4448886edf5692805e35f3"
|
||||
},
|
||||
"version": "3.2"
|
||||
},
|
||||
"org.ow2.asm:asm": {
|
||||
"shasums": {
|
||||
"jar": "876eab6a83daecad5ca67eb9fcabb063c97b5aeb8cf1fca7a989ecde17522051"
|
||||
@@ -1019,9 +1055,9 @@
|
||||
},
|
||||
"org.postgresql:postgresql": {
|
||||
"shasums": {
|
||||
"jar": "1981b31d3993c58702783c1cddf10a34e48c1f413d70ff1cb6def0a143484647"
|
||||
"jar": "6e0e4cc2d8cae902084f8a2b18728b073a6fd9d1f87c9d8bff8f298c18185b93"
|
||||
},
|
||||
"version": "42.7.11"
|
||||
"version": "42.7.13"
|
||||
},
|
||||
"org.reactivestreams:reactive-streams": {
|
||||
"shasums": {
|
||||
@@ -1067,9 +1103,9 @@
|
||||
},
|
||||
"org.scala-lang:scala3-library_3": {
|
||||
"shasums": {
|
||||
"jar": "a16e046f4a9df9e0d0e0bc54bf6298c94d7d867457f7b3b68291a88c568b92d0"
|
||||
"jar": "2c6eace9158df8ee4485a06128a479d968c1ebfc83ae9b1a394b58429f0d1492"
|
||||
},
|
||||
"version": "3.3.7"
|
||||
"version": "3.3.8"
|
||||
},
|
||||
"org.scala-lang:scala3-staging_3": {
|
||||
"shasums": {
|
||||
@@ -1079,111 +1115,111 @@
|
||||
},
|
||||
"org.scalameta:common2_2.13": {
|
||||
"shasums": {
|
||||
"jar": "42a4fae02d82dc7450dddd69ce1d8b8f159119132898af56169f77b8c9a623bd"
|
||||
"jar": "d16050f3557e472ac3489501edd4d941d975bc0a6f72a54363b6598e29a972d0"
|
||||
},
|
||||
"version": "4.17.0"
|
||||
"version": "4.17.3"
|
||||
},
|
||||
"org.scalameta:common_2.13": {
|
||||
"shasums": {
|
||||
"jar": "3b5b1f67809daf6044113737af8aa4cc5c3b36f071950cfcf7d02f4fe6b1d19a"
|
||||
"jar": "e15f1eca7e1d855daf0fd62874b944272b84d2bbee96f3adfaef0cdd34ec91e4"
|
||||
},
|
||||
"version": "4.17.0"
|
||||
"version": "4.17.3"
|
||||
},
|
||||
"org.scalameta:io_2.13": {
|
||||
"shasums": {
|
||||
"jar": "d09dac9887efb0aaf7bbcc5b5e20f7e7e4947800592a44359e2f31b2b6c9699d"
|
||||
"jar": "c9aa7493cf90a1acafcc96dad84670409aeb3b5e280a38ce40332eb6a21cf49f"
|
||||
},
|
||||
"version": "4.17.0"
|
||||
"version": "4.17.3"
|
||||
},
|
||||
"org.scalameta:mdoc-parser_2.13": {
|
||||
"shasums": {
|
||||
"jar": "37442a255f0e6a31c9786d7d8af4ef1e29ded76c4f44af29895b4b36d904a068"
|
||||
"jar": "4ce63efb015b6ea92bd0bcf107c8153b3e2857495821a9a59dc41424b4015831"
|
||||
},
|
||||
"version": "2.9.0"
|
||||
"version": "2.9.1"
|
||||
},
|
||||
"org.scalameta:metaconfig-core_2.13": {
|
||||
"shasums": {
|
||||
"jar": "f8ed537b95758409a0dd0a9533fdd6da703914f5901f42d6f96ba07c77f58aaf"
|
||||
"jar": "9493d98e6a9b8b43bdbd7a00e06fa1dcf295180e0bf0d94d705f18801c75ec80"
|
||||
},
|
||||
"version": "0.18.6"
|
||||
"version": "0.18.7"
|
||||
},
|
||||
"org.scalameta:metaconfig-pprint_2.13": {
|
||||
"shasums": {
|
||||
"jar": "cbe1531b67e0c080294e3eaeea35952524c91af0c100f517ba7004607f44be46"
|
||||
"jar": "9f727dfdf0883c4066509e82f9ab58142ce0fb25371599d4a1b15e35d52467af"
|
||||
},
|
||||
"version": "0.18.6"
|
||||
"version": "0.18.7"
|
||||
},
|
||||
"org.scalameta:metaconfig-typesafe-config_2.13": {
|
||||
"shasums": {
|
||||
"jar": "1ddbdfcc9f425ca385fa0e0079b8f6fc12571b2e03c9a3ae2bc0c832ca288bca"
|
||||
"jar": "85085a7a082eead66bf0dccd08d402e5a4bad1f3fc73673284fac8e8f9e6c4ec"
|
||||
},
|
||||
"version": "0.18.6"
|
||||
"version": "0.18.7"
|
||||
},
|
||||
"org.scalameta:munit-diff_2.13": {
|
||||
"shasums": {
|
||||
"jar": "789f02dfc17e62626560d48522d16bc550990a4d722d27b38d215a39dd8c9a5d"
|
||||
"jar": "79d52c3007ae90ee2c6e156e3b2a5c0cfeb5a0dd67e570a36cb9a9e84f6e1521"
|
||||
},
|
||||
"version": "1.3.0"
|
||||
"version": "1.3.4"
|
||||
},
|
||||
"org.scalameta:parsers_2.13": {
|
||||
"shasums": {
|
||||
"jar": "c0669f252bf96db52ac32c92bf54f2853b8f192287a827626c3efe51b20f94b3"
|
||||
"jar": "21931f60d741a95f6fc364f5514672b587b24a2316df0a80fc67059a9989828d"
|
||||
},
|
||||
"version": "4.17.0"
|
||||
"version": "4.17.3"
|
||||
},
|
||||
"org.scalameta:scalafmt-cli_2.13": {
|
||||
"shasums": {
|
||||
"jar": "1da5a56393f5fcf01d65c86ca91adc5f8d5448447a75bf9d8b229db5a0a84009"
|
||||
"jar": "86ab6706dcefa4e0efd8bd9fe6ab5d3223dfa3efcf2cd74cd0fddc2dc09864ee"
|
||||
},
|
||||
"version": "3.11.1"
|
||||
"version": "3.11.5"
|
||||
},
|
||||
"org.scalameta:scalafmt-config_2.13": {
|
||||
"shasums": {
|
||||
"jar": "c058aefc9f874c78072fca15a25e72a4d3ab31bc2872266c42cc6e2651f6cbf1"
|
||||
"jar": "0e000fe9c11c991f9a012331b829a7295b10995c1f5421701755e1cce6373149"
|
||||
},
|
||||
"version": "3.11.1"
|
||||
"version": "3.11.5"
|
||||
},
|
||||
"org.scalameta:scalafmt-core_2.13": {
|
||||
"shasums": {
|
||||
"jar": "f5f0e48927f0d5a32763f6b82a8427b40231db65a908c48b4438b000562c32d2"
|
||||
"jar": "301fffd68a5723390d62ce62b075ed2e9c84003a0994634951638c4df6eb77f8"
|
||||
},
|
||||
"version": "3.11.1"
|
||||
"version": "3.11.5"
|
||||
},
|
||||
"org.scalameta:scalafmt-dynamic-core_2.13": {
|
||||
"shasums": {
|
||||
"jar": "d4efc029f83f7c3ac6f3374b33636c3d38ef24bbb4d04ba300996d0e63991f2b"
|
||||
"jar": "f55a6614c5815f4435451eaf24b0b0a771fc26a2b3603cc8f4428ccfc6ae96c0"
|
||||
},
|
||||
"version": "3.11.1"
|
||||
"version": "3.11.5"
|
||||
},
|
||||
"org.scalameta:scalafmt-dynamic_2.13": {
|
||||
"shasums": {
|
||||
"jar": "101275030c12c0cc8baa9b6b015b93a4500915990a088d964f41fe482366b890"
|
||||
"jar": "fcaaf533c46fd0c297e33a4363587202e973d27f37dc37538e17195d964d0f3a"
|
||||
},
|
||||
"version": "3.11.1"
|
||||
"version": "3.11.5"
|
||||
},
|
||||
"org.scalameta:scalafmt-interfaces": {
|
||||
"shasums": {
|
||||
"jar": "ec8ac6769c5960e8ee04d4fee15a4b82ba6032b3df3f4d725221ef94dd1dad90"
|
||||
"jar": "eb8c7d5fd1728b1ea1905817d18d6e5859e5614ee4bf417b20c8f60c439f6f4c"
|
||||
},
|
||||
"version": "3.11.1"
|
||||
"version": "3.11.5"
|
||||
},
|
||||
"org.scalameta:scalafmt-macros_2.13": {
|
||||
"shasums": {
|
||||
"jar": "49a4ab5745cc0b54acdb74b3fd6017451d5699d5d327ca1bea0747bc622edc9f"
|
||||
"jar": "7edb248e9d74ad95ce8cabf9fd903dc20419f405a3e86eedbeefdf02e291cbe5"
|
||||
},
|
||||
"version": "3.11.1"
|
||||
"version": "3.11.5"
|
||||
},
|
||||
"org.scalameta:scalafmt-sysops_2.13": {
|
||||
"shasums": {
|
||||
"jar": "a862c2bd21ba6b83d403a8aca33af541de356f7d39e88c1656bd2cf6a3b28a80"
|
||||
"jar": "314582e8086be283b9df1c22b41269cf22eb6fed5af37be0a6049ba5ee08594e"
|
||||
},
|
||||
"version": "3.11.1"
|
||||
"version": "3.11.5"
|
||||
},
|
||||
"org.scalameta:scalameta_2.13": {
|
||||
"shasums": {
|
||||
"jar": "7606f691512cac7e52e1c28146cc95001212aeb3737b44d5ea6f4ec51b9a454c"
|
||||
"jar": "5d7b942d074c764f10da068903f7a8667838d68c6e7a737ad0d0061cada0d182"
|
||||
},
|
||||
"version": "4.17.0"
|
||||
"version": "4.17.3"
|
||||
},
|
||||
"org.scalameta:svm-subs": {
|
||||
"shasums": {
|
||||
@@ -1193,15 +1229,15 @@
|
||||
},
|
||||
"org.scalameta:trees2_2.13": {
|
||||
"shasums": {
|
||||
"jar": "c1ad0ac4c5617ee25ad97b72fb2d03c48d5ee89770e7cc70b0e3e984689296ce"
|
||||
"jar": "3f3729adf6faa48802f8b7c77ac67a6077cb99073596dc3eaf48d6ca9371fe55"
|
||||
},
|
||||
"version": "4.17.0"
|
||||
"version": "4.17.3"
|
||||
},
|
||||
"org.scalameta:trees_2.13": {
|
||||
"shasums": {
|
||||
"jar": "b45d46d995e166889cbda015f541ca79afd94ada860513438e6c0ab5f4f84591"
|
||||
"jar": "38047a5a7b0f66faad77e18177a6152413de848b23353b615b1cecab2c0212f3"
|
||||
},
|
||||
"version": "4.17.0"
|
||||
"version": "4.17.3"
|
||||
},
|
||||
"org.scalamock:scalamock_3": {
|
||||
"shasums": {
|
||||
@@ -1247,9 +1283,9 @@
|
||||
},
|
||||
"org.xerial:sqlite-jdbc": {
|
||||
"shasums": {
|
||||
"jar": "dc320e4102884c135ccc30c3c6fc3fb190b750e1586a100e3aba3be783cf33a9"
|
||||
"jar": "f55e405ed96d5ffe629e05b7b51b059e1c7d64527c0cc90a972fbac06730ccc1"
|
||||
},
|
||||
"version": "3.53.2.0"
|
||||
"version": "3.53.2.1"
|
||||
},
|
||||
"software.amazon.awssdk:annotations": {
|
||||
"shasums": {
|
||||
@@ -1581,6 +1617,9 @@
|
||||
"org.scala-lang.modules:scala-collection-compat_3",
|
||||
"org.scala-lang:scala3-library_3"
|
||||
],
|
||||
"com.zaxxer:HikariCP": [
|
||||
"org.slf4j:slf4j-api"
|
||||
],
|
||||
"io.get-coursier:coursier-cache_2.13": [
|
||||
"io.get-coursier.jniutils:windows-jni-utils",
|
||||
"io.get-coursier:cache-util",
|
||||
@@ -1839,6 +1878,14 @@
|
||||
"org.json4s:json4s-native-core_3",
|
||||
"org.scala-lang:scala3-library_3"
|
||||
],
|
||||
"org.mockito:mockito-core": [
|
||||
"net.bytebuddy:byte-buddy",
|
||||
"net.bytebuddy:byte-buddy-agent",
|
||||
"org.objenesis:objenesis"
|
||||
],
|
||||
"org.postgresql:postgresql": [
|
||||
"org.checkerframework:checker-qual"
|
||||
],
|
||||
"org.scala-lang.modules:scala-collection-compat_2.13": [
|
||||
"org.scala-lang:scala-library"
|
||||
],
|
||||
@@ -2311,20 +2358,12 @@
|
||||
"autovalue.shaded.com.google.auto.service",
|
||||
"autovalue.shaded.com.google.common.annotations",
|
||||
"autovalue.shaded.com.google.common.base",
|
||||
"autovalue.shaded.com.google.common.cache",
|
||||
"autovalue.shaded.com.google.common.collect",
|
||||
"autovalue.shaded.com.google.common.escape",
|
||||
"autovalue.shaded.com.google.common.eventbus",
|
||||
"autovalue.shaded.com.google.common.graph",
|
||||
"autovalue.shaded.com.google.common.hash",
|
||||
"autovalue.shaded.com.google.common.html",
|
||||
"autovalue.shaded.com.google.common.io",
|
||||
"autovalue.shaded.com.google.common.math",
|
||||
"autovalue.shaded.com.google.common.net",
|
||||
"autovalue.shaded.com.google.common.primitives",
|
||||
"autovalue.shaded.com.google.common.reflect",
|
||||
"autovalue.shaded.com.google.common.util.concurrent",
|
||||
"autovalue.shaded.com.google.common.xml",
|
||||
"autovalue.shaded.com.google.errorprone.annotations",
|
||||
"autovalue.shaded.com.google.errorprone.annotations.concurrent",
|
||||
"autovalue.shaded.com.google.escapevelocity",
|
||||
@@ -2557,6 +2596,16 @@
|
||||
"com.typesafe.config.impl",
|
||||
"com.typesafe.config.parser"
|
||||
],
|
||||
"com.zaxxer:HikariCP": [
|
||||
"com.zaxxer.hikari",
|
||||
"com.zaxxer.hikari.hibernate",
|
||||
"com.zaxxer.hikari.metrics",
|
||||
"com.zaxxer.hikari.metrics.dropwizard",
|
||||
"com.zaxxer.hikari.metrics.micrometer",
|
||||
"com.zaxxer.hikari.metrics.prometheus",
|
||||
"com.zaxxer.hikari.pool",
|
||||
"com.zaxxer.hikari.util"
|
||||
],
|
||||
"commons-codec:commons-codec": [
|
||||
"org.apache.commons.codec",
|
||||
"org.apache.commons.codec.binary",
|
||||
@@ -2735,8 +2784,6 @@
|
||||
"io.netty.util.concurrent",
|
||||
"io.netty.util.internal",
|
||||
"io.netty.util.internal.logging",
|
||||
"io.netty.util.internal.shaded.org.jctools.counters",
|
||||
"io.netty.util.internal.shaded.org.jctools.maps",
|
||||
"io.netty.util.internal.shaded.org.jctools.queues",
|
||||
"io.netty.util.internal.shaded.org.jctools.queues.atomic",
|
||||
"io.netty.util.internal.shaded.org.jctools.queues.atomic.unpadded",
|
||||
@@ -2911,6 +2958,50 @@
|
||||
"org.junit.runners.parameterized",
|
||||
"org.junit.validator"
|
||||
],
|
||||
"net.bytebuddy:byte-buddy": [
|
||||
"net.bytebuddy",
|
||||
"net.bytebuddy.agent.builder",
|
||||
"net.bytebuddy.asm",
|
||||
"net.bytebuddy.build",
|
||||
"net.bytebuddy.description",
|
||||
"net.bytebuddy.description.annotation",
|
||||
"net.bytebuddy.description.enumeration",
|
||||
"net.bytebuddy.description.field",
|
||||
"net.bytebuddy.description.method",
|
||||
"net.bytebuddy.description.modifier",
|
||||
"net.bytebuddy.description.type",
|
||||
"net.bytebuddy.dynamic",
|
||||
"net.bytebuddy.dynamic.loading",
|
||||
"net.bytebuddy.dynamic.scaffold",
|
||||
"net.bytebuddy.dynamic.scaffold.inline",
|
||||
"net.bytebuddy.dynamic.scaffold.subclass",
|
||||
"net.bytebuddy.implementation",
|
||||
"net.bytebuddy.implementation.attribute",
|
||||
"net.bytebuddy.implementation.auxiliary",
|
||||
"net.bytebuddy.implementation.bind",
|
||||
"net.bytebuddy.implementation.bind.annotation",
|
||||
"net.bytebuddy.implementation.bytecode",
|
||||
"net.bytebuddy.implementation.bytecode.assign",
|
||||
"net.bytebuddy.implementation.bytecode.assign.primitive",
|
||||
"net.bytebuddy.implementation.bytecode.assign.reference",
|
||||
"net.bytebuddy.implementation.bytecode.collection",
|
||||
"net.bytebuddy.implementation.bytecode.constant",
|
||||
"net.bytebuddy.implementation.bytecode.member",
|
||||
"net.bytebuddy.jar.asm",
|
||||
"net.bytebuddy.jar.asm.commons",
|
||||
"net.bytebuddy.jar.asm.signature",
|
||||
"net.bytebuddy.matcher",
|
||||
"net.bytebuddy.pool",
|
||||
"net.bytebuddy.utility",
|
||||
"net.bytebuddy.utility.dispatcher",
|
||||
"net.bytebuddy.utility.nullability",
|
||||
"net.bytebuddy.utility.privilege",
|
||||
"net.bytebuddy.utility.visitor"
|
||||
],
|
||||
"net.bytebuddy:byte-buddy-agent": [
|
||||
"net.bytebuddy.agent",
|
||||
"net.bytebuddy.agent.utility.nullability"
|
||||
],
|
||||
"net.java.dev.jna:jna": [
|
||||
"com.sun.jna",
|
||||
"com.sun.jna.ptr",
|
||||
@@ -2958,7 +3049,6 @@
|
||||
"org.apache.commons.compress.compressors.xz",
|
||||
"org.apache.commons.compress.compressors.z",
|
||||
"org.apache.commons.compress.compressors.zstandard",
|
||||
"org.apache.commons.compress.harmony",
|
||||
"org.apache.commons.compress.harmony.archive.internal.nls",
|
||||
"org.apache.commons.compress.harmony.pack200",
|
||||
"org.apache.commons.compress.harmony.unpack200",
|
||||
@@ -3364,6 +3454,84 @@
|
||||
"org.jspecify:jspecify": [
|
||||
"org.jspecify.annotations"
|
||||
],
|
||||
"org.mockito:mockito-core": [
|
||||
"org.mockito",
|
||||
"org.mockito.codegen",
|
||||
"org.mockito.configuration",
|
||||
"org.mockito.creation.instance",
|
||||
"org.mockito.exceptions.base",
|
||||
"org.mockito.exceptions.misusing",
|
||||
"org.mockito.exceptions.stacktrace",
|
||||
"org.mockito.exceptions.verification",
|
||||
"org.mockito.exceptions.verification.junit",
|
||||
"org.mockito.exceptions.verification.opentest4j",
|
||||
"org.mockito.hamcrest",
|
||||
"org.mockito.internal",
|
||||
"org.mockito.internal.configuration",
|
||||
"org.mockito.internal.configuration.injection",
|
||||
"org.mockito.internal.configuration.injection.filter",
|
||||
"org.mockito.internal.configuration.injection.scanner",
|
||||
"org.mockito.internal.configuration.plugins",
|
||||
"org.mockito.internal.creation",
|
||||
"org.mockito.internal.creation.bytebuddy",
|
||||
"org.mockito.internal.creation.instance",
|
||||
"org.mockito.internal.creation.proxy",
|
||||
"org.mockito.internal.creation.settings",
|
||||
"org.mockito.internal.creation.util",
|
||||
"org.mockito.internal.debugging",
|
||||
"org.mockito.internal.exceptions",
|
||||
"org.mockito.internal.exceptions.stacktrace",
|
||||
"org.mockito.internal.exceptions.util",
|
||||
"org.mockito.internal.framework",
|
||||
"org.mockito.internal.hamcrest",
|
||||
"org.mockito.internal.handler",
|
||||
"org.mockito.internal.invocation",
|
||||
"org.mockito.internal.invocation.finder",
|
||||
"org.mockito.internal.invocation.mockref",
|
||||
"org.mockito.internal.junit",
|
||||
"org.mockito.internal.listeners",
|
||||
"org.mockito.internal.matchers",
|
||||
"org.mockito.internal.matchers.apachecommons",
|
||||
"org.mockito.internal.matchers.text",
|
||||
"org.mockito.internal.progress",
|
||||
"org.mockito.internal.reporting",
|
||||
"org.mockito.internal.runners",
|
||||
"org.mockito.internal.runners.util",
|
||||
"org.mockito.internal.session",
|
||||
"org.mockito.internal.stubbing",
|
||||
"org.mockito.internal.stubbing.answers",
|
||||
"org.mockito.internal.stubbing.defaultanswers",
|
||||
"org.mockito.internal.util",
|
||||
"org.mockito.internal.util.collections",
|
||||
"org.mockito.internal.util.concurrent",
|
||||
"org.mockito.internal.util.io",
|
||||
"org.mockito.internal.util.reflection",
|
||||
"org.mockito.internal.verification",
|
||||
"org.mockito.internal.verification.api",
|
||||
"org.mockito.internal.verification.argumentmatching",
|
||||
"org.mockito.internal.verification.checkers",
|
||||
"org.mockito.invocation",
|
||||
"org.mockito.junit",
|
||||
"org.mockito.listeners",
|
||||
"org.mockito.mock",
|
||||
"org.mockito.plugins",
|
||||
"org.mockito.quality",
|
||||
"org.mockito.session",
|
||||
"org.mockito.stubbing",
|
||||
"org.mockito.verification"
|
||||
],
|
||||
"org.objenesis:objenesis": [
|
||||
"org.objenesis",
|
||||
"org.objenesis.instantiator",
|
||||
"org.objenesis.instantiator.android",
|
||||
"org.objenesis.instantiator.annotations",
|
||||
"org.objenesis.instantiator.basic",
|
||||
"org.objenesis.instantiator.gcj",
|
||||
"org.objenesis.instantiator.perc",
|
||||
"org.objenesis.instantiator.sun",
|
||||
"org.objenesis.instantiator.util",
|
||||
"org.objenesis.strategy"
|
||||
],
|
||||
"org.ow2.asm:asm": [
|
||||
"org.objectweb.asm",
|
||||
"org.objectweb.asm.signature"
|
||||
@@ -4021,8 +4189,6 @@
|
||||
"software.amazon.awssdk.thirdparty.jackson.core.filter",
|
||||
"software.amazon.awssdk.thirdparty.jackson.core.format",
|
||||
"software.amazon.awssdk.thirdparty.jackson.core.internal.shaded.fdp.v2_21_3",
|
||||
"software.amazon.awssdk.thirdparty.jackson.core.internal.shaded.fdp.v2_21_3.bte",
|
||||
"software.amazon.awssdk.thirdparty.jackson.core.internal.shaded.fdp.v2_21_3.chr",
|
||||
"software.amazon.awssdk.thirdparty.jackson.core.io",
|
||||
"software.amazon.awssdk.thirdparty.jackson.core.io.schubfach",
|
||||
"software.amazon.awssdk.thirdparty.jackson.core.json",
|
||||
@@ -4100,6 +4266,7 @@
|
||||
"com.thesamet.scalapb:scalapb-runtime-grpc_3",
|
||||
"com.thesamet.scalapb:scalapb-runtime_3",
|
||||
"com.typesafe:config",
|
||||
"com.zaxxer:HikariCP",
|
||||
"commons-codec:commons-codec",
|
||||
"commons-io:commons-io",
|
||||
"dev.dirs:directories",
|
||||
@@ -4152,6 +4319,8 @@
|
||||
"javax.xml.bind:jaxb-api",
|
||||
"joda-time:joda-time",
|
||||
"junit:junit",
|
||||
"net.bytebuddy:byte-buddy",
|
||||
"net.bytebuddy:byte-buddy-agent",
|
||||
"net.java.dev.jna:jna",
|
||||
"net.java.dev.jna:jna-platform",
|
||||
"org.apache.commons:commons-compress",
|
||||
@@ -4181,6 +4350,8 @@
|
||||
"org.json4s:json4s-native-core_3",
|
||||
"org.json4s:json4s-native_3",
|
||||
"org.jspecify:jspecify",
|
||||
"org.mockito:mockito-core",
|
||||
"org.objenesis:objenesis",
|
||||
"org.ow2.asm:asm",
|
||||
"org.postgresql:postgresql",
|
||||
"org.reactivestreams:reactive-streams",
|
||||
|
||||
+46
-5
@@ -6,10 +6,13 @@
|
||||
"dependencyDashboard": true,
|
||||
"enabledManagers": [
|
||||
"bazel-module",
|
||||
"bazelisk",
|
||||
"cargo",
|
||||
"custom.regex",
|
||||
"github-actions",
|
||||
"gomod",
|
||||
"swift"
|
||||
"swift",
|
||||
"unity3d"
|
||||
],
|
||||
"labels": [
|
||||
"dependencies"
|
||||
@@ -18,7 +21,9 @@
|
||||
{
|
||||
"description": "Bazel module artifact updates run in GitHub Actions so hosted Renovate does not need unsafe bazel mod deps execution",
|
||||
"matchManagers": [
|
||||
"bazel-module"
|
||||
"bazel-module",
|
||||
"bazelisk",
|
||||
"custom.regex"
|
||||
],
|
||||
"skipArtifactsUpdate": true
|
||||
},
|
||||
@@ -28,19 +33,55 @@
|
||||
"gomod"
|
||||
],
|
||||
"skipArtifactsUpdate": true
|
||||
},
|
||||
{
|
||||
"description": "Propose stable Unity editor patch updates for manual review",
|
||||
"matchManagers": [
|
||||
"unity3d"
|
||||
],
|
||||
"automerge": false,
|
||||
"dependencyDashboardApproval": true,
|
||||
"minimumReleaseAge": "7 days"
|
||||
},
|
||||
{
|
||||
"description": "Keep Unity editor upgrades on the current release line",
|
||||
"matchManagers": [
|
||||
"unity3d"
|
||||
],
|
||||
"matchUpdateTypes": [
|
||||
"major",
|
||||
"minor"
|
||||
],
|
||||
"enabled": false
|
||||
}
|
||||
],
|
||||
"customManagers": [
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "Update the LLVM and Clang toolchain release",
|
||||
"managerFilePatterns": [
|
||||
"/^MODULE\\.bazel$/"
|
||||
],
|
||||
"matchStrings": [
|
||||
"LLVM_VERSION = \"(?<currentValue>\\d+\\.\\d+\\.\\d+)\""
|
||||
],
|
||||
"depNameTemplate": "llvm/llvm-project",
|
||||
"datasourceTemplate": "github-releases",
|
||||
"extractVersionTemplate": "^llvmorg-(?<version>\\d+\\.\\d+\\.\\d+)$",
|
||||
"versioningTemplate": "semver"
|
||||
}
|
||||
],
|
||||
"lockFileMaintenance": {
|
||||
"enabled": true,
|
||||
"schedule": [
|
||||
"before 6am on monday"
|
||||
"* * * * 1"
|
||||
]
|
||||
},
|
||||
"prConcurrentLimit": 3,
|
||||
"prHourlyLimit": 1,
|
||||
"prHourlyLimit": 3,
|
||||
"rebaseWhen": "behind-base-branch",
|
||||
"schedule": [
|
||||
"before 6am on monday"
|
||||
"* * * * 1"
|
||||
],
|
||||
"separateMajorMinor": true,
|
||||
"timezone": "America/Los_Angeles"
|
||||
|
||||
@@ -10,4 +10,8 @@ if [ -n "${BAZEL_OUTPUT_BASE:-}" ]; then
|
||||
bazel_cmd+=("--output_base=${BAZEL_OUTPUT_BASE}")
|
||||
fi
|
||||
|
||||
if [ "$(uname -s)" = "Darwin" ]; then
|
||||
./scripts/sync_bazel_xcode.sh
|
||||
fi
|
||||
|
||||
"${bazel_cmd[@]}" build "$@" //src/main/scala/net/eagle0/eagle:eagle_server
|
||||
|
||||
@@ -10,4 +10,8 @@ if [ -n "${BAZEL_OUTPUT_BASE:-}" ]; then
|
||||
bazel_cmd+=("--output_base=${BAZEL_OUTPUT_BASE}")
|
||||
fi
|
||||
|
||||
if [ "$(uname -s)" = "Darwin" ]; then
|
||||
./scripts/sync_bazel_xcode.sh
|
||||
fi
|
||||
|
||||
"${bazel_cmd[@]}" build -c opt "$@" //src/main/cpp/net/eagle0/shardok:shardok-server
|
||||
|
||||
@@ -11,6 +11,12 @@ if [ -n "${BAZEL_OUTPUT_BASE:-}" ]; then
|
||||
fi
|
||||
|
||||
"${bazel_cmd[@]}" build \
|
||||
-c opt \
|
||||
--strip=never \
|
||||
--copt=-gline-tables-only \
|
||||
--copt=-fno-omit-frame-pointer \
|
||||
--copt=-mno-omit-leaf-frame-pointer \
|
||||
--linkopt=-Wl,--build-id=sha1 \
|
||||
--platforms=//:linux_arm64 \
|
||||
--extra_toolchains=@llvm_toolchain_linux_arm64//:cc-toolchain-aarch64-linux \
|
||||
"$@" \
|
||||
|
||||
@@ -241,6 +241,8 @@ main() {
|
||||
if [ -x "${WARMUP_SCRIPT}" ]; then
|
||||
if ! "${WARMUP_SCRIPT}" "localhost:${staging_port}"; then
|
||||
log_error "Warmup/smoke test failed, aborting deployment"
|
||||
log_error "Recent eagle-${staging} logs:"
|
||||
docker logs "eagle-${staging}" --tail 200 2>&1 || true
|
||||
docker compose -f "${COMPOSE_FILE}" stop "eagle-${staging}"
|
||||
cleanup_markers_on_failure "eagle-${active}"
|
||||
exit 1
|
||||
|
||||
+102
-25
@@ -4,41 +4,118 @@
|
||||
# Reads the active instance from /opt/eagle0/.active-instance (set by deploy-blue-green.sh).
|
||||
#
|
||||
# Usage:
|
||||
# eagle-logs # Tail logs (follow mode)
|
||||
# eagle-logs -n 100 # Show last 100 lines and follow
|
||||
# eagle-logs --no-follow -n 50 # Show last 50 lines without following
|
||||
# eagle-logs # Show existing logs and keep following
|
||||
# eagle-logs -n 100 # Show the last 100 lines and keep following
|
||||
# eagle-logs --no-follow -n 50 # Show the last 50 lines and exit
|
||||
#
|
||||
# To create an alias, add to ~/.bashrc:
|
||||
# alias eagle-logs='/opt/eagle0/scripts/eagle-logs.sh'
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
set -uo pipefail
|
||||
|
||||
APP_DIR="${APP_DIR:-/opt/eagle0}"
|
||||
ACTIVE_FILE="${APP_DIR}/.active-instance"
|
||||
RETRY_INTERVAL_SECONDS="${EAGLE_LOGS_RETRY_INTERVAL_SECONDS:-2}"
|
||||
|
||||
# Read active instance from file, with fallback
|
||||
if [ -f "$ACTIVE_FILE" ]; then
|
||||
ACTIVE=$(cat "$ACTIVE_FILE")
|
||||
else
|
||||
# Fallback: check which container is actually running
|
||||
if docker inspect --format='{{.State.Running}}' eagle-blue 2>/dev/null | grep -q true; then
|
||||
ACTIVE="eagle-blue"
|
||||
elif docker inspect --format='{{.State.Running}}' eagle-green 2>/dev/null | grep -q true; then
|
||||
ACTIVE="eagle-green"
|
||||
else
|
||||
ACTIVE=""
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
|
||||
FOLLOW=true
|
||||
DOCKER_LOG_ARGS=()
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--no-follow)
|
||||
FOLLOW=false
|
||||
;;
|
||||
-f | --follow)
|
||||
FOLLOW=true
|
||||
;;
|
||||
*)
|
||||
DOCKER_LOG_ARGS+=("$arg")
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
container_is_running() {
|
||||
[ "$(docker inspect --format='{{.State.Running}}' "$1" 2>/dev/null)" = "true" ]
|
||||
}
|
||||
|
||||
resolve_active_instance() {
|
||||
local recorded_active=""
|
||||
|
||||
if [ -f "$ACTIVE_FILE" ]; then
|
||||
IFS= read -r recorded_active < "$ACTIVE_FILE" || true
|
||||
fi
|
||||
|
||||
if [ -n "$recorded_active" ] && container_is_running "$recorded_active"; then
|
||||
printf '%s\n' "$recorded_active"
|
||||
elif container_is_running eagle-blue; then
|
||||
printf '%s\n' "eagle-blue"
|
||||
elif container_is_running eagle-green; then
|
||||
printf '%s\n' "eagle-green"
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
print_disconnect_marker() {
|
||||
local stopped_instance=${1:-unknown}
|
||||
|
||||
printf '\n' >&2
|
||||
printf '%s\n' '================================================================================' >&2
|
||||
printf '%s\n' '==================== EAGLE LOG STREAM DISCONNECTED ============================' >&2
|
||||
printf ' Server unavailable at %s (last instance: %s)\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$stopped_instance" >&2
|
||||
printf '%s\n' ' Waiting for the deployment or server restart; log following will resume.' >&2
|
||||
printf '%s\n' '================================================================================' >&2
|
||||
}
|
||||
|
||||
print_resume_marker() {
|
||||
local active_instance=$1
|
||||
|
||||
printf '\n' >&2
|
||||
printf '%s\n' '================================================================================' >&2
|
||||
printf '%s\n' '======================= EAGLE LOG STREAM RESUMED ==============================' >&2
|
||||
printf ' Attached to %s at %s\n' "$active_instance" "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" >&2
|
||||
printf '%s\n' '================================================================================' >&2
|
||||
}
|
||||
|
||||
if [ "$FOLLOW" = false ]; then
|
||||
if ! ACTIVE=$(resolve_active_instance); then
|
||||
echo "Error: No active Eagle instance found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exec docker logs "${DOCKER_LOG_ARGS[@]}" "$ACTIVE"
|
||||
fi
|
||||
|
||||
if [ -z "$ACTIVE" ]; then
|
||||
echo "Error: No active Eagle instance found" >&2
|
||||
exit 1
|
||||
fi
|
||||
ACTIVE=""
|
||||
DISCONNECTED=false
|
||||
RECONNECT_SINCE=""
|
||||
|
||||
# Default to follow mode if no args provided
|
||||
if [ $# -eq 0 ]; then
|
||||
exec docker logs -f "$ACTIVE"
|
||||
else
|
||||
exec docker logs "$@" "$ACTIVE"
|
||||
fi
|
||||
while true; do
|
||||
if NEXT_ACTIVE=$(resolve_active_instance); then
|
||||
if [ "$DISCONNECTED" = true ]; then
|
||||
print_resume_marker "$NEXT_ACTIVE"
|
||||
fi
|
||||
|
||||
ACTIVE="$NEXT_ACTIVE"
|
||||
DISCONNECTED=false
|
||||
|
||||
if [ -n "$RECONNECT_SINCE" ]; then
|
||||
docker logs --follow "${DOCKER_LOG_ARGS[@]}" --since "$RECONNECT_SINCE" "$ACTIVE" || true
|
||||
else
|
||||
docker logs --follow "${DOCKER_LOG_ARGS[@]}" "$ACTIVE" || true
|
||||
fi
|
||||
|
||||
RECONNECT_SINCE=$(date +%s)
|
||||
fi
|
||||
|
||||
if [ "$DISCONNECTED" = false ]; then
|
||||
print_disconnect_marker "$ACTIVE"
|
||||
DISCONNECTED=true
|
||||
fi
|
||||
|
||||
sleep "$RETRY_INTERVAL_SECONDS"
|
||||
done
|
||||
|
||||
Executable
+85
@@ -0,0 +1,85 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
TEST_DIR=$(mktemp -d)
|
||||
trap 'rm -rf "$TEST_DIR"' EXIT
|
||||
|
||||
FAKE_BIN="${TEST_DIR}/bin"
|
||||
APP_DIR="${TEST_DIR}/app"
|
||||
STATE_FILE="${TEST_DIR}/state"
|
||||
CALLS_FILE="${TEST_DIR}/calls"
|
||||
OUTPUT_FILE="${TEST_DIR}/output"
|
||||
mkdir -p "$FAKE_BIN" "$APP_DIR"
|
||||
|
||||
cat > "${FAKE_BIN}/docker" <<'EOF'
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
case "$1" in
|
||||
inspect)
|
||||
container=${@: -1}
|
||||
state=$(cat "$EAGLE_LOGS_TEST_STATE_FILE")
|
||||
if { [ "$state" = "blue" ] && [ "$container" = "eagle-blue" ]; } ||
|
||||
{ [ "$state" = "green" ] && [ "$container" = "eagle-green" ]; }; then
|
||||
echo true
|
||||
else
|
||||
echo false
|
||||
fi
|
||||
;;
|
||||
logs)
|
||||
container=${@: -1}
|
||||
printf '%s\n' "$*" >> "$EAGLE_LOGS_TEST_CALLS_FILE"
|
||||
if [ "$container" = "eagle-blue" ]; then
|
||||
echo "last blue log"
|
||||
echo "eagle-green" > "$EAGLE_LOGS_TEST_ACTIVE_FILE"
|
||||
echo "green" > "$EAGLE_LOGS_TEST_STATE_FILE"
|
||||
else
|
||||
echo "first green log"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "Unexpected docker command: $*" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
EOF
|
||||
chmod +x "${FAKE_BIN}/docker"
|
||||
|
||||
export EAGLE_LOGS_TEST_STATE_FILE="$STATE_FILE"
|
||||
export EAGLE_LOGS_TEST_CALLS_FILE="$CALLS_FILE"
|
||||
export EAGLE_LOGS_TEST_ACTIVE_FILE="${APP_DIR}/.active-instance"
|
||||
|
||||
echo "blue" > "$STATE_FILE"
|
||||
echo "eagle-blue" > "${APP_DIR}/.active-instance"
|
||||
|
||||
PATH="${FAKE_BIN}:$PATH" APP_DIR="$APP_DIR" "${SCRIPT_DIR}/eagle-logs.sh" --no-follow -n 25
|
||||
grep -q '^logs -n 25 eagle-blue$' "$CALLS_FILE"
|
||||
|
||||
: > "$CALLS_FILE"
|
||||
echo "blue" > "$STATE_FILE"
|
||||
echo "eagle-blue" > "${APP_DIR}/.active-instance"
|
||||
PATH="${FAKE_BIN}:$PATH" APP_DIR="$APP_DIR" EAGLE_LOGS_RETRY_INTERVAL_SECONDS=0.01 \
|
||||
"${SCRIPT_DIR}/eagle-logs.sh" -n 10 > "$OUTPUT_FILE" 2>&1 &
|
||||
FOLLOW_PID=$!
|
||||
|
||||
for _ in $(seq 1 100); do
|
||||
if grep -q 'first green log' "$OUTPUT_FILE"; then
|
||||
break
|
||||
fi
|
||||
sleep 0.01
|
||||
done
|
||||
|
||||
kill "$FOLLOW_PID" 2>/dev/null || true
|
||||
wait "$FOLLOW_PID" 2>/dev/null || true
|
||||
|
||||
grep -q 'EAGLE LOG STREAM DISCONNECTED' "$OUTPUT_FILE"
|
||||
grep -q 'EAGLE LOG STREAM RESUMED' "$OUTPUT_FILE"
|
||||
grep -q 'last blue log' "$OUTPUT_FILE"
|
||||
grep -q 'first green log' "$OUTPUT_FILE"
|
||||
grep -q '^logs --follow -n 10 eagle-blue$' "$CALLS_FILE"
|
||||
grep -Eq '^logs --follow -n 10 --since [0-9]+ eagle-green$' "$CALLS_FILE"
|
||||
|
||||
echo "eagle-logs tests passed"
|
||||
@@ -32,23 +32,33 @@ echo "=== Creating ZIP for notarization: $ZIP_PATH ===" >&2
|
||||
ditto -c -k --keepParent "$APP_PATH" "$ZIP_PATH"
|
||||
|
||||
echo "=== Submitting to Apple for notarization ===" >&2
|
||||
set +e
|
||||
SUBMIT_OUTPUT=$(xcrun notarytool submit "$ZIP_PATH" \
|
||||
--apple-id "$APPLE_ID" \
|
||||
--password "$APP_SPECIFIC_PASSWORD" \
|
||||
--team-id "$TEAM_ID" 2>&1)
|
||||
SUBMIT_STATUS=$?
|
||||
set -e
|
||||
|
||||
echo "$SUBMIT_OUTPUT" >&2
|
||||
|
||||
if [ "$SUBMIT_STATUS" -ne 0 ]; then
|
||||
rm -f "$ZIP_PATH"
|
||||
echo "ERROR: notarytool submit failed with exit code $SUBMIT_STATUS" >&2
|
||||
exit "$SUBMIT_STATUS"
|
||||
fi
|
||||
|
||||
# Extract submission ID
|
||||
SUBMISSION_ID=$(echo "$SUBMIT_OUTPUT" | grep "id:" | head -1 | awk '{print $2}')
|
||||
SUBMISSION_ID=$(echo "$SUBMIT_OUTPUT" | awk '/^[[:space:]]*id:/ {print $2; exit}')
|
||||
|
||||
if [ -z "$SUBMISSION_ID" ]; then
|
||||
rm -f "$ZIP_PATH"
|
||||
echo "ERROR: Failed to get submission ID" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Clean up the zip
|
||||
rm "$ZIP_PATH"
|
||||
rm -f "$ZIP_PATH"
|
||||
|
||||
echo "Submission ID: $SUBMISSION_ID" >&2
|
||||
|
||||
|
||||
@@ -105,6 +105,7 @@ public:
|
||||
|
||||
class Iter {
|
||||
public:
|
||||
using iterator_concept = std::forward_iterator_tag;
|
||||
using iterator_category = std::forward_iterator_tag;
|
||||
using difference_type = std::ptrdiff_t;
|
||||
using value_type = size_t;
|
||||
@@ -112,14 +113,14 @@ public:
|
||||
using reference = size_t;
|
||||
|
||||
private:
|
||||
size_t storeIndex;
|
||||
size_t bitIndex;
|
||||
size_t storeIndex = STORE_COUNT;
|
||||
size_t bitIndex = 0;
|
||||
|
||||
const IterableBitset& bs;
|
||||
const IterableBitset* bs = nullptr;
|
||||
|
||||
void ToNext() {
|
||||
do {
|
||||
const STORE_TYPE current = bs.store[storeIndex] >> bitIndex;
|
||||
const STORE_TYPE current = bs->store[storeIndex] >> bitIndex;
|
||||
if (current != 0) {
|
||||
// Skip ahead by the number of trailing zeroes
|
||||
const auto ctz = std::countr_zero(current);
|
||||
@@ -132,10 +133,12 @@ public:
|
||||
}
|
||||
|
||||
public:
|
||||
Iter() = default;
|
||||
|
||||
Iter(const IterableBitset& bs, const int startStore, const int startBit)
|
||||
: storeIndex(startStore),
|
||||
bitIndex(startBit),
|
||||
bs(bs) {
|
||||
bs(&bs) {
|
||||
ToNext();
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace shardok {
|
||||
using Coords = net::eagle0::shardok::storage::fb::Coords;
|
||||
|
||||
constexpr double kDefaultMorale = 50.0;
|
||||
constexpr float kExplicitMoraleSentinel = -1.0f;
|
||||
|
||||
auto ConvertBattalion(const net::eagle0::common::CommonBattalion &battalion) -> Battalion {
|
||||
Battalion shardokBattalion{};
|
||||
@@ -18,6 +19,10 @@ auto ConvertBattalion(const net::eagle0::common::CommonBattalion &battalion) ->
|
||||
shardokBattalion.mutate_type(
|
||||
static_cast<net::eagle0::shardok::storage::fb::BattalionTypeId>(battalion.type()));
|
||||
shardokBattalion.mutate_morale(kDefaultMorale);
|
||||
if (battalion.has_morale()) {
|
||||
shardokBattalion.mutate_morale(static_cast<float>(battalion.morale()));
|
||||
shardokBattalion.mutate_base_morale(kExplicitMoraleSentinel);
|
||||
}
|
||||
shardokBattalion.mutate_armament(static_cast<float>(battalion.armament()));
|
||||
shardokBattalion.mutate_training(static_cast<float>(battalion.training()));
|
||||
|
||||
@@ -125,4 +130,4 @@ auto ConvertUnit(
|
||||
return shardokUnit;
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
} // namespace shardok
|
||||
|
||||
@@ -40,7 +40,8 @@ auto AttackLocations::LocationsWithEnemyInRange(const Unit *unit) const -> Coord
|
||||
|
||||
AttackLocationsCache::AttackLocationsCache(const HexMap *hexMap, const SettingsGetter &settings)
|
||||
: rowCount(hexMap->row_count()),
|
||||
columnCount(hexMap->column_count()) {
|
||||
columnCount(hexMap->column_count()),
|
||||
landRegions(hexMap) {
|
||||
const int mageRange =
|
||||
std::max(settings.Backing().meteor_range(), settings.Backing().lightning_range());
|
||||
const auto fearRange = settings.Backing().fear_range();
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/Coordinates.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/LandRegions.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/unit.hpp"
|
||||
|
||||
@@ -151,6 +152,8 @@ struct alignas(128) AttackLocationsCache {
|
||||
private:
|
||||
const MapIndex rowCount;
|
||||
const MapIndex columnCount;
|
||||
// Immutable static-map analysis shared by every speculative state evaluated by this AI.
|
||||
const LandRegions landRegions;
|
||||
|
||||
// For each of these vectors, the index is the map index of the tile *that can be attacked*,
|
||||
// and the entry is the CoordsSet of the tiles that can attack that location.
|
||||
@@ -161,6 +164,7 @@ private:
|
||||
public:
|
||||
AttackLocationsCache(const HexMap* hexMap, const SettingsGetter& settings);
|
||||
|
||||
[[nodiscard]] auto GetLandRegions() const -> const LandRegions& { return landRegions; }
|
||||
auto CachedLocations(const Coords& singleTarget, bool lateGame) -> const AttackLocations&;
|
||||
auto CachedLocations(const CoordsSet& targets, bool lateGame) -> AttackLocations;
|
||||
auto CachedLocations(const std::vector<const Unit*>& enemies, bool lateGame) -> AttackLocations;
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
|
||||
namespace shardok {
|
||||
|
||||
constexpr bool kMultithread = true;
|
||||
constexpr bool kLogging = false;
|
||||
|
||||
[[nodiscard]] static auto AverageGenerator() -> std::shared_ptr<SequenceRandomGenerator> {
|
||||
@@ -120,10 +119,12 @@ static auto CommandSorter(
|
||||
AICommandEvaluator::AICommandEvaluator(
|
||||
const AIScoreCalculator& scorer,
|
||||
const APDCache& apdCache,
|
||||
BattalionTypeGetter battalionTypeGetter)
|
||||
BattalionTypeGetter battalionTypeGetter,
|
||||
AIThreadPool& threadPool)
|
||||
: scorer_(scorer),
|
||||
apdCache_(apdCache),
|
||||
battalionTypeGetter_(std::move(battalionTypeGetter)) {}
|
||||
battalionTypeGetter_(std::move(battalionTypeGetter)),
|
||||
threadPool_(threadPool) {}
|
||||
|
||||
auto AICommandEvaluator::PerformLookahead(
|
||||
const PlayerId pid,
|
||||
@@ -295,7 +296,7 @@ auto AICommandEvaluator::EvaluateWithRandomness(
|
||||
returnValue.lookaheadScore = p.get_future();
|
||||
p.set_value(EvaluationResult{.score = innerUtility, .completed = true});
|
||||
} else {
|
||||
auto lookaheadLambda = [this,
|
||||
auto lookaheadLambda = [evaluator = *this,
|
||||
pid,
|
||||
isDefender,
|
||||
remainingLookahead,
|
||||
@@ -307,8 +308,11 @@ auto AICommandEvaluator::EvaluateWithRandomness(
|
||||
guessedCommands,
|
||||
allCastleCoords,
|
||||
deadline]() -> EvaluationResult {
|
||||
if (std::chrono::steady_clock::now() > deadline) {
|
||||
return EvaluationResult{.score = 0.0, .completed = false};
|
||||
}
|
||||
try {
|
||||
auto lookaheadFuture = PerformLookahead(
|
||||
auto lookaheadFuture = evaluator.PerformLookahead(
|
||||
pid,
|
||||
isDefender,
|
||||
remainingLookahead,
|
||||
@@ -329,15 +333,14 @@ auto AICommandEvaluator::EvaluateWithRandomness(
|
||||
}
|
||||
};
|
||||
|
||||
if constexpr (kMultithread) {
|
||||
auto launchPolicy =
|
||||
remainingLookahead == 1 ? std::launch::async : std::launch::deferred;
|
||||
returnValue.lookaheadScore = std::async(launchPolicy, lookaheadLambda);
|
||||
// Only the depth-one frontier is parallel. A pool worker therefore reaches depth zero,
|
||||
// never another queued frontier. AIThreadPool also executes same-pool reentrant
|
||||
// submissions inline as a hard backstop against worker-waits-on-worker deadlocks.
|
||||
if (remainingLookahead == 1) {
|
||||
returnValue.lookaheadScore = threadPool_.Submit(std::move(lookaheadLambda));
|
||||
} else {
|
||||
std::promise<EvaluationResult> p;
|
||||
returnValue.lookaheadScore = p.get_future();
|
||||
auto lambdaResult = lookaheadLambda();
|
||||
p.set_value(lambdaResult);
|
||||
returnValue.lookaheadScore =
|
||||
std::async(std::launch::deferred, std::move(lookaheadLambda));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <future>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIStrategy.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIThreadPool.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCTypes.h"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistancesCache.hpp"
|
||||
@@ -35,7 +36,8 @@ public:
|
||||
AICommandEvaluator(
|
||||
const AIScoreCalculator& scorer,
|
||||
const APDCache& apdCache,
|
||||
BattalionTypeGetter battalionTypeGetter); // Pass by value
|
||||
BattalionTypeGetter battalionTypeGetter,
|
||||
AIThreadPool& threadPool = SharedAIThreadPool()); // Pass getter by value
|
||||
|
||||
/// Evaluates the score for a particular command index with lookahead.
|
||||
struct EvaluationResult {
|
||||
@@ -79,6 +81,7 @@ private:
|
||||
const AIScoreCalculator& scorer_;
|
||||
const APDCache& apdCache_;
|
||||
BattalionTypeGetter battalionTypeGetter_;
|
||||
AIThreadPool& threadPool_;
|
||||
|
||||
struct ImmediateAndLookaheadScore {
|
||||
ScoreValue immediateScore{};
|
||||
|
||||
@@ -4,9 +4,6 @@
|
||||
|
||||
#include "AIDefenderStrategySelector.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <ranges>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIAttackGroups.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIScoreUtilities.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIWaterCrossingCalculator.hpp"
|
||||
@@ -23,31 +20,14 @@ auto AIDefenderStrategySelector::BestDefenderStrategy(
|
||||
const CoordsSet& criticalTileCoords,
|
||||
int maxRounds,
|
||||
const APDCache& apdCache,
|
||||
const LandRegions& landRegions,
|
||||
const BattalionTypeGetter& battalionTypeGetter) -> AIStrategy {
|
||||
uint32_t attackerNonUndeadUnitCount = 0;
|
||||
uint32_t attackerNonUndeadUnitNotRequiringWaterCrossingCount = 0;
|
||||
int attackerTroops = 0;
|
||||
int defenderTroops = 0;
|
||||
|
||||
bool canFlee = false;
|
||||
|
||||
vector<UnitId> attackerUnitIdsRequiringWaterCrossing{};
|
||||
attackerUnitIdsRequiringWaterCrossing.reserve(gameState->units()->size());
|
||||
for (const auto& player : *gameState->player_infos()) {
|
||||
if (!player->is_defender()) {
|
||||
const auto& unitIdsRequiringWaterCrossing = UnitIdsRequiringWaterCrossing(
|
||||
gameState,
|
||||
player->player_id(),
|
||||
criticalTileCoords,
|
||||
apdCache,
|
||||
battalionTypeGetter);
|
||||
attackerUnitIdsRequiringWaterCrossing.insert(
|
||||
attackerUnitIdsRequiringWaterCrossing.end(),
|
||||
unitIdsRequiringWaterCrossing.begin(),
|
||||
unitIdsRequiringWaterCrossing.end());
|
||||
}
|
||||
}
|
||||
|
||||
for (const Unit* unit : *gameState->units()) {
|
||||
if (unit->status() != net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
|
||||
unit->status() != net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT)
|
||||
@@ -63,12 +43,6 @@ auto AIDefenderStrategySelector::BestDefenderStrategy(
|
||||
if (unit->battalion().type() !=
|
||||
net::eagle0::shardok::storage::fb::BattalionTypeId_UNDEAD) {
|
||||
++attackerNonUndeadUnitCount;
|
||||
|
||||
if (!std::ranges::contains(
|
||||
attackerUnitIdsRequiringWaterCrossing,
|
||||
unit->unit_id())) {
|
||||
++attackerNonUndeadUnitNotRequiringWaterCrossingCount;
|
||||
}
|
||||
}
|
||||
attackerTroops += unit->battalion().size();
|
||||
}
|
||||
@@ -86,11 +60,21 @@ auto AIDefenderStrategySelector::BestDefenderStrategy(
|
||||
// other considerations we should have but do not presently:
|
||||
// * the attacker is close to the defender / castles
|
||||
|
||||
if (canFlee && roundsRemaining > 2 &&
|
||||
defenderTroops < MAXIMUM_RATIO_FOR_DEFENDER_TO_FLEE * attackerTroops &&
|
||||
attackerNonUndeadUnitNotRequiringWaterCrossingCount >= criticalTileCoords.size()) {
|
||||
chosenStrategy = FleeStrategy;
|
||||
} else if (attackerNonUndeadUnitCount >= criticalTileCoords.size()) {
|
||||
const bool waterCrossingCouldAffectStrategy =
|
||||
canFlee && roundsRemaining > 2 &&
|
||||
defenderTroops < MAXIMUM_RATIO_FOR_DEFENDER_TO_FLEE * attackerTroops &&
|
||||
attackerNonUndeadUnitCount >= criticalTileCoords.size();
|
||||
if (waterCrossingCouldAffectStrategy && HasEnoughNonUndeadAttackersWithoutWaterCrossing(
|
||||
gameState,
|
||||
criticalTileCoords,
|
||||
landRegions,
|
||||
apdCache,
|
||||
battalionTypeGetter,
|
||||
criticalTileCoords.size())) {
|
||||
return FleeStrategy;
|
||||
}
|
||||
|
||||
if (attackerNonUndeadUnitCount >= criticalTileCoords.size()) {
|
||||
chosenStrategy = HoldCastlesStrategy;
|
||||
} else if (defenderTroops > MINIMUM_RATIO_FOR_DEFENDER_TO_HOLD * attackerTroops) {
|
||||
chosenStrategy = HoldCastlesStrategy;
|
||||
|
||||
@@ -22,6 +22,7 @@ public:
|
||||
const CoordsSet& criticalTileCoords,
|
||||
int maxRounds,
|
||||
const APDCache& apdCache,
|
||||
const LandRegions& landRegions,
|
||||
const BattalionTypeGetter& battalionTypeGetter) -> AIStrategy;
|
||||
};
|
||||
} // namespace shardok
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
#include "AIThreadPool.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace shardok {
|
||||
|
||||
namespace {
|
||||
|
||||
// Keep the process responsive to gRPC and command handling while still giving lookahead enough
|
||||
// parallelism to saturate typical production hosts.
|
||||
constexpr size_t kMaximumAIWorkerCount = 12;
|
||||
|
||||
} // namespace
|
||||
|
||||
thread_local const AIThreadPool* AIThreadPool::currentPool_ = nullptr;
|
||||
|
||||
AIThreadPool::AIThreadPool(const size_t workerCount) {
|
||||
const size_t normalizedWorkerCount = std::max<size_t>(1, workerCount);
|
||||
workers_.reserve(normalizedWorkerCount);
|
||||
try {
|
||||
for (size_t index = 0; index < normalizedWorkerCount; ++index) {
|
||||
workers_.emplace_back([this] { RunWorker(); });
|
||||
}
|
||||
} catch (...) {
|
||||
StopAndJoin();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
AIThreadPool::~AIThreadPool() { StopAndJoin(); }
|
||||
|
||||
void AIThreadPool::RunWorker() {
|
||||
currentPool_ = this;
|
||||
while (true) {
|
||||
std::function<void()> task;
|
||||
{
|
||||
std::unique_lock lock(mutex_);
|
||||
condition_.wait(lock, [this] { return stopping_ || !tasks_.empty(); });
|
||||
if (stopping_ && tasks_.empty()) { break; }
|
||||
task = std::move(tasks_.front());
|
||||
tasks_.pop();
|
||||
}
|
||||
task();
|
||||
}
|
||||
currentPool_ = nullptr;
|
||||
}
|
||||
|
||||
void AIThreadPool::StopAndJoin() {
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
stopping_ = true;
|
||||
}
|
||||
condition_.notify_all();
|
||||
for (auto& worker : workers_) {
|
||||
if (worker.joinable()) { worker.join(); }
|
||||
}
|
||||
}
|
||||
|
||||
auto AIWorkerCountForHardwareConcurrency(const unsigned int hardwareConcurrency) -> size_t {
|
||||
if (hardwareConcurrency <= 1) { return 1; }
|
||||
return std::min(kMaximumAIWorkerCount, static_cast<size_t>(hardwareConcurrency - 1));
|
||||
}
|
||||
|
||||
auto SharedAIThreadPool() -> AIThreadPool& {
|
||||
static AIThreadPool pool(
|
||||
AIWorkerCountForHardwareConcurrency(std::thread::hardware_concurrency()));
|
||||
return pool;
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
@@ -0,0 +1,79 @@
|
||||
//
|
||||
// Fixed-size worker pool for iterative-deepening lookahead.
|
||||
//
|
||||
|
||||
#ifndef EAGLE0_SHARDOK_AI_AI_THREAD_POOL_HPP
|
||||
#define EAGLE0_SHARDOK_AI_AI_THREAD_POOL_HPP
|
||||
|
||||
#include <condition_variable>
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <queue>
|
||||
#include <stdexcept>
|
||||
#include <thread>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace shardok {
|
||||
|
||||
class AIThreadPool {
|
||||
public:
|
||||
explicit AIThreadPool(size_t workerCount);
|
||||
~AIThreadPool();
|
||||
|
||||
AIThreadPool(const AIThreadPool&) = delete;
|
||||
auto operator=(const AIThreadPool&) -> AIThreadPool& = delete;
|
||||
AIThreadPool(AIThreadPool&&) = delete;
|
||||
auto operator=(AIThreadPool&&) -> AIThreadPool& = delete;
|
||||
|
||||
template<typename Function>
|
||||
[[nodiscard]] auto Submit(Function&& function)
|
||||
-> std::future<std::invoke_result_t<std::decay_t<Function>&>> {
|
||||
using Result = std::invoke_result_t<std::decay_t<Function>&>;
|
||||
|
||||
auto task =
|
||||
std::make_shared<std::packaged_task<Result()>>(std::forward<Function>(function));
|
||||
auto result = task->get_future();
|
||||
|
||||
// A worker must never enqueue work and then wait for the same bounded pool. Running
|
||||
// reentrant submissions inline makes that safe even if recursive search scheduling
|
||||
// changes in the future.
|
||||
if (currentPool_ == this) {
|
||||
(*task)();
|
||||
return result;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
if (stopping_) { throw std::runtime_error("Submit on stopped AIThreadPool"); }
|
||||
tasks_.emplace([task] { (*task)(); });
|
||||
}
|
||||
condition_.notify_one();
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto WorkerCount() const -> size_t { return workers_.size(); }
|
||||
|
||||
private:
|
||||
void RunWorker();
|
||||
void StopAndJoin();
|
||||
|
||||
static thread_local const AIThreadPool* currentPool_;
|
||||
|
||||
std::vector<std::thread> workers_;
|
||||
std::queue<std::function<void()>> tasks_;
|
||||
std::mutex mutex_;
|
||||
std::condition_variable condition_;
|
||||
bool stopping_ = false;
|
||||
};
|
||||
|
||||
[[nodiscard]] auto AIWorkerCountForHardwareConcurrency(unsigned int hardwareConcurrency) -> size_t;
|
||||
[[nodiscard]] auto SharedAIThreadPool() -> AIThreadPool&;
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
#endif // EAGLE0_SHARDOK_AI_AI_THREAD_POOL_HPP
|
||||
@@ -4,53 +4,251 @@
|
||||
|
||||
#include "AIWaterCrossingCalculator.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <optional>
|
||||
|
||||
#include "AIMinimumDistanceAndTarget.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/HexMapHelpers.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/HexMapHasher.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/LandRegions.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"
|
||||
|
||||
namespace shardok {
|
||||
namespace {
|
||||
|
||||
constexpr int kMaxWaterCrossingDistance = 1000;
|
||||
constexpr double kCrossingCreatorDistanceMultiplier = 100.0;
|
||||
|
||||
auto RecomputeModifierHash(HexMap *hexMap) -> MapId {
|
||||
hexMap->mutate_modifier_hash(GetModifierHash(hexMap));
|
||||
return ActionPointDistancesCache::GetMapId(hexMap);
|
||||
}
|
||||
|
||||
auto HasNonWaterFire(const HexMap *hexMap) -> bool {
|
||||
return std::ranges::any_of(*hexMap->terrain(), [](const auto *terrain) {
|
||||
return !IsWater(terrain->type()) && terrain->modifier().fire().present();
|
||||
});
|
||||
}
|
||||
|
||||
auto StaticRegionsAreConclusiveFor(const BattalionType &battalionType) -> bool {
|
||||
return battalionType.costToEnterWater.type == ActionCost::impossible &&
|
||||
battalionType.costToEnterMountain.type == ActionCost::impossible;
|
||||
}
|
||||
|
||||
auto CanReachWithDistances(
|
||||
const Coords &origin,
|
||||
const Coords &destination,
|
||||
const ActionPointDistances *distances) -> bool {
|
||||
return distances->Distance(origin, destination) != ActionPointDistances::IMPOSSIBLE;
|
||||
}
|
||||
|
||||
auto CanReachAllWithDistances(
|
||||
const Coords &origin,
|
||||
const CoordsSet &destinations,
|
||||
const ActionPointDistances *distances) -> bool {
|
||||
return std::ranges::all_of(destinations, [&](const Coords &destination) {
|
||||
return CanReachWithDistances(origin, destination, distances);
|
||||
});
|
||||
}
|
||||
|
||||
class EffectiveDestinations {
|
||||
private:
|
||||
const EffectiveLandRegions &effectiveLandRegions;
|
||||
std::optional<Coords> representative;
|
||||
bool shareRegion;
|
||||
|
||||
public:
|
||||
EffectiveDestinations(
|
||||
const EffectiveLandRegions &effectiveLandRegions,
|
||||
const CoordsSet &destinations)
|
||||
: effectiveLandRegions(effectiveLandRegions),
|
||||
shareRegion(true) {
|
||||
for (const Coords &destination : destinations) {
|
||||
if (!representative.has_value()) {
|
||||
representative = destination;
|
||||
} else if (!effectiveLandRegions.DefinitelyConnectedWithoutCreatingCrossing(
|
||||
*representative,
|
||||
destination)) {
|
||||
shareRegion = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] auto DefinitelyReachableFrom(const Coords &origin) const -> bool {
|
||||
return !representative.has_value() ||
|
||||
(shareRegion && effectiveLandRegions.DefinitelyConnectedWithoutCreatingCrossing(
|
||||
origin,
|
||||
*representative));
|
||||
}
|
||||
};
|
||||
|
||||
auto DefinitelyReachesAllWithoutCreatingCrossing(
|
||||
const HexMap *map,
|
||||
const net::eagle0::shardok::storage::fb::Unit *unit,
|
||||
const EffectiveDestinations &effectiveDestinations) -> bool {
|
||||
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) {
|
||||
return effectiveDestinations.DefinitelyReachableFrom(unit->location());
|
||||
}
|
||||
|
||||
const auto *startingPositions =
|
||||
map->attacker_starting_positions()->Get(unit->starting_position_index())->positions();
|
||||
return std::ranges::all_of(*startingPositions, [&](const Coords *position) {
|
||||
return effectiveDestinations.DefinitelyReachableFrom(*position);
|
||||
});
|
||||
}
|
||||
|
||||
auto CanReachAllWithoutWater(
|
||||
const HexMap *map,
|
||||
const net::eagle0::shardok::storage::fb::Unit *unit,
|
||||
const CoordsSet &destinations,
|
||||
const ActionPointDistances *distances) -> bool {
|
||||
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) {
|
||||
return CanReachAllWithDistances(unit->location(), destinations, distances);
|
||||
}
|
||||
|
||||
const auto *startingPositions =
|
||||
map->attacker_starting_positions()->Get(unit->starting_position_index())->positions();
|
||||
return std::ranges::all_of(*startingPositions, [&](const Coords *position) {
|
||||
return CanReachAllWithDistances(*position, destinations, distances);
|
||||
});
|
||||
}
|
||||
|
||||
auto IsActiveNonUndeadAttacker(
|
||||
const GameState *gameState,
|
||||
const net::eagle0::shardok::storage::fb::Unit *unit) -> bool {
|
||||
if (unit->status() != net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
|
||||
unit->status() != net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT) {
|
||||
return false;
|
||||
}
|
||||
if (unit->battalion().type() == net::eagle0::shardok::storage::fb::BattalionTypeId_UNDEAD) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return std::ranges::any_of(*gameState->player_infos(), [&](const auto *playerInfo) {
|
||||
return playerInfo->player_id() == unit->player_id() && !playerInfo->is_defender();
|
||||
});
|
||||
}
|
||||
|
||||
auto CanCreateWaterCrossing(
|
||||
const net::eagle0::shardok::storage::fb::Unit *unit,
|
||||
const BattalionTypeGetter &battalionTypeGetter) -> bool {
|
||||
if (!unit->has_attached_hero()) return false;
|
||||
|
||||
const auto profession = unit->attached_hero().profession_info().profession();
|
||||
const auto &battalionType = battalionTypeGetter(unit->battalion().type());
|
||||
|
||||
return profession == net::eagle0::shardok::storage::fb::Profession_ENGINEER ||
|
||||
(profession == net::eagle0::shardok::storage::fb::Profession_MAGE &&
|
||||
battalionType->allowsCasting);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto UnitIdsRequiringWaterCrossing(
|
||||
const GameStateW &gameState,
|
||||
const PlayerId pid,
|
||||
const CoordsSet &destinations,
|
||||
const LandRegions &landRegions,
|
||||
const APDCache &apdCache,
|
||||
const BattalionTypeGetter &battalionTypeGetter) -> vector<UnitId> {
|
||||
// Put out all the fires, except on bridges
|
||||
fb::HexMapW mapCopy = fb::CopyHexMap(gameState->hex_map());
|
||||
for (uint32_t index = 0; index < mapCopy->terrain()->size(); ++index) {
|
||||
if (IsWater(mapCopy->terrain()->Get(index)->type())) continue;
|
||||
// const_cast is safe because we own the mutable buffer (mapCopy)
|
||||
const_cast<net::eagle0::shardok::storage::fb::Terrain *>(
|
||||
mapCopy->mutable_terrain()->GetMutableObject(index))
|
||||
->mutable_modifier()
|
||||
.mutable_fire()
|
||||
.mutate_present(false);
|
||||
}
|
||||
const GameState *state = gameState.Get();
|
||||
const auto *units = state->units();
|
||||
const HexMap *map = state->hex_map();
|
||||
const EffectiveLandRegions effectiveLandRegions(landRegions, map);
|
||||
const EffectiveDestinations effectiveDestinations(effectiveLandRegions, destinations);
|
||||
|
||||
const auto mapId = ActionPointDistancesCache::GetMapId(mapCopy);
|
||||
|
||||
vector<UnitId> unitIdsToReturn{};
|
||||
|
||||
// Set this to -1 for unchecked, 0 for does not need crossing, 1 for does need crossing
|
||||
std::vector<int> startingPositionIndices(9, -1);
|
||||
for (const auto *unit : *gameState->units()) {
|
||||
std::vector<const net::eagle0::shardok::storage::fb::Unit *> unitsToCheck{};
|
||||
const auto queueForExactCheck = [&](const auto *unit) {
|
||||
if (unitsToCheck.empty()) { unitsToCheck.reserve(units->size()); }
|
||||
unitsToCheck.push_back(unit);
|
||||
};
|
||||
std::array<int8_t, 9> startingPositionIndices{};
|
||||
startingPositionIndices.fill(-1);
|
||||
for (const auto *unit : *units) {
|
||||
if (unit->player_id() != pid) continue;
|
||||
|
||||
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) {
|
||||
if (!effectiveDestinations.DefinitelyReachableFrom(unit->location())) {
|
||||
queueForExactCheck(unit);
|
||||
}
|
||||
} else if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT) {
|
||||
if (startingPositionIndices[unit->starting_position_index()] == -1) {
|
||||
startingPositionIndices[unit->starting_position_index()] = 0;
|
||||
const auto *startingPositions = map->attacker_starting_positions()
|
||||
->Get(unit->starting_position_index())
|
||||
->positions();
|
||||
for (const Coords *position : *startingPositions) {
|
||||
if (!effectiveDestinations.DefinitelyReachableFrom(*position)) {
|
||||
startingPositionIndices[unit->starting_position_index()] = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (startingPositionIndices[unit->starting_position_index()] == 1) {
|
||||
queueForExactCheck(unit);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (unitsToCheck.empty()) return {};
|
||||
|
||||
MapId mapId = ActionPointDistancesCache::GetMapId(map);
|
||||
|
||||
fb::HexMapW fireClearedMap;
|
||||
bool mapPreparedForDistances = false;
|
||||
std::array<const ActionPointDistances *, BattalionTypeId::BattalionTypeId_MAX + 1>
|
||||
distancesByBattalionType{};
|
||||
const auto distancesFor = [&](const BattalionTypeSPtr &battalionType) {
|
||||
if (!mapPreparedForDistances) {
|
||||
mapPreparedForDistances = true;
|
||||
|
||||
// Most states have no relevant fire, so avoid copying the entire map unless exact
|
||||
// distances need it.
|
||||
if (HasNonWaterFire(map)) {
|
||||
fireClearedMap = fb::CopyHexMap(map);
|
||||
for (uint32_t index = 0; index < fireClearedMap->terrain()->size(); ++index) {
|
||||
if (IsWater(fireClearedMap->terrain()->Get(index)->type())) continue;
|
||||
// const_cast is safe because we own the mutable buffer (fireClearedMap)
|
||||
const_cast<net::eagle0::shardok::storage::fb::Terrain *>(
|
||||
fireClearedMap->mutable_terrain()->GetMutableObject(index))
|
||||
->mutable_modifier()
|
||||
.mutable_fire()
|
||||
.mutate_present(false);
|
||||
}
|
||||
|
||||
map = fireClearedMap.Get();
|
||||
mapId = RecomputeModifierHash(fireClearedMap.Get());
|
||||
}
|
||||
}
|
||||
|
||||
auto &distances = distancesByBattalionType[battalionType->typeId];
|
||||
if (distances == nullptr) {
|
||||
distances = apdCache->GetRaw(map, mapId, battalionType, false);
|
||||
}
|
||||
return distances;
|
||||
};
|
||||
|
||||
vector<UnitId> unitIdsToReturn{};
|
||||
unitIdsToReturn.reserve(unitsToCheck.size());
|
||||
|
||||
// The effective-region pass above removed every unit that can reach every destination through
|
||||
// static land and current bridges. Resolve full APDs only for battalions that can traverse raw
|
||||
// water or mountains.
|
||||
startingPositionIndices.fill(-1);
|
||||
for (const auto *unit : unitsToCheck) {
|
||||
const auto &battType = battalionTypeGetter(unit->battalion().type());
|
||||
|
||||
if (StaticRegionsAreConclusiveFor(*battType)) {
|
||||
unitIdsToReturn.push_back(unit->unit_id());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) {
|
||||
const auto *distances = distancesFor(battType);
|
||||
for (const Coords &destination : destinations) {
|
||||
if (!CanReach(unit->location(), destination, mapCopy, mapId, apdCache, battType)) {
|
||||
if (!CanReachWithDistances(unit->location(), destination, distances)) {
|
||||
unitIdsToReturn.push_back(unit->unit_id());
|
||||
break;
|
||||
}
|
||||
@@ -58,14 +256,14 @@ auto UnitIdsRequiringWaterCrossing(
|
||||
} else if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT) {
|
||||
if (startingPositionIndices[unit->starting_position_index()] == -1) {
|
||||
startingPositionIndices[unit->starting_position_index()] = 0;
|
||||
const auto *startingPositions = gameState->hex_map()
|
||||
->attacker_starting_positions()
|
||||
const auto *distances = distancesFor(battType);
|
||||
const auto *startingPositions = map->attacker_starting_positions()
|
||||
->Get(unit->starting_position_index())
|
||||
->positions();
|
||||
|
||||
for (const Coords *pos : *startingPositions) {
|
||||
for (const Coords &destination : destinations) {
|
||||
if (!CanReach(*pos, destination, mapCopy, mapId, apdCache, battType)) {
|
||||
if (!CanReachWithDistances(*pos, destination, distances)) {
|
||||
startingPositionIndices[unit->starting_position_index()] = 1;
|
||||
break;
|
||||
}
|
||||
@@ -83,6 +281,87 @@ auto UnitIdsRequiringWaterCrossing(
|
||||
return unitIdsToReturn;
|
||||
}
|
||||
|
||||
auto HasEnoughNonUndeadAttackersWithoutWaterCrossing(
|
||||
const GameStateW &gameState,
|
||||
const CoordsSet &destinations,
|
||||
const LandRegions &landRegions,
|
||||
const APDCache &apdCache,
|
||||
const BattalionTypeGetter &battalionTypeGetter,
|
||||
const std::size_t requiredUnitCount) -> bool {
|
||||
if (requiredUnitCount == 0) return true;
|
||||
|
||||
const GameState *state = gameState.Get();
|
||||
const auto *units = state->units();
|
||||
const HexMap *baseMap = state->hex_map();
|
||||
const EffectiveLandRegions effectiveLandRegions(landRegions, baseMap);
|
||||
const EffectiveDestinations effectiveDestinations(effectiveLandRegions, destinations);
|
||||
|
||||
std::size_t eligibleUnitCount = 0;
|
||||
std::size_t reachableUnitCount = 0;
|
||||
for (const auto *unit : *units) {
|
||||
if (!IsActiveNonUndeadAttacker(state, unit)) continue;
|
||||
|
||||
++eligibleUnitCount;
|
||||
if (DefinitelyReachesAllWithoutCreatingCrossing(baseMap, unit, effectiveDestinations) &&
|
||||
++reachableUnitCount >= requiredUnitCount) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (eligibleUnitCount < requiredUnitCount) return false;
|
||||
|
||||
const HexMap *map = baseMap;
|
||||
MapId mapId = ActionPointDistancesCache::GetMapId(baseMap);
|
||||
fb::HexMapW fireClearedMap;
|
||||
bool mapPreparedForDistances = false;
|
||||
std::array<const ActionPointDistances *, BattalionTypeId::BattalionTypeId_MAX + 1>
|
||||
distancesByBattalionType{};
|
||||
const auto distancesFor = [&](const BattalionTypeSPtr &battalionType) {
|
||||
if (!mapPreparedForDistances) {
|
||||
mapPreparedForDistances = true;
|
||||
|
||||
if (HasNonWaterFire(map)) {
|
||||
fireClearedMap = fb::CopyHexMap(map);
|
||||
for (uint32_t index = 0; index < fireClearedMap->terrain()->size(); ++index) {
|
||||
if (IsWater(fireClearedMap->terrain()->Get(index)->type())) continue;
|
||||
// const_cast is safe because we own the mutable buffer (fireClearedMap)
|
||||
const_cast<net::eagle0::shardok::storage::fb::Terrain *>(
|
||||
fireClearedMap->mutable_terrain()->GetMutableObject(index))
|
||||
->mutable_modifier()
|
||||
.mutable_fire()
|
||||
.mutate_present(false);
|
||||
}
|
||||
|
||||
map = fireClearedMap.Get();
|
||||
mapId = RecomputeModifierHash(fireClearedMap.Get());
|
||||
}
|
||||
}
|
||||
|
||||
auto &distances = distancesByBattalionType[battalionType->typeId];
|
||||
if (distances == nullptr) {
|
||||
distances = apdCache->GetRaw(map, mapId, battalionType, false);
|
||||
}
|
||||
return distances;
|
||||
};
|
||||
|
||||
for (const auto *unit : *units) {
|
||||
if (!IsActiveNonUndeadAttacker(state, unit)) continue;
|
||||
if (DefinitelyReachesAllWithoutCreatingCrossing(baseMap, unit, effectiveDestinations)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto &battalionType = battalionTypeGetter(unit->battalion().type());
|
||||
if (StaticRegionsAreConclusiveFor(*battalionType)) continue;
|
||||
|
||||
if (CanReachAllWithoutWater(map, unit, destinations, distancesFor(battalionType)) &&
|
||||
++reachableUnitCount >= requiredUnitCount) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
auto UnitIdsToCreateWaterCrossing(
|
||||
const GameStateW &gameState,
|
||||
const PlayerId pid,
|
||||
@@ -94,14 +373,8 @@ auto UnitIdsToCreateWaterCrossing(
|
||||
if (unit->status() != net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
|
||||
unit->status() != net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT)
|
||||
continue;
|
||||
if (!unit->has_attached_hero()) continue;
|
||||
|
||||
const auto profession = unit->attached_hero().profession_info().profession();
|
||||
const auto &battalionType = battalionTypeGetter(unit->battalion().type());
|
||||
|
||||
if (profession == net::eagle0::shardok::storage::fb::Profession_ENGINEER ||
|
||||
(profession == net::eagle0::shardok::storage::fb::Profession_MAGE &&
|
||||
battalionType->allowsCasting)) {
|
||||
if (CanCreateWaterCrossing(unit, battalionTypeGetter)) {
|
||||
unitIds.push_back(unit->unit_id());
|
||||
}
|
||||
}
|
||||
@@ -116,10 +389,10 @@ auto CanReach(
|
||||
const MapId &mapId,
|
||||
const APDCache &apdCache,
|
||||
const BattalionTypeSPtr &battalionType) -> bool {
|
||||
const DIST_T startingDistance =
|
||||
apdCache->GetRaw(hexMap, mapId, battalionType, false)->Distance(origin, destination);
|
||||
|
||||
return startingDistance != ActionPointDistances::IMPOSSIBLE;
|
||||
return CanReachWithDistances(
|
||||
origin,
|
||||
destination,
|
||||
apdCache->GetRaw(hexMap, mapId, battalionType, false));
|
||||
}
|
||||
|
||||
// Returns the *non*-water tiles from which you could bridge/freeze water to allow a unit to cross
|
||||
@@ -181,7 +454,7 @@ auto WaterCrossingTiles(
|
||||
terr->mutable_modifier().mutable_bridge().mutate_integrity(100.0);
|
||||
terr->mutable_modifier().mutable_fire().mutate_present(false);
|
||||
|
||||
auto hash = ActionPointDistancesCache::GetMapId(mapCopy);
|
||||
const MapId hash = RecomputeModifierHash(mapCopy.Get());
|
||||
|
||||
if (const auto *distances = apdCache->GetRaw(mapCopy, hash, battalionType, false);
|
||||
distances->Distance(origin, destination) != ActionPointDistances::IMPOSSIBLE) {
|
||||
@@ -274,6 +547,7 @@ auto WaterCrossingScore(
|
||||
const GameStateW &gameState,
|
||||
const CoordsSet &castleCoords,
|
||||
const CoordsSet &startCrossingFrom,
|
||||
const LandRegions &landRegions,
|
||||
const APDCache &apdCache) -> double {
|
||||
uint32_t castleClaimCount = 0;
|
||||
for (const auto *unit : *gameState->units()) {
|
||||
@@ -303,6 +577,7 @@ auto WaterCrossingScore(
|
||||
gameState,
|
||||
playerId,
|
||||
castleCoords,
|
||||
landRegions,
|
||||
apdCache,
|
||||
battalionTypeGetter);
|
||||
if (unitIdsRequiringCrossing.empty()) return kNoRequiredCrossingScore;
|
||||
@@ -314,6 +589,31 @@ auto WaterCrossingScore(
|
||||
|
||||
const auto mapId = ActionPointDistancesCache::GetMapId(gameState->hex_map());
|
||||
|
||||
// Put a big penalty on the distance for every unit that could create a crossing, on a single
|
||||
// saturating scale: unavailable (fled/captured/destroyed) and unplaced creators sit at the
|
||||
// maximum, and live distances (including IMPOSSIBLE) are clamped to it. Losing a crossing
|
||||
// creator therefore never scores better than any placed position.
|
||||
for (const auto *unit : *gameState->units()) {
|
||||
if (unit->player_id() != playerId) continue;
|
||||
if (!CanCreateWaterCrossing(unit, battalionTypeGetter)) continue;
|
||||
|
||||
const auto status = unit->status();
|
||||
const bool available =
|
||||
status == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT ||
|
||||
status == net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT;
|
||||
|
||||
int thisDistance = kMaxWaterCrossingDistance;
|
||||
if (available && unit->location().row() >= 0) {
|
||||
const auto &battalionType = battalionTypeGetter(unit->battalion().type());
|
||||
const auto *apd = apdCache->GetRaw(gameState->hex_map(), mapId, battalionType, false);
|
||||
thisDistance = std::min(
|
||||
MinimumDistance(apd, unit->location(), startCrossingFrom),
|
||||
kMaxWaterCrossingDistance);
|
||||
}
|
||||
|
||||
totalScore -= thisDistance * kCrossingCreatorDistanceMultiplier;
|
||||
}
|
||||
|
||||
if (unitIdsCreatingCrossing.empty()) {
|
||||
for (const UnitId uid : unitIdsRequiringCrossing) {
|
||||
const Unit *unit = gameState->units()->Get(uid);
|
||||
@@ -321,31 +621,15 @@ auto WaterCrossingScore(
|
||||
const Coords location = unit->location();
|
||||
const auto *apd = apdCache->GetRaw(gameState->hex_map(), mapId, battalionType, false);
|
||||
|
||||
const int thisDistance =
|
||||
location.row() < 0 ? 1000 : MinimumDistance(apd, location, startCrossingFrom);
|
||||
const int thisDistance = location.row() < 0
|
||||
? kMaxWaterCrossingDistance
|
||||
: MinimumDistance(apd, location, startCrossingFrom);
|
||||
totalScore -= thisDistance;
|
||||
}
|
||||
|
||||
return totalScore;
|
||||
}
|
||||
|
||||
// First put a big penalty on the distance for units that can create a crossing
|
||||
for (const UnitId uid : unitIdsCreatingCrossing) {
|
||||
const Unit *unit = gameState->units()->Get(uid);
|
||||
const auto &battalionType = battalionTypeGetter(unit->battalion().type());
|
||||
Coords location = unit->location();
|
||||
|
||||
int thisDistance;
|
||||
if (location.row() < 0) thisDistance = 1000;
|
||||
else {
|
||||
const auto *apd = apdCache->GetRaw(gameState->hex_map(), mapId, battalionType, false);
|
||||
|
||||
thisDistance = MinimumDistance(apd, location, startCrossingFrom);
|
||||
}
|
||||
|
||||
totalScore -= thisDistance * 100.0;
|
||||
}
|
||||
|
||||
// Now a smaller penalty for distance for units that need to cross, except if they block -- then
|
||||
// a large penalty
|
||||
for (const UnitId uid : unitIdsRequiringCrossing) {
|
||||
@@ -358,7 +642,7 @@ auto WaterCrossingScore(
|
||||
const auto *apd = apdCache->GetRaw(gameState->hex_map(), mapId, battalionType, false);
|
||||
|
||||
int thisDistance;
|
||||
if (location.row() < 0) thisDistance = 1000;
|
||||
if (location.row() < 0) thisDistance = kMaxWaterCrossingDistance;
|
||||
else { thisDistance = MinimumDistance(apd, location, startCrossingFrom); }
|
||||
|
||||
bool targetBlocks = false;
|
||||
|
||||
@@ -5,10 +5,13 @@
|
||||
#ifndef EAGLE0_SHARDOK_AI_AI_WATER_CROSSING_CALCULATOR_HPP
|
||||
#define EAGLE0_SHARDOK_AI_AI_WATER_CROSSING_CALCULATOR_HPP
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AICommonTypes.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistancesCache.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/LandRegions.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
|
||||
|
||||
@@ -35,9 +38,20 @@ static inline void AssertValid(const Coords& c, const HexMap* hexMap) {
|
||||
const GameStateW& gameState,
|
||||
PlayerId pid,
|
||||
const CoordsSet& destinations,
|
||||
const LandRegions& landRegions,
|
||||
const APDCache& apdCache,
|
||||
const BattalionTypeGetter& battalionTypeGetter) -> vector<UnitId>;
|
||||
|
||||
// Whether at least requiredUnitCount active, non-undead attacker units can reach every destination
|
||||
// without crossing water.
|
||||
[[nodiscard]] auto HasEnoughNonUndeadAttackersWithoutWaterCrossing(
|
||||
const GameStateW& gameState,
|
||||
const CoordsSet& destinations,
|
||||
const LandRegions& landRegions,
|
||||
const APDCache& apdCache,
|
||||
const BattalionTypeGetter& battalionTypeGetter,
|
||||
std::size_t requiredUnitCount) -> bool;
|
||||
|
||||
// Units belonging to the player that are capable of creating water crossings
|
||||
[[nodiscard]] auto UnitIdsToCreateWaterCrossing(
|
||||
const GameStateW& gameState,
|
||||
@@ -92,6 +106,7 @@ static inline void AssertValid(const Coords& c, const HexMap* hexMap) {
|
||||
const GameStateW& gameState,
|
||||
const CoordsSet& castleCoords,
|
||||
const CoordsSet& startCrossingFrom,
|
||||
const LandRegions& landRegions,
|
||||
const APDCache& apdCache) -> double;
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
@@ -4,132 +4,12 @@
|
||||
|
||||
#include "AIWaterCrossingCommandChooser.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <ranges>
|
||||
|
||||
#include "AIMinimumDistanceAndTarget.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIWaterCrossingCalculator.hpp"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
using Unit = net::eagle0::shardok::storage::fb::Unit;
|
||||
|
||||
constexpr ScoreValue kNoRequiredCrossingScore = std::numeric_limits<ScoreValue>::max();
|
||||
|
||||
[[nodiscard]] auto AIWaterCrossingCommandChooser::WaterCrossingScore(
|
||||
const BattalionTypeGetter &battalionTypeGetter,
|
||||
const GameStateW &gameState,
|
||||
const CoordsSet &castleCoords,
|
||||
const CoordsSet &startCrossingFrom) const -> ScoreValue {
|
||||
uint32_t castleClaimCount = 0;
|
||||
for (const auto *unit : *gameState->units()) {
|
||||
if (unit->player_id() != playerId) continue;
|
||||
const auto status = unit->status();
|
||||
if (status != net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
|
||||
status != net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT)
|
||||
continue;
|
||||
if (!unit->has_attached_hero()) continue;
|
||||
|
||||
++castleClaimCount;
|
||||
}
|
||||
|
||||
CoordsSet destinations = castleCoords;
|
||||
if (castleClaimCount < castleCoords.size()) {
|
||||
destinations = CoordsSet(gameState->hex_map());
|
||||
for (const auto *enemyUnit : *gameState->units()) {
|
||||
if (enemyUnit->player_id() == playerId) continue;
|
||||
const auto status = enemyUnit->status();
|
||||
if (status != net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) continue;
|
||||
AssertValid(enemyUnit->location(), gameState->hex_map());
|
||||
destinations.Add(enemyUnit->location());
|
||||
}
|
||||
}
|
||||
|
||||
const auto unitIdsRequiringCrossing = UnitIdsRequiringWaterCrossing(
|
||||
gameState,
|
||||
playerId,
|
||||
castleCoords,
|
||||
apdCache,
|
||||
battalionTypeGetter);
|
||||
if (unitIdsRequiringCrossing.empty()) return kNoRequiredCrossingScore;
|
||||
|
||||
const auto unitIdsCreatingCrossing =
|
||||
UnitIdsToCreateWaterCrossing(gameState, playerId, battalionTypeGetter);
|
||||
|
||||
ScoreValue totalScore = 0;
|
||||
|
||||
const auto mapId = ActionPointDistancesCache::GetMapId(gameState->hex_map());
|
||||
|
||||
if (unitIdsCreatingCrossing.empty()) {
|
||||
for (const UnitId uid : unitIdsRequiringCrossing) {
|
||||
const Unit *unit = gameState->units()->Get(uid);
|
||||
const auto &battalionType = battalionTypeGetter(unit->battalion().type());
|
||||
const Coords location = unit->location();
|
||||
const auto *apd = apdCache->GetRaw(gameState->hex_map(), mapId, battalionType, false);
|
||||
|
||||
const int thisDistance =
|
||||
location.row() < 0 ? 1000 : MinimumDistance(apd, location, startCrossingFrom);
|
||||
totalScore -= thisDistance;
|
||||
}
|
||||
|
||||
return totalScore;
|
||||
}
|
||||
|
||||
// First put a big penalty on the distance for units that can create a crossing
|
||||
for (const UnitId uid : unitIdsCreatingCrossing) {
|
||||
const Unit *unit = gameState->units()->Get(uid);
|
||||
const auto &battalionType = battalionTypeGetter(unit->battalion().type());
|
||||
Coords location = unit->location();
|
||||
|
||||
int thisDistance;
|
||||
if (location.row() < 0) thisDistance = 1000;
|
||||
else {
|
||||
const auto *apd = apdCache->GetRaw(gameState->hex_map(), mapId, battalionType, false);
|
||||
|
||||
thisDistance = MinimumDistance(apd, location, startCrossingFrom);
|
||||
}
|
||||
|
||||
totalScore -= thisDistance * 100.0;
|
||||
}
|
||||
|
||||
// Now a smaller penalty for distance for units that need to cross, except if they block -- then
|
||||
// a large penalty
|
||||
for (const UnitId uid : unitIdsRequiringCrossing) {
|
||||
// If this unit ID can also create a crossing, we already handled it
|
||||
if (std::ranges::contains(unitIdsCreatingCrossing, uid)) continue;
|
||||
|
||||
const Unit *unit = gameState->units()->Get(uid);
|
||||
const auto &battalionType = battalionTypeGetter(unit->battalion().type());
|
||||
Coords location = unit->location();
|
||||
const auto *apd = apdCache->GetRaw(gameState->hex_map(), mapId, battalionType, false);
|
||||
|
||||
int thisDistance;
|
||||
if (location.row() < 0) thisDistance = 1000;
|
||||
else { thisDistance = MinimumDistance(apd, location, startCrossingFrom); }
|
||||
|
||||
bool targetBlocks = false;
|
||||
// If we're not capable of creating a crossing, don't get in the way of somebody that is.
|
||||
for (const UnitId crossingUid : unitIdsCreatingCrossing) {
|
||||
const auto *crossingCapableUnit = gameState->units()->Get(crossingUid);
|
||||
|
||||
// Don't check for units that aren't yet placed
|
||||
if (crossingCapableUnit->location().row() < 0) continue;
|
||||
AssertValid(crossingCapableUnit->location(), gameState->hex_map());
|
||||
|
||||
if (thisDistance <
|
||||
MinimumDistance(apd, crossingCapableUnit->location(), startCrossingFrom)) {
|
||||
targetBlocks = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (targetBlocks) continue;
|
||||
|
||||
totalScore -= thisDistance;
|
||||
}
|
||||
|
||||
return totalScore;
|
||||
}
|
||||
|
||||
auto AIWaterCrossingCommandChooser::StartCrossingFrom(
|
||||
const BattalionTypeGetter &battalionTypeGetter,
|
||||
const GameStateW &gameState,
|
||||
@@ -164,6 +44,7 @@ auto AIWaterCrossingCommandChooser::StartCrossingFrom(
|
||||
gameState,
|
||||
playerId,
|
||||
castleCoords,
|
||||
landRegions,
|
||||
apdCache,
|
||||
battalionTypeGetter);
|
||||
if (unitIdsRequiringCrossing.empty()) return startCrossingFrom;
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistancesCache.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/LandRegions.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
|
||||
|
||||
@@ -18,28 +19,26 @@ namespace shardok {
|
||||
|
||||
using GameState = net::eagle0::shardok::storage::fb::GameState;
|
||||
using Unit = net::eagle0::shardok::storage::fb::Unit;
|
||||
using ScoreValue = double;
|
||||
|
||||
class AIWaterCrossingCommandChooser {
|
||||
private:
|
||||
const PlayerId playerId;
|
||||
const APDCache apdCache;
|
||||
const LandRegions &landRegions;
|
||||
|
||||
public:
|
||||
AIWaterCrossingCommandChooser(const PlayerId pid, APDCache apdCache)
|
||||
AIWaterCrossingCommandChooser(
|
||||
const PlayerId pid,
|
||||
APDCache apdCache,
|
||||
const LandRegions &landRegions)
|
||||
: playerId(pid),
|
||||
apdCache(std::move(apdCache)) {}
|
||||
apdCache(std::move(apdCache)),
|
||||
landRegions(landRegions) {}
|
||||
|
||||
[[nodiscard]] auto StartCrossingFrom(
|
||||
const BattalionTypeGetter &battalionTypeGetter,
|
||||
const GameStateW &gameState,
|
||||
const CoordsSet &castleCoords) const -> CoordsSet;
|
||||
|
||||
[[nodiscard]] auto WaterCrossingScore(
|
||||
const BattalionTypeGetter &battalionTypeGetter,
|
||||
const GameStateW &gameState,
|
||||
const CoordsSet &castleCoords,
|
||||
const CoordsSet &startCrossingFrom) const -> ScoreValue;
|
||||
};
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
@@ -73,6 +73,7 @@ cc_library(
|
||||
],
|
||||
deps = [
|
||||
":ai_score_utilities",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/map:land_regions",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/map:terrain",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/settings:game_settings",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/util:hex_map_utils",
|
||||
@@ -194,6 +195,16 @@ cc_library(
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "ai_thread_pool",
|
||||
srcs = ["AIThreadPool.cpp"],
|
||||
hdrs = ["AIThreadPool.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = [
|
||||
"//src/test/cpp/net/eagle0/shardok/ai:__pkg__",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "ai_command_evaluator",
|
||||
srcs = ["AICommandEvaluator.cpp"],
|
||||
@@ -207,6 +218,7 @@ cc_library(
|
||||
":ai_command_filter",
|
||||
":ai_experiment_config",
|
||||
":ai_strategy",
|
||||
":ai_thread_pool",
|
||||
":transposition_table",
|
||||
"//src/main/cpp/net/eagle0/common:sequence_random_generator",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai/score:ai_score_calculator_interface",
|
||||
@@ -306,6 +318,7 @@ cc_library(
|
||||
"//src/main/cpp/net/eagle0/shardok/library/action_point_distances",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/action_point_distances:action_point_distances_cache",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/fb_helpers:hex_map_helpers",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/map:land_regions",
|
||||
"//src/main/flatbuffer/net/eagle0/shardok/storage:game_state_cc_fbs",
|
||||
"//src/main/flatbuffer/net/eagle0/shardok/storage:hex_map_cc_fbs",
|
||||
],
|
||||
@@ -323,6 +336,7 @@ cc_library(
|
||||
"//src/main/cpp/net/eagle0/shardok/library:game_state_w",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/action_point_distances",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/action_point_distances:action_point_distances_cache",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/map:land_regions",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -404,3 +418,19 @@ cc_library(
|
||||
"@com_google_protobuf//:protobuf",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "starting_position_recommender",
|
||||
srcs = ["StartingPositionRecommender.cpp"],
|
||||
hdrs = ["StartingPositionRecommender.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
":ai_config",
|
||||
":shardok_ai_client",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:engine",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:shardok_c_types",
|
||||
"//src/main/protobuf/net/eagle0/shardok/api:placement_command_cc_proto",
|
||||
"//src/main/protobuf/net/eagle0/shardok/common:command_type_cc_proto",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -292,8 +292,6 @@ auto IterativeDeepeningAI::IterativeSearch(
|
||||
<< " < 100%\n";
|
||||
}
|
||||
|
||||
// Print TranspositionTable statistics
|
||||
g_transpositionTable.printStats();
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ ShardokAIClient::ShardokAIClient(
|
||||
aiAlgorithmType(aiAlgorithmType),
|
||||
scoringCalculatorType(scoringCalculatorType),
|
||||
alCache(std::make_unique<AttackLocationsCache>(hexMap, settings)),
|
||||
waterCrossingCommandChooser(playerId, apdCache),
|
||||
waterCrossingCommandChooser(playerId, apdCache, alCache->GetLandRegions()),
|
||||
mctsConfig(mctsConfig) {
|
||||
// Pre-generate the most common cache entries for better performance
|
||||
const auto mapId = ActionPointDistancesCache::GetMapId(hexMap);
|
||||
@@ -137,15 +137,20 @@ void CheckCommand(const CommandSPtr &realCommand, const CommandSPtr &guessedComm
|
||||
auto ShardokAIClient::StandardChooseCommandIndex(
|
||||
const GameSettingsSPtr &settings,
|
||||
const GameStateW &guessedState,
|
||||
const CommandListSPtr &realAvailableCommands) const -> CommandChoiceResults {
|
||||
const CommandListSPtr &realAvailableCommands,
|
||||
const std::optional<std::chrono::milliseconds> timeLimit) const -> CommandChoiceResults {
|
||||
const auto settingsGetter = settings->GetGetter();
|
||||
const auto guessedEngine = ShardokEngine(settings, guessedState);
|
||||
const auto guessedCommands = guessedEngine.GetAvailableCommandsForAIPlayer(playerId);
|
||||
const auto commandCount = guessedCommands->size();
|
||||
|
||||
// Calculate time budget based on game situation using new dynamic per-command settings
|
||||
const auto timeBudget =
|
||||
auto timeBudget =
|
||||
CalculateTimeBudget(playerId, settings, guessedState, commandCount, isAllAiBattle);
|
||||
if (timeLimit.has_value()) {
|
||||
timeBudget.remainingBudget = *timeLimit;
|
||||
timeBudget.minDepthRequired = 1;
|
||||
}
|
||||
|
||||
// Configure MCTS based on proximity to enemy
|
||||
// When far from enemy: use AVERAGING with maxPlayerFlips=0 (single-player lookahead)
|
||||
@@ -297,6 +302,7 @@ auto ShardokAIClient::StandardChooseCommandIndex(
|
||||
castleCoords,
|
||||
maxRounds,
|
||||
apdCache,
|
||||
alCache->GetLandRegions(),
|
||||
battalionTypeGetter)
|
||||
: AIAttackerStrategySelector::BestAttackerStrategy(
|
||||
playerId,
|
||||
@@ -398,7 +404,8 @@ auto ShardokAIClient::StandardChooseCommandIndex(
|
||||
auto ShardokAIClient::LateRoundAttackerChooseCommandIndex(
|
||||
const GameSettingsSPtr &settings,
|
||||
const GameStateW &guessedState,
|
||||
const CommandListSPtr &realAvailableCommands) const -> CommandChoiceResults {
|
||||
const CommandListSPtr &realAvailableCommands,
|
||||
const std::optional<std::chrono::milliseconds> timeLimit) const -> CommandChoiceResults {
|
||||
if (const auto dismissCommand = std::ranges::find_if(
|
||||
*realAvailableCommands,
|
||||
[](const CommandSPtr &cmd) {
|
||||
@@ -406,7 +413,7 @@ auto ShardokAIClient::LateRoundAttackerChooseCommandIndex(
|
||||
net::eagle0::shardok::common::DISMISS_UNIT_COMMAND;
|
||||
});
|
||||
dismissCommand == realAvailableCommands->end()) {
|
||||
return StandardChooseCommandIndex(settings, guessedState, realAvailableCommands);
|
||||
return StandardChooseCommandIndex(settings, guessedState, realAvailableCommands, timeLimit);
|
||||
} else {
|
||||
CommandChoiceResults results{};
|
||||
results.chosenIndex =
|
||||
@@ -423,14 +430,19 @@ auto ShardokAIClient::LateRoundAttackerChooseCommandIndex(
|
||||
auto ShardokAIClient::FinalRoundAttackerChooseCommandIndex(
|
||||
const GameSettingsSPtr &settings,
|
||||
const GameStateW &guessedState,
|
||||
const CommandListSPtr &realAvailableCommands) const -> CommandChoiceResults {
|
||||
const CommandListSPtr &realAvailableCommands,
|
||||
const std::optional<std::chrono::milliseconds> timeLimit) const -> CommandChoiceResults {
|
||||
const auto fleeCommand =
|
||||
std::ranges::find_if(*realAvailableCommands, [](const CommandSPtr &cmd) {
|
||||
return cmd->GetCommandType() == net::eagle0::shardok::common::FLEE_COMMAND;
|
||||
});
|
||||
|
||||
if (fleeCommand == realAvailableCommands->end()) {
|
||||
return LateRoundAttackerChooseCommandIndex(settings, guessedState, realAvailableCommands);
|
||||
return LateRoundAttackerChooseCommandIndex(
|
||||
settings,
|
||||
guessedState,
|
||||
realAvailableCommands,
|
||||
timeLimit);
|
||||
}
|
||||
|
||||
// Extract values directly from settings for flee decision evaluation
|
||||
@@ -460,14 +472,15 @@ auto ShardokAIClient::FinalRoundAttackerChooseCommandIndex(
|
||||
return results;
|
||||
} else {
|
||||
// Fight instead of flee
|
||||
return StandardChooseCommandIndex(settings, guessedState, realAvailableCommands);
|
||||
return StandardChooseCommandIndex(settings, guessedState, realAvailableCommands, timeLimit);
|
||||
}
|
||||
}
|
||||
|
||||
auto ShardokAIClient::ChooseCommandIndex(
|
||||
const GameSettingsSPtr &settings,
|
||||
const GameStateView &gsv,
|
||||
const CommandListSPtr &realAvailableCommands) const -> CommandChoiceResults {
|
||||
const CommandListSPtr &realAvailableCommands,
|
||||
const std::optional<std::chrono::milliseconds> timeLimit) const -> CommandChoiceResults {
|
||||
static std::array<std::atomic<int>, net::eagle0::shardok::common::CommandType_MAX + 1>
|
||||
typeChosenCount;
|
||||
static std::atomic<int> totalChoices = 0;
|
||||
@@ -479,13 +492,23 @@ auto ShardokAIClient::ChooseCommandIndex(
|
||||
|
||||
if (const int roundsRemaining = RoundsRemaining(settings, gsv);
|
||||
!isDefender && roundsRemaining <= 1) {
|
||||
results =
|
||||
FinalRoundAttackerChooseCommandIndex(settings, guessedState, realAvailableCommands);
|
||||
results = FinalRoundAttackerChooseCommandIndex(
|
||||
settings,
|
||||
guessedState,
|
||||
realAvailableCommands,
|
||||
timeLimit);
|
||||
} else if (!isDefender && roundsRemaining <= 3) {
|
||||
results =
|
||||
LateRoundAttackerChooseCommandIndex(settings, guessedState, realAvailableCommands);
|
||||
results = LateRoundAttackerChooseCommandIndex(
|
||||
settings,
|
||||
guessedState,
|
||||
realAvailableCommands,
|
||||
timeLimit);
|
||||
} else {
|
||||
results = StandardChooseCommandIndex(settings, guessedState, realAvailableCommands);
|
||||
results = StandardChooseCommandIndex(
|
||||
settings,
|
||||
guessedState,
|
||||
realAvailableCommands,
|
||||
timeLimit);
|
||||
}
|
||||
|
||||
const auto chosenType = (*realAvailableCommands)[results.chosenIndex]->GetCommandType();
|
||||
@@ -526,7 +549,7 @@ auto ShardokAIClient::ChooseCommandIndex(const ShardokEngine &engine) const
|
||||
const auto &settings = engine.GetGameSettings();
|
||||
const auto &gsv = engine.GetGameStateView(GetPlayerId());
|
||||
|
||||
const auto results = ChooseCommandIndex(settings, gsv, availableCommands);
|
||||
const auto results = ChooseCommandIndex(settings, gsv, availableCommands, std::nullopt);
|
||||
apdCache->ConsolidateThreadLocalCache_Racy();
|
||||
return results;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
#ifndef EAGLE0_SHARDOK_AI_SHARDOK_AI_CLIENT_HPP
|
||||
#define EAGLE0_SHARDOK_AI_SHARDOK_AI_CLIENT_HPP
|
||||
|
||||
#include <chrono>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/common/RandomGenerator.hpp"
|
||||
@@ -56,15 +58,18 @@ private:
|
||||
[[nodiscard]] auto StandardChooseCommandIndex(
|
||||
const GameSettingsSPtr& settings,
|
||||
const GameStateW& guessedState,
|
||||
const CommandListSPtr& realAvailableCommands) const -> CommandChoiceResults;
|
||||
const CommandListSPtr& realAvailableCommands,
|
||||
std::optional<std::chrono::milliseconds> timeLimit) const -> CommandChoiceResults;
|
||||
[[nodiscard]] auto LateRoundAttackerChooseCommandIndex(
|
||||
const GameSettingsSPtr& settings,
|
||||
const GameStateW& guessedState,
|
||||
const CommandListSPtr& realAvailableCommands) const -> CommandChoiceResults;
|
||||
const CommandListSPtr& realAvailableCommands,
|
||||
std::optional<std::chrono::milliseconds> timeLimit) const -> CommandChoiceResults;
|
||||
[[nodiscard]] auto FinalRoundAttackerChooseCommandIndex(
|
||||
const GameSettingsSPtr& settings,
|
||||
const GameStateW& guessedState,
|
||||
const CommandListSPtr& realAvailableCommands) const -> CommandChoiceResults;
|
||||
const CommandListSPtr& realAvailableCommands,
|
||||
std::optional<std::chrono::milliseconds> timeLimit) const -> CommandChoiceResults;
|
||||
|
||||
public:
|
||||
explicit ShardokAIClient(
|
||||
@@ -87,7 +92,8 @@ public:
|
||||
[[nodiscard]] auto ChooseCommandIndex(
|
||||
const GameSettingsSPtr& settings,
|
||||
const net::eagle0::shardok::api::GameStateView& gsv,
|
||||
const CommandListSPtr& realAvailableCommands) const -> CommandChoiceResults;
|
||||
const CommandListSPtr& realAvailableCommands,
|
||||
std::optional<std::chrono::milliseconds> timeLimit) const -> CommandChoiceResults;
|
||||
|
||||
// MCTS configuration methods (only relevant when using MCTS algorithm)
|
||||
[[nodiscard]] auto GetMCTSConfig() const -> const mcts::MCTSConfig& { return mctsConfig; }
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
//
|
||||
// Created by Codex on 7/14/26.
|
||||
//
|
||||
|
||||
#include "StartingPositionRecommender.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <optional>
|
||||
#include <ranges>
|
||||
|
||||
#include "AIConfig.hpp"
|
||||
#include "ShardokAIClient.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/common/command_type.pb.h"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
using net::eagle0::shardok::api::PlacementCommand;
|
||||
using net::eagle0::shardok::common::CommandType;
|
||||
|
||||
namespace {
|
||||
|
||||
struct TutorialPlacement {
|
||||
UnitId unitId;
|
||||
int row;
|
||||
int column;
|
||||
};
|
||||
|
||||
auto RecommendTutorialStartingPositions(const ShardokEngine& engine, const PlayerId playerId)
|
||||
-> std::optional<std::vector<PlacementCommand>> {
|
||||
const auto* units = engine.GetCurrentGameState()->units();
|
||||
const auto isUnplacedPlayerUnit = [playerId](const auto* unit) {
|
||||
return unit->player_id() == playerId &&
|
||||
unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT;
|
||||
};
|
||||
if (std::ranges::count_if(*units, isUnplacedPlayerUnit) != 3) return std::nullopt;
|
||||
|
||||
const auto longbowmen = std::ranges::find_if(*units, [playerId](const auto* unit) {
|
||||
return unit->player_id() == playerId &&
|
||||
unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT &&
|
||||
unit->battalion().type() ==
|
||||
net::eagle0::shardok::storage::fb::BattalionTypeId_LONGBOWMEN;
|
||||
});
|
||||
const auto sadar = std::ranges::find_if(*units, [playerId](const auto* unit) {
|
||||
return unit->player_id() == playerId &&
|
||||
unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT &&
|
||||
unit->attached_hero().profession_info().profession() ==
|
||||
net::eagle0::shardok::storage::fb::Profession_CHAMPION;
|
||||
});
|
||||
const auto marek =
|
||||
std::ranges::find_if(*units, [playerId, &units, &longbowmen, &sadar](const auto* unit) {
|
||||
return unit->player_id() == playerId &&
|
||||
unit->status() ==
|
||||
net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT &&
|
||||
(longbowmen == units->end() ||
|
||||
unit->unit_id() != (*longbowmen)->unit_id()) &&
|
||||
(sadar == units->end() || unit->unit_id() != (*sadar)->unit_id());
|
||||
});
|
||||
if (longbowmen == units->end() || sadar == units->end() || marek == units->end()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
if ((*longbowmen)->unit_id() == (*sadar)->unit_id()) return std::nullopt;
|
||||
|
||||
const std::array desiredPlacements{
|
||||
TutorialPlacement{(*longbowmen)->unit_id(), 6, 12},
|
||||
TutorialPlacement{(*sadar)->unit_id(), 8, 11},
|
||||
TutorialPlacement{(*marek)->unit_id(), 10, 11}};
|
||||
ShardokEngine simulatedEngine(engine, false);
|
||||
std::vector<PlacementCommand> recommendations;
|
||||
recommendations.reserve(desiredPlacements.size());
|
||||
|
||||
for (const auto& placement : desiredPlacements) {
|
||||
const auto commands = simulatedEngine.GetAvailableCommandsForAIPlayer(playerId);
|
||||
const auto command = std::ranges::find_if(*commands, [&placement](const auto& candidate) {
|
||||
return candidate->GetActorUnitId() == placement.unitId &&
|
||||
candidate->GetTargetRow() == placement.row &&
|
||||
candidate->GetTargetColumn() == placement.column &&
|
||||
(candidate->GetCommandType() ==
|
||||
net::eagle0::shardok::common::PLACE_UNIT_COMMAND ||
|
||||
candidate->GetCommandType() ==
|
||||
net::eagle0::shardok::common::PLACE_HIDDEN_UNIT_COMMAND);
|
||||
});
|
||||
if (command == commands->end()) return std::nullopt;
|
||||
|
||||
PlacementCommand recommendation;
|
||||
recommendation.set_unit_id(placement.unitId);
|
||||
recommendation.set_row(placement.row);
|
||||
recommendation.set_column(placement.column);
|
||||
recommendations.push_back(recommendation);
|
||||
simulatedEngine.PostCommand(playerId, std::distance(commands->begin(), command));
|
||||
}
|
||||
|
||||
return recommendations;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto RecommendStartingPositions(
|
||||
const ShardokEngine& engine,
|
||||
const PlayerId playerId,
|
||||
const std::chrono::milliseconds timePerChoice) -> std::vector<PlacementCommand> {
|
||||
if (engine.GetGameStatus()->state() !=
|
||||
net::eagle0::shardok::storage::fb::GameStatus_::State_SET_UP ||
|
||||
engine.GetCurrentPlayerId() != playerId) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto playerInfos = engine.GetPlayerInfos();
|
||||
const auto playerInfo = std::ranges::find_if(playerInfos, [playerId](const auto& info) {
|
||||
return info.player_id() == playerId;
|
||||
});
|
||||
if (playerInfo == playerInfos.end()) return {};
|
||||
|
||||
if (engine.IsTutorialBattleEnabled() && playerInfo->is_defender()) {
|
||||
if (const auto tutorialRecommendations =
|
||||
RecommendTutorialStartingPositions(engine, playerId)) {
|
||||
return *tutorialRecommendations;
|
||||
}
|
||||
}
|
||||
|
||||
ShardokEngine simulatedEngine(engine, false);
|
||||
|
||||
mcts::MCTSConfig mctsConfig;
|
||||
mctsConfig.backpropagationPolicy = mcts::MCTSBackpropagationPolicy::MINIMAX;
|
||||
mctsConfig.maxPlayerFlips = 1;
|
||||
mctsConfig.maxSimulationFlips = 1;
|
||||
mctsConfig.simulationPolicy = mcts::MCTSSimulationPolicy::WEIGHTED_HEURISTIC;
|
||||
|
||||
ShardokAIClient ai(
|
||||
playerId,
|
||||
playerInfo->is_defender(),
|
||||
false,
|
||||
simulatedEngine.GetCurrentGameState()->hex_map(),
|
||||
simulatedEngine.GetGameSettings()->GetGetter(),
|
||||
AIAlgorithmType::ITERATIVE_DEEPENING,
|
||||
ScoringCalculatorType::STANDARD,
|
||||
mctsConfig);
|
||||
|
||||
std::vector<PlacementCommand> recommendations;
|
||||
while (simulatedEngine.GetGameStatus()->state() ==
|
||||
net::eagle0::shardok::storage::fb::GameStatus_::State_SET_UP &&
|
||||
simulatedEngine.GetCurrentPlayerId() == playerId) {
|
||||
const auto commands = simulatedEngine.GetAvailableCommandsForAIPlayer(playerId);
|
||||
if (commands->empty()) break;
|
||||
|
||||
const auto gameStateView = simulatedEngine.GetGameStateView(playerId);
|
||||
const auto choice = ai.ChooseCommandIndex(
|
||||
simulatedEngine.GetGameSettings(),
|
||||
gameStateView,
|
||||
commands,
|
||||
timePerChoice);
|
||||
const auto& command = (*commands)[choice.chosenIndex];
|
||||
const auto commandType = command->GetCommandType();
|
||||
|
||||
if (commandType == net::eagle0::shardok::common::END_PLAYER_SETUP_COMMAND) break;
|
||||
if (commandType != net::eagle0::shardok::common::PLACE_UNIT_COMMAND &&
|
||||
commandType != net::eagle0::shardok::common::PLACE_HIDDEN_UNIT_COMMAND) {
|
||||
break;
|
||||
}
|
||||
|
||||
PlacementCommand recommendation;
|
||||
recommendation.set_unit_id(command->GetActorUnitId());
|
||||
recommendation.set_row(command->GetTargetRow());
|
||||
recommendation.set_column(command->GetTargetColumn());
|
||||
recommendations.push_back(recommendation);
|
||||
|
||||
simulatedEngine.PostCommand(playerId, static_cast<int64_t>(choice.chosenIndex));
|
||||
}
|
||||
|
||||
return recommendations;
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// Created by Codex on 7/14/26.
|
||||
//
|
||||
|
||||
#ifndef EAGLE0_SHARDOK_AI_STARTING_POSITION_RECOMMENDER_HPP
|
||||
#define EAGLE0_SHARDOK_AI_STARTING_POSITION_RECOMMENDER_HPP
|
||||
|
||||
#include <chrono>
|
||||
#include <vector>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCTypes.h"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/api/placement_command.pb.h"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
class ShardokEngine;
|
||||
|
||||
[[nodiscard]] auto RecommendStartingPositions(
|
||||
const ShardokEngine& engine,
|
||||
PlayerId playerId,
|
||||
std::chrono::milliseconds timePerChoice = std::chrono::milliseconds(500))
|
||||
-> std::vector<net::eagle0::shardok::api::PlacementCommand>;
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
#endif // EAGLE0_SHARDOK_AI_STARTING_POSITION_RECOMMENDER_HPP
|
||||
@@ -4,8 +4,9 @@
|
||||
|
||||
#include "AbstractAIScoreCalculator.hpp"
|
||||
|
||||
#include <array>
|
||||
|
||||
#include "AIScoreCalculatorSharedUtilities.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIScoreUtilities.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIStrategy.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIUnitScoreCalculator.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIWaterCrossingCalculator.hpp"
|
||||
@@ -41,16 +42,44 @@ auto AbstractAIScoreCalculator::CalculateUnitsScoreComponents(
|
||||
|
||||
bool isLateGame = cachedCurrentRound > 18; // Inline IsLateGame for efficiency
|
||||
|
||||
// APDCache now has built-in thread-local caching - no need for PreCachedAPDs
|
||||
ActionPoints braveWaterCost = GetBraveWaterCost();
|
||||
|
||||
// Memoization cache for EffectiveDistance calls
|
||||
EffectiveDistanceCache distanceCache;
|
||||
|
||||
// Early return for empty game states
|
||||
const size_t estimatedUnitCount = cachedUnits->size();
|
||||
if (estimatedUnitCount == 0) { return UnitsScoreComponents{0.0, 0.0}; }
|
||||
|
||||
ActionPoints braveWaterCost = GetBraveWaterCost();
|
||||
|
||||
struct ResolvedActionPointDistances {
|
||||
const ActionPointDistances *notBraving = nullptr;
|
||||
const ActionPointDistances *braving = nullptr;
|
||||
};
|
||||
std::array<ResolvedActionPointDistances, BattalionTypeId::BattalionTypeId_MAX + 1>
|
||||
resolvedActionPointDistances{};
|
||||
|
||||
// A scoring call repeatedly asks for the same map/type APDs. Resolve each pointer once while
|
||||
// leaving ownership and cross-call caching with ActionPointDistancesCache.
|
||||
const auto notBravingApdFor = [&](int battalionTypeId, const BattalionTypeSPtr &battalionType) {
|
||||
auto &resolved = resolvedActionPointDistances[battalionTypeId];
|
||||
if (resolved.notBraving == nullptr) {
|
||||
resolved.notBraving = GetApdCache()->GetRaw(cachedHexMap, mapId, battalionType, false);
|
||||
}
|
||||
return resolved.notBraving;
|
||||
};
|
||||
const auto bravingApdFor = [&](int battalionTypeId, const BattalionTypeSPtr &battalionType) {
|
||||
if (!battalionType->allowsBraveWater) {
|
||||
return static_cast<const ActionPointDistances *>(nullptr);
|
||||
}
|
||||
|
||||
auto &resolved = resolvedActionPointDistances[battalionTypeId];
|
||||
if (resolved.braving == nullptr) {
|
||||
resolved.braving =
|
||||
GetApdCache()->GetRaw(cachedHexMap, mapId, battalionType, true, braveWaterCost);
|
||||
}
|
||||
return resolved.braving;
|
||||
};
|
||||
|
||||
// Memoization cache for EffectiveDistance calls
|
||||
EffectiveDistanceCache distanceCache;
|
||||
|
||||
std::vector<const Unit *> attackerUnits{};
|
||||
std::vector<const Unit *> defenderUnits{};
|
||||
// Pre-allocate vectors based on estimated unit ratios to avoid reallocations
|
||||
@@ -62,13 +91,24 @@ auto AbstractAIScoreCalculator::CalculateUnitsScoreComponents(
|
||||
|
||||
auto occupants = Occupants(*cachedUnits, cachedRowCount, cachedColumnCount);
|
||||
|
||||
// StartGame assigns real player IDs densely from zero, matching their player_infos indexes.
|
||||
// Use that invariant instead of scanning PlayerInfo for every unit or initializing a separate
|
||||
// lookup table for every evaluated state.
|
||||
const auto *playerInfos = gameStateRawPtr->player_infos();
|
||||
if (playerInfos == nullptr) { return UnitsScoreComponents{0.0, 0.0}; }
|
||||
|
||||
for (const Unit *unit : *cachedUnits) {
|
||||
const auto *pi = PlayerInfoForPid(gameState, unit->player_id());
|
||||
if (pi == nullptr) { continue; }
|
||||
const PlayerId playerId = unit->player_id();
|
||||
if (playerId < 0 || static_cast<flatbuffers::uoffset_t>(playerId) >= playerInfos->size()) {
|
||||
continue;
|
||||
}
|
||||
const auto *playerInfo = playerInfos->Get(static_cast<flatbuffers::uoffset_t>(playerId));
|
||||
if (playerInfo->player_id() != playerId) { continue; }
|
||||
const bool isDefender = playerInfo->is_defender();
|
||||
|
||||
switch (unit->status()) {
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT: {
|
||||
if (pi->is_defender()) {
|
||||
if (isDefender) {
|
||||
defenderUnits.push_back(unit);
|
||||
} else {
|
||||
attackerUnits.push_back(unit);
|
||||
@@ -79,7 +119,7 @@ auto AbstractAIScoreCalculator::CalculateUnitsScoreComponents(
|
||||
double thisScore = unit->has_attached_hero() && unit->attached_hero().is_vip()
|
||||
? CAPTURED_VIP_SCORE
|
||||
: CAPTURED_UNIT_SCORE;
|
||||
if (pi->is_defender()) {
|
||||
if (isDefender) {
|
||||
defenderUnitsValue += thisScore;
|
||||
} else {
|
||||
attackerUnitsValue += thisScore;
|
||||
@@ -115,16 +155,8 @@ auto AbstractAIScoreCalculator::CalculateUnitsScoreComponents(
|
||||
// Cache battalion type reference to avoid shared_ptr atomic operations
|
||||
const auto &battalionType = GetBattalionType(static_cast<BattalionTypeId>(battTypeId));
|
||||
|
||||
// Cache APD lookups - same battalion type is used multiple times below
|
||||
const auto *notBravingApd =
|
||||
GetApdCache()->GetRaw(cachedHexMap, mapId, battalionType, false);
|
||||
const auto *bravingApd = battalionType->allowsBraveWater ? GetApdCache()->GetRaw(
|
||||
cachedHexMap,
|
||||
mapId,
|
||||
battalionType,
|
||||
true,
|
||||
braveWaterCost)
|
||||
: nullptr;
|
||||
const auto *notBravingApd = notBravingApdFor(battTypeId, battalionType);
|
||||
const auto *bravingApd = bravingApdFor(battTypeId, battalionType);
|
||||
|
||||
const auto &priorityList = std::ranges::find_if(
|
||||
attackerTargetPriorities,
|
||||
@@ -174,9 +206,7 @@ auto AbstractAIScoreCalculator::CalculateUnitsScoreComponents(
|
||||
// Cache battalion type reference to avoid shared_ptr atomic operations
|
||||
const auto &battalionType = GetBattalionType(static_cast<BattalionTypeId>(battTypeId));
|
||||
|
||||
// Cache APD lookups for this defender unit
|
||||
const auto *defenderNotBravingApd =
|
||||
GetApdCache()->GetRaw(cachedHexMap, mapId, battalionType, false);
|
||||
const auto *defenderNotBravingApd = notBravingApdFor(battTypeId, battalionType);
|
||||
|
||||
auto dv = UnitValue(
|
||||
unit,
|
||||
@@ -211,14 +241,8 @@ auto AbstractAIScoreCalculator::CalculateUnitsScoreComponents(
|
||||
const DIST_T thisDistance = distanceCache.GetOrCompute(
|
||||
attackerUnit,
|
||||
unit->location(),
|
||||
GetApdCache()->GetRaw(cachedHexMap, mapId, attackerBattalionType, false),
|
||||
attackerBattalionType->allowsBraveWater ? GetApdCache()->GetRaw(
|
||||
cachedHexMap,
|
||||
mapId,
|
||||
attackerBattalionType,
|
||||
true,
|
||||
braveWaterCost)
|
||||
: nullptr,
|
||||
notBravingApdFor(attackerBattTypeId, attackerBattalionType),
|
||||
bravingApdFor(attackerBattTypeId, attackerBattalionType),
|
||||
cachedHexMap);
|
||||
if (thisDistance < closestDistanceToEnemy) {
|
||||
closestDistanceToEnemy = thisDistance;
|
||||
@@ -241,19 +265,8 @@ auto AbstractAIScoreCalculator::CalculateUnitsScoreComponents(
|
||||
const DIST_T thisDistance = distanceCache.GetOrCompute(
|
||||
defenderUnit,
|
||||
unit->location(),
|
||||
GetApdCache()->GetRaw(
|
||||
cachedHexMap,
|
||||
mapId,
|
||||
defenderBattalionType,
|
||||
false),
|
||||
defenderBattalionType->allowsBraveWater
|
||||
? GetApdCache()->GetRaw(
|
||||
cachedHexMap,
|
||||
mapId,
|
||||
defenderBattalionType,
|
||||
true,
|
||||
braveWaterCost)
|
||||
: nullptr,
|
||||
notBravingApdFor(defenderBattTypeId, defenderBattalionType),
|
||||
bravingApdFor(defenderBattTypeId, defenderBattalionType),
|
||||
cachedHexMap);
|
||||
if (thisDistance < closestDistanceToFriendly) {
|
||||
closestDistanceToFriendly = thisDistance;
|
||||
@@ -306,6 +319,7 @@ auto AbstractAIScoreCalculator::CalculateAttackerVictoryConditionScore(
|
||||
gameState,
|
||||
castleCoords,
|
||||
attackerStrategy.targetLocations,
|
||||
GetAlCache()->GetLandRegions(),
|
||||
GetApdCache());
|
||||
break;
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ cc_library(
|
||||
":game_update_receiver",
|
||||
"//src/main/cpp/net/eagle0/common:shardok_latency_trace",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:shardok_ai_client",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:starting_position_recommender",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:engine",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:shardok_c_types",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:unit_placement_info",
|
||||
|
||||
@@ -20,10 +20,37 @@
|
||||
|
||||
#include "src/main/cpp/net/eagle0/common/ShardokLatencyTrace.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/ShardokAIClient.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/StartingPositionRecommender.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/api/game_state_view.pb.h"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
auto PlayerBattleProgressFrom(const net::eagle0::shardok::storage::fb::GameState &gameState)
|
||||
-> std::vector<PlayerBattleProgress> {
|
||||
std::vector<PlayerBattleProgress> progress;
|
||||
progress.reserve(gameState.player_infos()->size());
|
||||
for (const auto *player : *gameState.player_infos()) {
|
||||
int32_t troopCount = 0;
|
||||
bool isRetreating = false;
|
||||
for (const auto *unit : *gameState.units()) {
|
||||
if (unit->player_id() != player->player_id()) continue;
|
||||
switch (unit->status()) {
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_NEVER_ENTERED_UNIT:
|
||||
troopCount += unit->battalion().size();
|
||||
break;
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_FLED_UNIT:
|
||||
isRetreating = true;
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
progress.push_back(PlayerBattleProgress{player->player_id(), troopCount, isRetreating});
|
||||
}
|
||||
return progress;
|
||||
}
|
||||
using net::eagle0::common::ScopedShardokLatencyTrace;
|
||||
|
||||
using net::eagle0::shardok::common::GameStatus;
|
||||
@@ -237,7 +264,7 @@ void ShardokGameController::DoAIThread() {
|
||||
{{"player_id", std::to_string(playerId)},
|
||||
{"round", std::to_string(gsv.current_round())},
|
||||
{"available_command_count", std::to_string(availableCommands->size())}});
|
||||
return aiClient->ChooseCommandIndex(settings, gsv, availableCommands);
|
||||
return aiClient->ChooseCommandIndex(settings, gsv, availableCommands, std::nullopt);
|
||||
}();
|
||||
chosenIndex = results.chosenIndex;
|
||||
|
||||
@@ -455,6 +482,10 @@ auto ShardokGameController::GetUpdates(const int64_t startingActionId) -> AllUpd
|
||||
cachedGameId,
|
||||
{{"starting_action_id", std::to_string(startingActionId)}});
|
||||
AllUpdates updates{};
|
||||
std::unique_ptr<ShardokEngine> recommendationEngine;
|
||||
std::shared_ptr<AvailableCommands> recommendationCommands;
|
||||
PlayerId recommendationPlayerId = -1;
|
||||
std::pair<int64_t, PlayerId> recommendationCacheKey;
|
||||
|
||||
incomingRegistrations++;
|
||||
std::vector<net::eagle0::shardok::storage::ShardokActionWithResultingState> awrs;
|
||||
@@ -527,6 +558,15 @@ auto ShardokGameController::GetUpdates(const int64_t startingActionId) -> AllUpd
|
||||
*acs->mutable_preview_command() = {
|
||||
std::begin(previewCommands),
|
||||
std::end(previewCommands)};
|
||||
|
||||
if (engine->GetGameStatus()->state() ==
|
||||
net::eagle0::shardok::storage::fb::GameStatus_::State_SET_UP &&
|
||||
engine->GetCurrentPlayerId() == pi.player_id() && !availableCommands.empty()) {
|
||||
recommendationEngine = std::make_unique<ShardokEngine>(*engine, false);
|
||||
recommendationCommands = acs;
|
||||
recommendationPlayerId = pi.player_id();
|
||||
recommendationCacheKey = {updates.newUnfilteredCount, recommendationPlayerId};
|
||||
}
|
||||
}
|
||||
|
||||
updates.filteredResults.emplace_back(fid, actionResultViews, acs);
|
||||
@@ -574,22 +614,26 @@ auto ShardokGameController::GetUpdates(const int64_t startingActionId) -> AllUpd
|
||||
// Extract battle progress data from the FlatBuffer game state
|
||||
const auto *gs = engine->GetCurrentGameState().Get();
|
||||
updates.currentRound = gs->current_round();
|
||||
updates.playerTroopCounts.reserve(gs->player_infos()->size());
|
||||
for (const auto *player : *gs->player_infos()) {
|
||||
int32_t troopCount = 0;
|
||||
for (const auto *unit : *gs->units()) {
|
||||
if (unit->player_id() != player->player_id()) continue;
|
||||
switch (unit->status()) {
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_NEVER_ENTERED_UNIT:
|
||||
troopCount += unit->battalion().size();
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
updates.playerTroopCounts.emplace_back(player->player_id(), troopCount);
|
||||
updates.playerBattleProgress = PlayerBattleProgressFrom(*gs);
|
||||
}
|
||||
|
||||
if (recommendationEngine) {
|
||||
std::scoped_lock<std::mutex> recommendationGuard(recommendationLock);
|
||||
auto cached = recommendedStartingPositionsCache.find(recommendationCacheKey);
|
||||
if (cached == recommendedStartingPositionsCache.end()) {
|
||||
cached = recommendedStartingPositionsCache
|
||||
.emplace(
|
||||
recommendationCacheKey,
|
||||
RecommendStartingPositions(
|
||||
*recommendationEngine,
|
||||
recommendationPlayerId))
|
||||
.first;
|
||||
}
|
||||
recommendationCommands->mutable_recommended_starting_positions()->Reserve(
|
||||
cached->second.size());
|
||||
*recommendationCommands->mutable_recommended_starting_positions() = {
|
||||
cached->second.begin(),
|
||||
cached->second.end()};
|
||||
}
|
||||
|
||||
return updates;
|
||||
@@ -691,7 +735,7 @@ auto ShardokGameController::WaitForUpdatesAndPush(
|
||||
updates.newUnfilteredCount,
|
||||
updates.currentGameState,
|
||||
updates.currentRound,
|
||||
updates.playerTroopCounts);
|
||||
updates.playerBattleProgress);
|
||||
}
|
||||
|
||||
// Tutorial battle reset: defender lost, signal reset instead of game over
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <condition_variable>
|
||||
#include <ctime>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
@@ -27,6 +28,7 @@
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/UnitPlacementInfo.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/GameStateHelpers.hpp"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/api/game_state_view.pb.h"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/api/placement_command.pb.h"
|
||||
|
||||
namespace shardok {
|
||||
using net::eagle0::shardok::api::ActionResultView;
|
||||
@@ -57,6 +59,16 @@ struct OnePlayerUpdates {
|
||||
availableCommands(acs) {}
|
||||
};
|
||||
|
||||
struct PlayerBattleProgress {
|
||||
int32_t playerId;
|
||||
int32_t troopCount;
|
||||
bool isRetreating;
|
||||
};
|
||||
|
||||
[[nodiscard]] auto PlayerBattleProgressFrom(
|
||||
const net::eagle0::shardok::storage::fb::GameState& gameState)
|
||||
-> std::vector<PlayerBattleProgress>;
|
||||
|
||||
/// Result of WaitForUpdatesAndPush indicating how the wait ended
|
||||
enum class WaitResult {
|
||||
GAME_OVER, // Game ended normally
|
||||
@@ -76,7 +88,7 @@ public:
|
||||
int32_t newUnfilteredCount,
|
||||
const byte_vector& currentGameState,
|
||||
int32_t currentRound,
|
||||
const std::vector<std::pair<int32_t, int32_t>>& playerTroopCounts) = 0;
|
||||
const std::vector<PlayerBattleProgress>& playerBattleProgress) = 0;
|
||||
|
||||
/// Called when the game ends
|
||||
virtual void OnGameOver(const GameOverInfo& info) = 0;
|
||||
@@ -121,6 +133,10 @@ private:
|
||||
std::vector<std::shared_ptr<ShardokAIClient>> aiClients;
|
||||
std::thread aiThread;
|
||||
|
||||
std::mutex recommendationLock{};
|
||||
std::map<std::pair<int64_t, PlayerId>, std::vector<net::eagle0::shardok::api::PlacementCommand>>
|
||||
recommendedStartingPositionsCache;
|
||||
|
||||
// Each entry maps an Eagle faction id of a non-participant watcher to the
|
||||
// Shardok pids of participants whose allies list named this watcher. Used
|
||||
// when generating per-watcher filtered views in GetUpdates.
|
||||
@@ -183,7 +199,7 @@ public:
|
||||
int32_t newUnfilteredCount;
|
||||
byte_vector currentGameState;
|
||||
int32_t currentRound = 0;
|
||||
std::vector<std::pair<int32_t, int32_t>> playerTroopCounts; // (playerId, troopCount)
|
||||
std::vector<PlayerBattleProgress> playerBattleProgress;
|
||||
};
|
||||
auto GetUpdates(int64_t startingActionId) -> AllUpdates;
|
||||
auto GetCurrentGameStateBytes() -> byte_vector;
|
||||
|
||||
@@ -52,11 +52,22 @@ private:
|
||||
const std::vector<std::shared_ptr<const CommandFactory>> commandFactories;
|
||||
const PlayerSetupCommandFactory playerSetupCommandFactory;
|
||||
|
||||
struct PlayerCommandContext {
|
||||
std::vector<PlayerId> allyPids;
|
||||
PossibleChargees possibleChargees;
|
||||
bool isAttacker;
|
||||
bool cannotBecomeOutlaw;
|
||||
};
|
||||
|
||||
static auto MakePlayerCommandContext(const GameStateW &gameState, PlayerId playerId)
|
||||
-> PlayerCommandContext;
|
||||
|
||||
// AvailableCommands helpers for a single unit
|
||||
void AddAvailableCommandsForOneUnit(
|
||||
const GameStateW &gameState,
|
||||
CommandList &commands,
|
||||
const Unit *unit,
|
||||
const PlayerCommandContext &playerContext,
|
||||
bool onlyFollowUps) const;
|
||||
|
||||
public:
|
||||
@@ -95,30 +106,39 @@ auto AvailableCommandsFactoryImpl::GetPlayerSetupCommands(
|
||||
gameState,
|
||||
reinforcementPositions);
|
||||
|
||||
return std::make_shared<CommandList>(previewCommands);
|
||||
return std::make_shared<CommandList>(std::move(previewCommands));
|
||||
}
|
||||
|
||||
auto AvailableCommandsFactoryImpl::MakePlayerCommandContext(
|
||||
const GameStateW &gameState,
|
||||
const PlayerId playerId) -> PlayerCommandContext {
|
||||
PossibleChargees possibleChargees{};
|
||||
possibleChargees.reserve(gameState->possible_chargee_ids()->size());
|
||||
for (const UnitId chargeeId : *gameState->possible_chargee_ids()) {
|
||||
if (chargeeId != -1) { possibleChargees.push_back(chargeeId); }
|
||||
}
|
||||
|
||||
return PlayerCommandContext{
|
||||
.allyPids = AlliedPids(gameState, playerId),
|
||||
.possibleChargees = std::move(possibleChargees),
|
||||
.isAttacker = IsAttacker(gameState, playerId),
|
||||
.cannotBecomeOutlaw = CannotBecomeOutlaw(gameState, playerId)};
|
||||
}
|
||||
|
||||
void AvailableCommandsFactoryImpl::AddAvailableCommandsForOneUnit(
|
||||
const GameStateW &gameState,
|
||||
CommandList &commands,
|
||||
const Unit *unit,
|
||||
const PlayerCommandContext &playerContext,
|
||||
const bool onlyFollowUps) const {
|
||||
const auto &battType = settings.GetBattalionType(unit->battalion().type());
|
||||
const bool hasHero = unit->has_attached_hero();
|
||||
const auto allyPids = AlliedPids(gameState, unit->player_id());
|
||||
const bool isAttacker = IsAttacker(gameState, unit->player_id());
|
||||
const bool cannotBecomeOutlaw = CannotBecomeOutlaw(gameState, unit->player_id());
|
||||
|
||||
const bool unitMovedIntoZoc = unit->has_moved_in_zoc();
|
||||
const ActionPoints remainingActionPoints = unit->remaining_action_points();
|
||||
const Coords ¤tCoords = unit->location();
|
||||
|
||||
const bool isEligibleCharger = unit->unit_id() == gameState->eligible_charger_id();
|
||||
std::vector<UnitId> possibleChargees{};
|
||||
possibleChargees.reserve(gameState->possible_chargee_ids()->size());
|
||||
for (const UnitId cid : *gameState->possible_chargee_ids()) {
|
||||
if (cid != -1) { possibleChargees.push_back(cid); }
|
||||
}
|
||||
|
||||
// Undead units cannot act until the commanding hero has controlled them
|
||||
if (unit->commanding_unit_id() != -1) {
|
||||
@@ -140,12 +160,12 @@ void AvailableCommandsFactoryImpl::AddAvailableCommandsForOneUnit(
|
||||
.position = currentCoords,
|
||||
.weatherFb = gameState->weather(),
|
||||
.isEligibleCharger = isEligibleCharger,
|
||||
.possibleChargees = possibleChargees,
|
||||
.possibleChargees = playerContext.possibleChargees,
|
||||
.hexMap = gameState->hex_map(),
|
||||
.units = gameState->units(),
|
||||
.allyPids = allyPids,
|
||||
.isAttacker = isAttacker,
|
||||
.cannotBecomeOutlaw = cannotBecomeOutlaw,
|
||||
.allyPids = playerContext.allyPids,
|
||||
.isAttacker = playerContext.isAttacker,
|
||||
.cannotBecomeOutlaw = playerContext.cannotBecomeOutlaw,
|
||||
.unitMovedIntoZoc = unitMovedIntoZoc};
|
||||
|
||||
for (const auto &commandFactory : commandFactories) {
|
||||
@@ -184,7 +204,10 @@ void AvailableCommandsFactoryImpl::AddAvailableCommandsForOneUnit(
|
||||
});
|
||||
}
|
||||
|
||||
commands.insert(commands.end(), oneUnitCommands.begin(), oneUnitCommands.end());
|
||||
commands.insert(
|
||||
commands.end(),
|
||||
std::make_move_iterator(oneUnitCommands.begin()),
|
||||
std::make_move_iterator(oneUnitCommands.end()));
|
||||
}
|
||||
|
||||
auto AvailableCommandsFactoryImpl::GetAvailableCommands(
|
||||
@@ -196,7 +219,9 @@ auto AvailableCommandsFactoryImpl::GetAvailableCommands(
|
||||
|
||||
playerSetupCommandFactory
|
||||
.AddAvailablePlayerSetupCommands(commands, playerId, gameState, reinforcementPositions);
|
||||
if (!commands.empty()) return std::make_shared<CommandList>(commands);
|
||||
if (!commands.empty()) return std::make_shared<CommandList>(std::move(commands));
|
||||
|
||||
const auto playerContext = MakePlayerCommandContext(gameState, playerId);
|
||||
|
||||
for (const Unit *unit : *gameState->units()) {
|
||||
if (unit->status() != net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) continue;
|
||||
@@ -205,6 +230,7 @@ auto AvailableCommandsFactoryImpl::GetAvailableCommands(
|
||||
gameState,
|
||||
commands,
|
||||
unit,
|
||||
playerContext,
|
||||
/* onlyFollowUps=*/false);
|
||||
}
|
||||
|
||||
@@ -229,10 +255,13 @@ auto AvailableCommandsFactoryImpl::GetAvailableCommands(
|
||||
auto after = ApplyResults(copy, results, settings);
|
||||
|
||||
CommandList followUpCommands{};
|
||||
const auto followUpPlayerContext =
|
||||
MakePlayerCommandContext(after, after->units()->Get(actor)->player_id());
|
||||
AddAvailableCommandsForOneUnit(
|
||||
after,
|
||||
followUpCommands,
|
||||
after->units()->Get(actor),
|
||||
followUpPlayerContext,
|
||||
/* onlyFollowUps=*/true);
|
||||
|
||||
std::unordered_set<CommandType> followUpTypes{};
|
||||
@@ -249,7 +278,7 @@ auto AvailableCommandsFactoryImpl::GetAvailableCommands(
|
||||
}
|
||||
|
||||
if (commands.empty()) return nullptr;
|
||||
return std::make_shared<CommandList>(commands);
|
||||
return std::make_shared<CommandList>(std::move(commands));
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <optional>
|
||||
#include <ranges>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
@@ -566,9 +567,81 @@ void ShardokEngine::PostCommand(
|
||||
const CommandSPtr command = (*cachedAvailableCommands)[commandIndex];
|
||||
cachedAvailableCommands = nullptr;
|
||||
|
||||
if (MaybeInterceptTutorialEndTurn(command)) { return; }
|
||||
|
||||
PostActionUnchecked(command, randomGenerator, roll);
|
||||
}
|
||||
|
||||
auto ShardokEngine::MaybeInterceptTutorialEndTurn(const CommandSPtr &command) -> bool {
|
||||
using net::eagle0::shardok::common::ActionType;
|
||||
using net::eagle0::shardok::common::CommandType;
|
||||
|
||||
if (command->GetCommandType() != CommandType::END_TURN_COMMAND ||
|
||||
!tutorialController_.IsEnabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto configuredHeroIds = tutorialController_.EndTurnWarningEagleHeroIds();
|
||||
if (configuredHeroIds.empty()) { return false; }
|
||||
|
||||
std::unordered_set<int32_t> warnedHeroIds;
|
||||
for (const auto &entry : actionHistory) {
|
||||
const auto &result = entry.action_result();
|
||||
if (result.type() != ActionType::TUTORIAL_UNUSED_HERO_WARNING) { continue; }
|
||||
warnedHeroIds.insert(
|
||||
result.tutorial_warning_eagle_hero_ids().begin(),
|
||||
result.tutorial_warning_eagle_hero_ids().end());
|
||||
}
|
||||
|
||||
std::unordered_set<UnitId> actedUnitIds;
|
||||
for (auto entry = actionHistory.rbegin(); entry != actionHistory.rend(); ++entry) {
|
||||
const auto &result = entry->action_result();
|
||||
if (result.type() == ActionType::PLAYER_TURN_START && result.has_next_player() &&
|
||||
result.next_player().value() == command->GetPlayerId()) {
|
||||
break;
|
||||
}
|
||||
if (result.has_actor()) { actedUnitIds.insert(result.actor().value()); }
|
||||
}
|
||||
|
||||
const auto availableCommands = availableCommandsFactory->GetAvailableCommands(
|
||||
gameState,
|
||||
command->GetPlayerId(),
|
||||
/* includeFollowUps=*/true);
|
||||
std::unordered_set<UnitId> unitsAbleToAct;
|
||||
for (const auto &availableCommand : *availableCommands) {
|
||||
const auto proto = availableCommand->GetCommandProto();
|
||||
if (proto.has_actor()) { unitsAbleToAct.insert(proto.actor().value()); }
|
||||
}
|
||||
|
||||
const std::unordered_set<int32_t> configuredHeroIdSet(
|
||||
configuredHeroIds.begin(),
|
||||
configuredHeroIds.end());
|
||||
std::vector<int32_t> newlyWarnedHeroIds;
|
||||
for (const auto *unit : *gameState->units()) {
|
||||
if (unit->player_id() != command->GetPlayerId() || !unit->has_attached_hero() ||
|
||||
unit->status() != net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT ||
|
||||
actedUnitIds.contains(unit->unit_id()) || !unitsAbleToAct.contains(unit->unit_id())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto eagleHeroId = unit->attached_hero().eagle_hero_id();
|
||||
if (configuredHeroIdSet.contains(eagleHeroId) && !warnedHeroIds.contains(eagleHeroId)) {
|
||||
newlyWarnedHeroIds.push_back(eagleHeroId);
|
||||
}
|
||||
}
|
||||
|
||||
if (newlyWarnedHeroIds.empty()) { return false; }
|
||||
|
||||
ActionResultProto warningResult;
|
||||
warningResult.set_type(ActionType::TUTORIAL_UNUSED_HERO_WARNING);
|
||||
warningResult.mutable_player()->set_value(command->GetPlayerId());
|
||||
warningResult.mutable_tutorial_warning_eagle_hero_ids()->Add(
|
||||
newlyWarnedHeroIds.begin(),
|
||||
newlyWarnedHeroIds.end());
|
||||
ApplyAndAddActionResult(warningResult);
|
||||
return true;
|
||||
}
|
||||
|
||||
void ShardokEngine::PostActionUnchecked(
|
||||
const CommandSPtr &command,
|
||||
const std::shared_ptr<RandomGenerator> &randomGenerator,
|
||||
|
||||
@@ -82,6 +82,8 @@ private:
|
||||
const std::shared_ptr<RandomGenerator> &randomGenerator,
|
||||
std::optional<int32_t> roll);
|
||||
|
||||
[[nodiscard]] auto MaybeInterceptTutorialEndTurn(const CommandSPtr &command) -> bool;
|
||||
|
||||
public:
|
||||
// Constructor using existing game state
|
||||
ShardokEngine(
|
||||
|
||||
@@ -57,7 +57,7 @@ auto shardok::MoveCommand::InternalExecute(
|
||||
const auto* destinationTerrain = GetTerrain(map, destination);
|
||||
bool startedInEnemyZoc = enemyZocCoords.Contains(origin);
|
||||
vector<const Unit*> knownAdjacentEnemies =
|
||||
KnownAdjacentEnemies(player, allUnits, map, allyPids, origin);
|
||||
KnownAdjacentEnemies(player, currentState, allyPids, origin);
|
||||
|
||||
MutatingSpendActionPoints(
|
||||
&mover,
|
||||
@@ -132,7 +132,7 @@ auto shardok::MoveCommand::InternalExecute(
|
||||
|
||||
if (!startedInEnemyZoc) {
|
||||
vector<const Unit*> newAdjacentEnemies =
|
||||
KnownAdjacentEnemies(player, allUnits, map, allyPids, destination);
|
||||
KnownAdjacentEnemies(player, currentState, allyPids, destination);
|
||||
|
||||
for (const Unit* possibleChargee : newAdjacentEnemies) {
|
||||
if (std::ranges::find_if(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user