mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-29 05:15:44 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8769dd1132 | ||
|
|
98afc28762 | ||
|
|
4cd1349a5a | ||
|
|
cebac64264 | ||
|
|
172125e03b | ||
|
|
15006091b4 | ||
|
|
2175f1d957 | ||
|
|
d445cbfe25 | ||
|
|
5fc56c342d | ||
|
|
e5350d03a8 | ||
|
|
097daf3dc1 | ||
|
|
3c3dc85187 | ||
|
|
a775a667f6 | ||
|
|
a1a7a69aeb | ||
|
|
ff549ab337 | ||
|
|
464651503e | ||
|
|
2f93250c8e | ||
|
|
22ce7ba750 | ||
|
|
acbb09f6a1 | ||
|
|
900dd23ae7 | ||
|
|
0f391776aa | ||
|
|
817fb613ab | ||
|
|
2f57ea0160 | ||
|
|
f5d5aedf9d | ||
|
|
c5114e9bf1 | ||
|
|
7514aed437 | ||
|
|
f4d381c9d4 | ||
|
|
8698036f7c | ||
|
|
21f04cfd46 | ||
|
|
074cf62cc9 | ||
|
|
a66f833c90 | ||
|
|
d704d23d39 | ||
|
|
e2570aec4a | ||
|
|
6b97fde9ab | ||
|
|
30adf1bcbf | ||
|
|
f205b49a54 | ||
|
|
5c7e18696e | ||
|
|
c78b00b0fc | ||
|
|
b05d6e1e1b | ||
|
|
da0bce655c | ||
|
|
d4dcbcc9d5 | ||
|
|
f132e5b87b | ||
|
|
872475e82b | ||
|
|
66d062bfda | ||
|
|
a514ceb62f | ||
|
|
b0d8a5bc41 | ||
|
|
8f4bb5130e | ||
|
|
9386589558 | ||
|
|
5945289a81 | ||
|
|
53346cc85b | ||
|
|
a6fad3ac4e | ||
|
|
cb7e2b5d5c | ||
|
|
0f764416a3 | ||
|
|
c9e0012bf7 | ||
|
|
457f2a677a | ||
|
|
ced56ee41f | ||
|
|
4e910f1f22 | ||
|
|
34bceebace | ||
|
|
b4f802ba77 | ||
|
|
3d9d464870 | ||
|
|
a7e21427f9 | ||
|
|
bdbf10294b | ||
|
|
8588430cf0 | ||
|
|
d920bb5c17 | ||
|
|
64fe9bf59d | ||
|
|
101b0bf41f | ||
|
|
019865b5ee | ||
|
|
799d34a1f1 | ||
|
|
951d533ed3 | ||
|
|
459eb0ef7b | ||
|
|
f710105bd9 | ||
|
|
4998e3ce40 | ||
|
|
d739b374a4 | ||
|
|
bd61ebcbb6 | ||
|
|
3963275092 | ||
|
|
a796e0fe67 | ||
|
|
52ac61fa1d | ||
|
|
29ca7665b1 | ||
|
|
d9bdb814f4 | ||
|
|
3dfb094a08 | ||
|
|
7ce1818a7f | ||
|
|
6e6eff7991 | ||
|
|
aa9c98f081 | ||
|
|
cdf0ad45f0 | ||
|
|
1b97b6673f | ||
|
|
721d432bc6 | ||
|
|
e513a784eb | ||
|
|
ff8da78992 | ||
|
|
84a69ce982 | ||
|
|
7d743f8678 | ||
|
|
3f24e15392 | ||
|
|
0f86bb6874 | ||
|
|
05ee7a2ab2 | ||
|
|
b9321433d0 | ||
|
|
62791401e9 | ||
|
|
4d28bba9cc | ||
|
|
1f3660e1f3 | ||
|
|
95ff44e205 | ||
|
|
987a164092 | ||
|
|
1b5b11fd5b | ||
|
|
ddcde52e27 | ||
|
|
ea2da84535 | ||
|
|
abed8ee574 | ||
|
|
556c59a9c7 | ||
|
|
ad2d085b24 | ||
|
|
d7209102b8 | ||
|
|
14e1ba6795 | ||
|
|
65b56081ce | ||
|
|
eafb51cbfa | ||
|
|
3444ae296e | ||
|
|
4f2fff5da6 | ||
|
|
144483a45d | ||
|
|
53345b2bb0 | ||
|
|
5d6bc429b0 | ||
|
|
a4faf61613 | ||
|
|
fdd516598d | ||
|
|
70da6990c4 | ||
|
|
e8b18d0805 | ||
|
|
1ca9e8678c | ||
|
|
94670149fe | ||
|
|
8e253c2972 | ||
|
|
6e15c6d9cb | ||
|
|
ca00e90ad6 | ||
|
|
b1d76ea661 | ||
|
|
059d428cfa | ||
|
|
a4b50eb357 | ||
|
|
0dfd5aa11d | ||
|
|
2b63ccd145 | ||
|
|
97cec5ec38 | ||
|
|
31fb5a3616 | ||
|
|
7b7130d67d | ||
|
|
6707ad55ff | ||
|
|
89aa73b63a | ||
|
|
bb7453a002 | ||
|
|
8cea29dcb0 | ||
|
|
d5d74ec8d6 | ||
|
|
9b000b94ac | ||
|
|
82ed9fcc83 | ||
|
|
cd53283935 | ||
|
|
3faddf76f8 | ||
|
|
6e24ae2f4f | ||
|
|
66912580c8 | ||
|
|
219dd830e1 | ||
|
|
3d5cdcc78e | ||
|
|
ab1d43fae3 | ||
|
|
2d51f3c23d | ||
|
|
f9447cf7aa | ||
|
|
c6ed0bfc7d | ||
|
|
2d4621b1a6 | ||
|
|
3bf929b4a5 | ||
|
|
ebed01ba86 | ||
|
|
880d005347 | ||
|
|
e8ef216de8 | ||
|
|
baa453594a | ||
|
|
9c767d5c49 | ||
|
|
e0892bd2fa | ||
|
|
315be7c465 | ||
|
|
9d34dc0954 | ||
|
|
bdedd43e3c | ||
|
|
97e779675e | ||
|
|
2ef4216886 | ||
|
|
8c9764a57e | ||
|
|
eb94eb1a6d | ||
|
|
12e3fb99eb | ||
|
|
2bd90a26a8 | ||
|
|
24db3a4cb1 | ||
|
|
0d05ea1730 | ||
|
|
9c13b3c1bf | ||
|
|
492d9d16eb | ||
|
|
c4cd0eb683 | ||
|
|
287f7b9818 | ||
|
|
15b5a3e0c1 | ||
|
|
cf93c43ef2 | ||
|
|
380c24537b | ||
|
|
6200bc7e02 | ||
|
|
e92465c894 | ||
|
|
6f432d221a | ||
|
|
b8ddca4e76 | ||
|
|
f343ee9f7f | ||
|
|
9a8c4c6fdd | ||
|
|
96f2dc0454 | ||
|
|
e0988ef186 | ||
|
|
a154174fa1 | ||
|
|
042861f065 | ||
|
|
0f776af050 | ||
|
|
166e50924f | ||
|
|
efb34ede56 | ||
|
|
22b55eb223 | ||
|
|
f5d9027206 | ||
|
|
9dcbeff21a | ||
|
|
0cc7c4d0a2 | ||
|
|
b0ca65b720 | ||
|
|
e3eb3eb7bf | ||
|
|
b8b2c92227 | ||
|
|
424dde5da6 | ||
|
|
ce7e636987 | ||
|
|
c51f61ba56 | ||
|
|
91623bcf7a | ||
|
|
34e14a3521 | ||
|
|
6c1a79b342 | ||
|
|
6af5014866 | ||
|
|
16f53dc1b1 | ||
|
|
df09fe0684 | ||
|
|
e765764cea | ||
|
|
2e63bcb5c3 | ||
|
|
e231f0c34e | ||
|
|
8155fe7b3f | ||
|
|
6fe1bcdff2 | ||
|
|
b6b488bd46 | ||
|
|
d97cc0cadc | ||
|
|
2a3c742be9 | ||
|
|
cdee86dbcb | ||
|
|
60264e2bfc | ||
|
|
19fb315500 | ||
|
|
66710659c9 | ||
|
|
de5a34c485 | ||
|
|
e0c7ba3874 | ||
|
|
9e58fbee56 | ||
|
|
5315941f88 | ||
|
|
3ffeb0250b | ||
|
|
451e6bb406 | ||
|
|
58d680bd59 | ||
|
|
79eb708adb | ||
|
|
1282e62781 | ||
|
|
4af501feee | ||
|
|
ef7c24f6de | ||
|
|
62edd02a22 | ||
|
|
6f22edc8f3 | ||
|
|
27bba1c2da | ||
|
|
c95bc22f0e | ||
|
|
844bab253d | ||
|
|
b8372487cf | ||
|
|
8cc38466e6 | ||
|
|
14935b1a52 | ||
|
|
f7b14cd9ba | ||
|
|
9d1e61ef8d | ||
|
|
8c912c1b3e | ||
|
|
1ff6ae0195 | ||
|
|
641bf456fa | ||
|
|
f21eaabb15 | ||
|
|
7d38336e67 | ||
|
|
f37ec78d10 | ||
|
|
eb0f92dcd0 | ||
|
|
ee06235cf4 | ||
|
|
48dfe9054d | ||
|
|
c7e39e67aa | ||
|
|
86e51a11b7 | ||
|
|
3c0e98280d | ||
|
|
c54c9f7955 | ||
|
|
614c404bf4 | ||
|
|
c1ebcd49fc | ||
|
|
49551614fe | ||
|
|
b2b8603f23 | ||
|
|
4a6ecda8f8 | ||
|
|
b65d3c25e1 | ||
|
|
65f5ec1c38 | ||
|
|
b8b225eb84 | ||
|
|
c20239b9e8 | ||
|
|
a8df7228b3 | ||
|
|
b8ebc0efd8 | ||
|
|
5ee86faf80 | ||
|
|
3d6524b2fa | ||
|
|
82b671b234 | ||
|
|
3ad6de784f | ||
|
|
6a9e1d7ad7 | ||
|
|
84f182d74f | ||
|
|
d08402c502 | ||
|
|
b6a7c38a70 | ||
|
|
9513c1e404 | ||
|
|
d67d8b9784 | ||
|
|
fdc643bac0 | ||
|
|
9e7890a1a6 | ||
|
|
e9d876e5c9 | ||
|
|
524c15b824 | ||
|
|
0891ccb0af | ||
|
|
179288fa25 | ||
|
|
4a8b6df484 | ||
|
|
73b5e1a8cc | ||
|
|
8fa012cb73 | ||
|
|
8561c606eb | ||
|
|
abed097cad | ||
|
|
ccd7e6db18 | ||
|
|
e6c648c03e | ||
|
|
a4278cb6f2 | ||
|
|
ce9ddc049e | ||
|
|
3646fd868a | ||
|
|
288428c874 | ||
|
|
1f30d13454 | ||
|
|
6712a495ff | ||
|
|
8ac26578e3 | ||
|
|
049c7abb81 | ||
|
|
123e0066d7 | ||
|
|
3cff277132 | ||
|
|
ebb6a55ef8 | ||
|
|
a2d371fcfc | ||
|
|
b1d30c4f03 | ||
|
|
67911c12f6 | ||
|
|
25db6649f3 | ||
|
|
6a5d9df1a4 | ||
|
|
8254600b5c | ||
|
|
8b0d43a5ee | ||
|
|
99b1827048 | ||
|
|
8368f4f762 | ||
|
|
eda94bd881 | ||
|
|
2c8b3e6089 | ||
|
|
d9376ee870 | ||
|
|
94473eaac6 | ||
|
|
ff2caaac1c | ||
|
|
2fc7d91b6b | ||
|
|
f74ef641d8 | ||
|
|
1aa23d2992 | ||
|
|
2fd86301ce | ||
|
|
2c2a6a083b | ||
|
|
3462d7e6b3 | ||
|
|
d9a56d5cc5 | ||
|
|
db8a50edab | ||
|
|
e815438212 | ||
|
|
a31ce39009 | ||
|
|
8d8abf0f90 | ||
|
|
c9faf440ce | ||
|
|
14aec4f6d0 | ||
|
|
5b11135abb | ||
|
|
3ce4ad3770 | ||
|
|
65bc952a5a | ||
|
|
01be354271 | ||
|
|
2634c7c3c8 | ||
|
|
30789f86f6 | ||
|
|
404028bb59 | ||
|
|
ea25b9ea79 | ||
|
|
423940ad3b | ||
|
|
44d8ba6c40 | ||
|
|
87ea1f1727 | ||
|
|
76c70166ee | ||
|
|
40706f96a3 | ||
|
|
f04bcd61f7 | ||
|
|
d415e97672 | ||
|
|
e22a6d987c | ||
|
|
ba60421024 | ||
|
|
0dd7d75878 | ||
|
|
e77afba638 | ||
|
|
2d69336245 | ||
|
|
934f3c594e | ||
|
|
4cb56f80c6 | ||
|
|
212f19ba40 | ||
|
|
9e4f052653 | ||
|
|
afa85afebf | ||
|
|
c6514be984 | ||
|
|
719323d3c7 | ||
|
|
47fd3733b2 | ||
|
|
d7a6241a90 | ||
|
|
ce37e37fb6 | ||
|
|
536af1f547 | ||
|
|
7c69a9da0e | ||
|
|
548f72e6fc | ||
|
|
1d5aa0c7aa | ||
|
|
c00631d6dc | ||
|
|
f3e6285828 | ||
|
|
866c39283d | ||
|
|
6a98cd8d6d | ||
|
|
28000e389a | ||
|
|
a336ee086c | ||
|
|
08338c1910 | ||
|
|
3fd9147567 | ||
|
|
1cd7f58817 | ||
|
|
ebeb7cf2b1 | ||
|
|
5ff379df96 | ||
|
|
4150f1de11 | ||
|
|
a4565196c2 | ||
|
|
fb222665bf | ||
|
|
627f716eb4 | ||
|
|
1606a056b5 | ||
|
|
3ac1225808 | ||
|
|
6805f9ef2d | ||
|
|
27e0f350cc | ||
|
|
809beb77ea | ||
|
|
2ca12b8f19 | ||
|
|
7ca5106e5d | ||
|
|
3a4476374f | ||
|
|
ba6a776688 | ||
|
|
3a9d2f7eec | ||
|
|
e1b77eabdd | ||
|
|
6f95e4a36c | ||
|
|
5de7c5bc0b | ||
|
|
2523d87a75 | ||
|
|
5eae8ba403 | ||
|
|
a84555f1ce | ||
|
|
0762a78b49 | ||
|
|
735b3bed7f | ||
|
|
7bd62c2d3b | ||
|
|
a25e88894d | ||
|
|
0f2cabd469 | ||
|
|
b2f22bbeb1 | ||
|
|
c3129ae47b | ||
|
|
aa3b240653 | ||
|
|
f7c4638452 | ||
|
|
122277d3e5 | ||
|
|
bf7aaaccd3 | ||
|
|
4a752fcae1 | ||
|
|
d0b1e5b09e | ||
|
|
aab7df62e7 | ||
|
|
7991ec0990 | ||
|
|
a970a1bd6c | ||
|
|
8196e1dce4 | ||
|
|
77ebe010f2 | ||
|
|
93c84ea22e | ||
|
|
f898d094a2 | ||
|
|
096da09f87 | ||
|
|
9eca2d872c | ||
|
|
a3f88f1944 | ||
|
|
a6dada807d | ||
|
|
fee2bced12 | ||
|
|
0d35c3b39b | ||
|
|
40731fe88e | ||
|
|
cbdff442a8 | ||
|
|
4db9f7b591 | ||
|
|
823225e97f | ||
|
|
ede235ec7a | ||
|
|
827d07e4af | ||
|
|
52bee7a092 | ||
|
|
a91ca6826e | ||
|
|
73fc114a59 | ||
|
|
c7f054e4b8 | ||
|
|
fad88168e2 | ||
|
|
55ad961f19 | ||
|
|
88a224c093 | ||
|
|
31d41fbff9 | ||
|
|
9a54732fae | ||
|
|
42f8fd5982 | ||
|
|
4026e76a37 | ||
|
|
fd29767921 | ||
|
|
dc969c8305 | ||
|
|
6d0bd6366c | ||
|
|
dcc27c66d5 | ||
|
|
dbf2cd17e2 | ||
|
|
9a706dd219 | ||
|
|
95e6917d20 | ||
|
|
0a7f356464 | ||
|
|
6e56176d4c | ||
|
|
b097e0c9ff | ||
|
|
a71e411132 | ||
|
|
668371480e | ||
|
|
ef660c9c61 | ||
|
|
40c63cfa5a | ||
|
|
42c639d868 | ||
|
|
e30b8fdb7c | ||
|
|
4f57a2f41b | ||
|
|
d029ed298e | ||
|
|
35bcd51cb8 | ||
|
|
ba98850de3 | ||
|
|
61369a886f | ||
|
|
441be82e45 | ||
|
|
59d26ffa32 | ||
|
|
6ca7ef8632 | ||
|
|
d69dc14dd5 | ||
|
|
07c3ed6129 | ||
|
|
65a97388c9 | ||
|
|
5da05a58b2 | ||
|
|
bd483d0ef2 | ||
|
|
a0fb712cba | ||
|
|
b7db0f0cf9 | ||
|
|
912fe03d59 | ||
|
|
3c9a11b6b3 | ||
|
|
9ee717a89e | ||
|
|
5d0d49bd55 | ||
|
|
62633cf659 | ||
|
|
0a3ec59963 | ||
|
|
deb93e45bd | ||
|
|
55de6afc1a | ||
|
|
5631ab25e5 | ||
|
|
c82aab3b87 | ||
|
|
9a94f2d510 | ||
|
|
82d77e79c1 | ||
|
|
5a5361762a | ||
|
|
5bf55f458e | ||
|
|
305f98689f | ||
|
|
6f5ecbeeae | ||
|
|
b248b41a33 | ||
|
|
237147351e | ||
|
|
7a29782044 | ||
|
|
b56a6c0c58 | ||
|
|
64740b63b0 | ||
|
|
f8bfb2dc0c | ||
|
|
304041dfff | ||
|
|
94675b3492 | ||
|
|
917f8df46e | ||
|
|
af13c70848 | ||
|
|
4ca777bb2b | ||
|
|
a59e540051 | ||
|
|
88930d7e4d | ||
|
|
368c78d36b | ||
|
|
48c1cebb69 | ||
|
|
1b7479af28 | ||
|
|
782c028c0a | ||
|
|
6fcc8643b4 | ||
|
|
6f0259ede8 | ||
|
|
1690e5b1f1 | ||
|
|
991dd1b49c | ||
|
|
e32461361c | ||
|
|
537a18e6e8 | ||
|
|
73ad823a41 | ||
|
|
3eaf1ebf0a | ||
|
|
3ef689b7b4 | ||
|
|
60b34e93a1 | ||
|
|
b09b114aa4 | ||
|
|
2dd89afd30 | ||
|
|
7f916d72ad | ||
|
|
cdfb2b1902 | ||
|
|
6676b808f1 | ||
|
|
4685aff0b1 | ||
|
|
02ede68108 | ||
|
|
5e1a5bb6de | ||
|
|
06dec7b1fd | ||
|
|
0a8991d828 | ||
|
|
0e080d0a20 | ||
|
|
bbde4eb00c | ||
|
|
212ad4a72d | ||
|
|
4f1827e75b | ||
|
|
54f0b5442a | ||
|
|
6bf9867be3 | ||
|
|
b8ac4d7515 | ||
|
|
12cc9682fd | ||
|
|
da49e18b36 | ||
|
|
2522a42021 | ||
|
|
dc73fa12b0 | ||
|
|
af1057e7b0 | ||
|
|
e80abb24d1 | ||
|
|
4e4704174e | ||
|
|
bd51acb6a2 | ||
|
|
a319460f01 | ||
|
|
e9e7fd1d61 | ||
|
|
a600086709 | ||
|
|
176ec1b03c | ||
|
|
edf3d7596f | ||
|
|
593f515c46 | ||
|
|
3452f906c4 | ||
|
|
a3c64a504e |
@@ -3,10 +3,6 @@
|
||||
# for now: filter out annoying TASTY warnings
|
||||
common --ui_event_filters=-INFO
|
||||
|
||||
# Bazel 9 removes native rule symbols. Keep external repositories that still
|
||||
# call native.cc_library building while upstream Starlark catches up.
|
||||
common --incompatible_autoload_externally=+cc_library
|
||||
|
||||
# Don't use toolchains_llvm for the swift app build
|
||||
common:mactools --ignore_dev_dependency
|
||||
|
||||
@@ -26,8 +22,38 @@ common --per_file_copt=src/test/cpp/.*@-Wno-sign-compare
|
||||
common --per_file_copt=src/test/cpp/.*@-Wno-unknown-warning-option
|
||||
common --host_cxxopt="--std=c++23"
|
||||
|
||||
# C++ sanitizer configs for targeted Shardok safety checks.
|
||||
# Example: bazel test --config=asan //src/test/cpp/net/eagle0/shardok/library/...
|
||||
build:asan --compilation_mode=dbg
|
||||
build:asan --strip=never
|
||||
build:asan --copt=-fno-omit-frame-pointer
|
||||
build:asan --copt=-Wno-macro-redefined
|
||||
build:asan --copt=-fsanitize=address
|
||||
build:asan --linkopt=-fsanitize=address
|
||||
test:asan --test_env=ASAN_OPTIONS=detect_leaks=0:strict_init_order=1
|
||||
|
||||
build:ubsan --compilation_mode=dbg
|
||||
build:ubsan --strip=never
|
||||
build:ubsan --copt=-fno-omit-frame-pointer
|
||||
build:ubsan --copt=-Wno-macro-redefined
|
||||
build:ubsan --copt=-fsanitize=undefined
|
||||
build:ubsan --linkopt=-fsanitize=undefined
|
||||
test:ubsan --test_env=UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1
|
||||
|
||||
build:tsan --compilation_mode=dbg
|
||||
build:tsan --strip=never
|
||||
build:tsan --copt=-fno-omit-frame-pointer
|
||||
build:tsan --copt=-Wno-macro-redefined
|
||||
build:tsan --copt=-fsanitize=thread
|
||||
build:tsan --linkopt=-fsanitize=thread
|
||||
test:tsan --test_env=TSAN_OPTIONS=halt_on_error=1
|
||||
|
||||
common --javacopt="-Xlint:-options"
|
||||
|
||||
# Prefer protobuf's prebuilt protoc toolchain instead of building protoc from
|
||||
# source when compatible binaries are available.
|
||||
common --@com_google_protobuf//bazel/toolchains:prefer_prebuilt_protoc
|
||||
|
||||
# std::filesystem and other modern C++ deps require macOS 10.15+.
|
||||
common:macos --macos_minimum_os=10.15
|
||||
common:macos --host_macos_minimum_os=10.15
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
Checks: >
|
||||
-*,
|
||||
bugprone-*,
|
||||
performance-*,
|
||||
readability-*,
|
||||
modernize-*,
|
||||
cppcoreguidelines-*,
|
||||
-bugprone-easily-swappable-parameters,
|
||||
-cppcoreguidelines-avoid-magic-numbers,
|
||||
-cppcoreguidelines-macro-usage,
|
||||
-cppcoreguidelines-owning-memory,
|
||||
-cppcoreguidelines-pro-bounds-array-to-pointer-decay,
|
||||
-cppcoreguidelines-pro-bounds-constant-array-index,
|
||||
-cppcoreguidelines-pro-bounds-pointer-arithmetic,
|
||||
-cppcoreguidelines-pro-type-const-cast,
|
||||
-cppcoreguidelines-pro-type-reinterpret-cast,
|
||||
-cppcoreguidelines-pro-type-union-access,
|
||||
-cppcoreguidelines-pro-type-vararg,
|
||||
-modernize-use-trailing-return-type,
|
||||
-readability-convert-member-functions-to-static,
|
||||
-readability-function-cognitive-complexity,
|
||||
-readability-identifier-length,
|
||||
-readability-magic-numbers
|
||||
WarningsAsErrors: ''
|
||||
HeaderFilterRegex: 'src/(main|test)/cpp/net/eagle0/(shardok|common)/.*'
|
||||
FormatStyle: file
|
||||
CheckOptions:
|
||||
readability-braces-around-statements.ShortStatementLines: '1'
|
||||
readability-function-size.LineThreshold: '160'
|
||||
readability-function-size.StatementThreshold: '80'
|
||||
readability-function-size.BranchThreshold: '20'
|
||||
modernize-use-nullptr.NullMacros: 'NULL'
|
||||
@@ -0,0 +1,9 @@
|
||||
name: Setup Bazel
|
||||
description: Ensure Bazel is installed on a self-hosted runner.
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Ensure Bazel installed
|
||||
shell: bash
|
||||
run: ./ci/github_actions/ensure_bazel_installed.sh
|
||||
@@ -1,76 +0,0 @@
|
||||
name: Artifact Storage Check
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run every 6 hours
|
||||
- cron: '0 */6 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: write
|
||||
|
||||
jobs:
|
||||
cleanup-expired:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Delete expired artifacts and artifacts older than 3 days
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
echo "Fetching all artifacts..."
|
||||
gh api "repos/${{ github.repository }}/actions/artifacts" \
|
||||
--paginate -q '.artifacts[] | "\(.id)\t\(.created_at)\t\(.expired)\t\(.name)"' > /tmp/all_artifacts.txt
|
||||
|
||||
total=$(wc -l < /tmp/all_artifacts.txt)
|
||||
echo "Found $total total artifacts"
|
||||
|
||||
cutoff=$(date -u -d '3 days ago' '+%Y-%m-%dT%H:%M:%SZ')
|
||||
echo "Deleting expired artifacts and artifacts created before $cutoff"
|
||||
|
||||
deleted=0
|
||||
while IFS=$'\t' read -r id created_at expired name; do
|
||||
if [[ "$expired" == "true" || "$created_at" < "$cutoff" ]]; then
|
||||
gh api -X DELETE "repos/${{ github.repository }}/actions/artifacts/$id" 2>/dev/null && deleted=$((deleted + 1))
|
||||
if [ $((deleted % 50)) -eq 0 ]; then
|
||||
echo "Deleted $deleted artifacts so far..."
|
||||
fi
|
||||
fi
|
||||
done < /tmp/all_artifacts.txt
|
||||
|
||||
echo "Cleanup complete. Deleted $deleted artifacts out of $total total."
|
||||
rm -f /tmp/all_artifacts.txt
|
||||
|
||||
check-storage:
|
||||
needs: cleanup-expired
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check artifact storage size
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
# Calculate active artifact storage. The artifacts API can list expired
|
||||
# artifacts until they are explicitly deleted, so do not count them.
|
||||
total_bytes=$(gh api "repos/${{ github.repository }}/actions/artifacts" \
|
||||
--paginate -q '.artifacts[] | select(.expired == false) | .size_in_bytes' | awk '{sum+=$1} END {print sum}')
|
||||
|
||||
total_mb=$(( ${total_bytes:-0} / 1024 / 1024 ))
|
||||
echo "Total artifact storage: ${total_mb} MB"
|
||||
|
||||
# Fail if over 500MB
|
||||
if [ "$total_mb" -gt 500 ]; then
|
||||
echo "::error::Artifact storage is ${total_mb} MB, which exceeds the 500 MB threshold!"
|
||||
echo ""
|
||||
echo "Largest artifacts:"
|
||||
# Save to temp file to avoid SIGPIPE/broken pipe errors with head
|
||||
gh api "repos/${{ github.repository }}/actions/artifacts" \
|
||||
--paginate -q '.artifacts[] | select(.expired == false) | "\(.size_in_bytes)\t\(.name)\t\(.created_at)"' > /tmp/artifacts.txt
|
||||
sort -rn /tmp/artifacts.txt | head -20 | \
|
||||
awk -F'\t' '{printf "%d MB\t%s\t%s\n", $1/1024/1024, $2, $3}'
|
||||
rm -f /tmp/artifacts.txt
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Storage is within acceptable limits."
|
||||
@@ -6,12 +6,38 @@ on:
|
||||
paths:
|
||||
- 'src/main/go/net/eagle0/authservice/**'
|
||||
- 'src/main/go/net/eagle0/authcli/**'
|
||||
- 'src/main/go/net/eagle0/common/**'
|
||||
- 'src/main/go/net/eagle0/util/**'
|
||||
- 'src/main/protobuf/net/eagle0/eagle/api/auth.proto'
|
||||
- 'src/main/protobuf/net/eagle0/eagle/api/admin/**'
|
||||
- 'src/main/protobuf/net/eagle0/eagle/internal/auth_internal.proto'
|
||||
- 'src/main/resources/net/eagle0/attributions.json'
|
||||
- 'ci/BUILD.bazel'
|
||||
- 'ci/github_actions/ensure_bazel_installed.sh'
|
||||
- '.github/actions/setup-bazel/**'
|
||||
- 'go.mod'
|
||||
- 'go.sum'
|
||||
- 'MODULE.bazel'
|
||||
- 'MODULE.bazel.lock'
|
||||
- '.bazelrc'
|
||||
- '.github/workflows/auth_build.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'src/main/go/net/eagle0/authservice/**'
|
||||
- 'src/main/go/net/eagle0/authcli/**'
|
||||
- 'src/main/go/net/eagle0/common/**'
|
||||
- 'src/main/go/net/eagle0/util/**'
|
||||
- 'src/main/protobuf/net/eagle0/eagle/api/auth.proto'
|
||||
- 'src/main/protobuf/net/eagle0/eagle/api/admin/**'
|
||||
- 'src/main/protobuf/net/eagle0/eagle/internal/auth_internal.proto'
|
||||
- 'src/main/resources/net/eagle0/attributions.json'
|
||||
- 'ci/BUILD.bazel'
|
||||
- 'ci/github_actions/ensure_bazel_installed.sh'
|
||||
- '.github/actions/setup-bazel/**'
|
||||
- 'go.mod'
|
||||
- 'go.sum'
|
||||
- 'MODULE.bazel'
|
||||
- 'MODULE.bazel.lock'
|
||||
- '.bazelrc'
|
||||
- '.github/workflows/auth_build.yml'
|
||||
workflow_dispatch:
|
||||
@@ -26,8 +52,8 @@ permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: auth-build-deploy
|
||||
cancel-in-progress: false
|
||||
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || 'deploy' }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
jobs:
|
||||
build-auth:
|
||||
@@ -44,6 +70,12 @@ jobs:
|
||||
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
|
||||
@@ -56,7 +88,7 @@ jobs:
|
||||
lfs: false
|
||||
|
||||
- name: Ensure Bazel installed
|
||||
run: ./ci/github_actions/ensure_bazel_installed.sh
|
||||
uses: ./.github/actions/setup-bazel
|
||||
|
||||
- name: Build Auth Server Docker image
|
||||
id: build-auth
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
name: Bazel Cache Cleanup
|
||||
|
||||
on:
|
||||
# Disabled: bazel clean fails when another runner shares the output_base
|
||||
# on /Volumes/remote_cache (unlinkat "Directory not empty" race).
|
||||
# See https://github.com/nolen777/eagle0/issues/TBD for details.
|
||||
# schedule:
|
||||
# - cron: '0 0 * * 0'
|
||||
workflow_dispatch: # Allow manual trigger
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
runs-on: [self-hosted, bazel]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
lfs: false
|
||||
|
||||
- name: Show disk usage before cleanup
|
||||
run: |
|
||||
echo "=== Disk usage before cleanup ==="
|
||||
BAZEL_USER_ROOT="/private/var/tmp/_bazel_$(whoami)"
|
||||
echo "Bazel user root: $BAZEL_USER_ROOT"
|
||||
du -sh "$BAZEL_USER_ROOT" 2>/dev/null || echo "Not found"
|
||||
df -h .
|
||||
|
||||
- name: Run bazel clean
|
||||
run: |
|
||||
echo "=== Running bazel clean ==="
|
||||
bazel clean
|
||||
echo "Clean complete"
|
||||
|
||||
- name: Show disk usage after cleanup
|
||||
run: |
|
||||
echo "=== Disk usage after cleanup ==="
|
||||
BAZEL_USER_ROOT="/private/var/tmp/_bazel_$(whoami)"
|
||||
du -sh "$BAZEL_USER_ROOT" 2>/dev/null || echo "Not found"
|
||||
df -h .
|
||||
+46
-3
@@ -1,7 +1,16 @@
|
||||
name: Bazel Cache Parity
|
||||
name: Bazel Cache Maintenance
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
target:
|
||||
description: 'Maintenance target'
|
||||
required: true
|
||||
default: 'parity'
|
||||
type: choice
|
||||
options:
|
||||
- parity
|
||||
- clean
|
||||
schedule:
|
||||
- cron: '17 11 * * *'
|
||||
|
||||
@@ -14,6 +23,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
warm-cache:
|
||||
if: github.event_name == 'schedule' || github.event.inputs.target == 'parity'
|
||||
runs-on: [self-hosted, bazel]
|
||||
|
||||
steps:
|
||||
@@ -46,12 +56,13 @@ jobs:
|
||||
persist-credentials: false
|
||||
lfs: false
|
||||
- name: Ensure Bazel installed
|
||||
run: ./ci/github_actions/ensure_bazel_installed.sh
|
||||
uses: ./.github/actions/setup-bazel
|
||||
- name: Warm remote cache
|
||||
run: ./scripts/check_bazel_remote_cache_parity.sh warm
|
||||
|
||||
verify-cache:
|
||||
needs: warm-cache
|
||||
if: github.event_name == 'schedule' || github.event.inputs.target == 'parity'
|
||||
runs-on: [self-hosted, bazel]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -88,6 +99,38 @@ jobs:
|
||||
persist-credentials: false
|
||||
lfs: false
|
||||
- name: Ensure Bazel installed
|
||||
run: ./ci/github_actions/ensure_bazel_installed.sh
|
||||
uses: ./.github/actions/setup-bazel
|
||||
- name: Verify remote cache parity
|
||||
run: ./scripts/check_bazel_remote_cache_parity.sh verify
|
||||
|
||||
clean-cache:
|
||||
if: github.event_name == 'workflow_dispatch' && github.event.inputs.target == 'clean'
|
||||
runs-on: [self-hosted, bazel]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
lfs: false
|
||||
|
||||
- name: Show disk usage before cleanup
|
||||
run: |
|
||||
echo "=== Disk usage before cleanup ==="
|
||||
BAZEL_USER_ROOT="/private/var/tmp/_bazel_$(whoami)"
|
||||
echo "Bazel user root: $BAZEL_USER_ROOT"
|
||||
du -sh "$BAZEL_USER_ROOT" 2>/dev/null || echo "Not found"
|
||||
df -h .
|
||||
|
||||
- name: Run bazel clean
|
||||
run: |
|
||||
echo "=== Running bazel clean ==="
|
||||
bazel clean
|
||||
echo "Clean complete"
|
||||
|
||||
- name: Show disk usage after cleanup
|
||||
run: |
|
||||
echo "=== Disk usage after cleanup ==="
|
||||
BAZEL_USER_ROOT="/private/var/tmp/_bazel_$(whoami)"
|
||||
du -sh "$BAZEL_USER_ROOT" 2>/dev/null || echo "Not found"
|
||||
df -h .
|
||||
@@ -11,6 +11,8 @@ on:
|
||||
- 'BUILD.bazel'
|
||||
- '.bazelrc'
|
||||
- 'ci/github_actions/ensure_bazel_installed.sh'
|
||||
- 'ci/github_actions/summarize_bazel_bep.py'
|
||||
- '.github/actions/setup-bazel/**'
|
||||
- '.github/workflows/bazel_test.yml'
|
||||
- '!src/main/csharp/**'
|
||||
- '!src/test/csharp/**'
|
||||
@@ -23,6 +25,8 @@ on:
|
||||
- 'BUILD.bazel'
|
||||
- '.bazelrc'
|
||||
- 'ci/github_actions/ensure_bazel_installed.sh'
|
||||
- 'ci/github_actions/summarize_bazel_bep.py'
|
||||
- '.github/actions/setup-bazel/**'
|
||||
- '.github/workflows/bazel_test.yml'
|
||||
- '!src/main/csharp/**'
|
||||
- '!src/test/csharp/**'
|
||||
@@ -106,10 +110,13 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
lfs: false
|
||||
- name: Ensure Bazel installed
|
||||
run: ./ci/github_actions/ensure_bazel_installed.sh
|
||||
- name: Setup Bazel
|
||||
uses: ./.github/actions/setup-bazel
|
||||
- name: Run tests
|
||||
run: bazel test --build_event_json_file=test.json //src/test/... //src/main/go/...
|
||||
- name: Summarize Bazel build metrics
|
||||
if: always()
|
||||
run: python3 ci/github_actions/summarize_bazel_bep.py test.json
|
||||
- name: Collect failed test logs
|
||||
if: always()
|
||||
run: |
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
name: Blob Cleanup
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run daily at 04:00 UTC
|
||||
- cron: '0 4 * * *'
|
||||
workflow_dispatch: # Allow manual trigger
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
runs-on: [self-hosted, bazel]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
lfs: false
|
||||
clean: false
|
||||
|
||||
- name: Ensure Bazel installed
|
||||
run: ./ci/github_actions/ensure_bazel_installed.sh
|
||||
|
||||
- name: Clean up unreferenced blobs
|
||||
env:
|
||||
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
run: |
|
||||
bazel run //src/main/go/net/eagle0/build/blob_cleanup:blob_cleanup -- --min-age=1h
|
||||
@@ -22,65 +22,13 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build-sysroot-amd64:
|
||||
if: ${{ inputs.architecture == 'amd64' || inputs.architecture == 'both' }}
|
||||
build-sysroot:
|
||||
name: Build Linux Sysroot (${{ matrix.architecture }})
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJSON(inputs.architecture == 'both' && '{"include":[{"architecture":"amd64","build_script":"./tools/sysroot/build_sysroot.sh","artifact_name":"ubuntu-noble-sysroot-amd64","archive_name":"ubuntu_noble_amd64_sysroot.tar.xz","checksum_name":"ubuntu_noble_amd64_sysroot.sha256","module_name":"linux_sysroot"},{"architecture":"arm64","build_script":"./tools/sysroot/build_sysroot_arm64.sh","artifact_name":"ubuntu-noble-sysroot-arm64","archive_name":"ubuntu_noble_arm64_sysroot.tar.xz","checksum_name":"ubuntu_noble_arm64_sysroot.sha256","module_name":"linux_sysroot_arm64"}]}' || inputs.architecture == 'arm64' && '{"include":[{"architecture":"arm64","build_script":"./tools/sysroot/build_sysroot_arm64.sh","artifact_name":"ubuntu-noble-sysroot-arm64","archive_name":"ubuntu_noble_arm64_sysroot.tar.xz","checksum_name":"ubuntu_noble_arm64_sysroot.sha256","module_name":"linux_sysroot_arm64"}]}' || '{"include":[{"architecture":"amd64","build_script":"./tools/sysroot/build_sysroot.sh","artifact_name":"ubuntu-noble-sysroot-amd64","archive_name":"ubuntu_noble_amd64_sysroot.tar.xz","checksum_name":"ubuntu_noble_amd64_sysroot.sha256","module_name":"linux_sysroot"}]}') }}
|
||||
|
||||
- name: Build sysroot
|
||||
run: ./tools/sysroot/build_sysroot.sh
|
||||
|
||||
- name: Upload sysroot artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ubuntu-noble-sysroot-amd64
|
||||
path: tools/sysroot/output/
|
||||
retention-days: 1
|
||||
|
||||
- name: Install AWS CLI
|
||||
run: |
|
||||
if ! command -v aws &> /dev/null; then
|
||||
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
|
||||
unzip -q awscliv2.zip
|
||||
sudo ./aws/install
|
||||
fi
|
||||
|
||||
- name: Upload to DigitalOcean Spaces
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.SECRET_KEY }}
|
||||
run: |
|
||||
# Upload sysroot tarball to DO Spaces
|
||||
aws s3 cp tools/sysroot/output/ubuntu_noble_amd64_sysroot.tar.xz \
|
||||
s3://eagle0-sysroot/${{ inputs.version }}/ubuntu_noble_amd64_sysroot.tar.xz \
|
||||
--endpoint-url https://sfo3.digitaloceanspaces.com \
|
||||
--acl public-read
|
||||
|
||||
# Upload sha256 file
|
||||
aws s3 cp tools/sysroot/output/ubuntu_noble_amd64_sysroot.sha256 \
|
||||
s3://eagle0-sysroot/${{ inputs.version }}/ubuntu_noble_amd64_sysroot.sha256 \
|
||||
--endpoint-url https://sfo3.digitaloceanspaces.com \
|
||||
--acl public-read
|
||||
|
||||
echo ""
|
||||
echo "=== AMD64 Sysroot uploaded ==="
|
||||
echo "URL: https://eagle0-sysroot.sfo3.digitaloceanspaces.com/${{ inputs.version }}/ubuntu_noble_amd64_sysroot.tar.xz"
|
||||
echo "SHA256: $(cat tools/sysroot/output/ubuntu_noble_amd64_sysroot.sha256)"
|
||||
echo ""
|
||||
echo "Update MODULE.bazel with:"
|
||||
echo "sysroot("
|
||||
echo " name = \"linux_sysroot\","
|
||||
echo " sha256 = \"$(cat tools/sysroot/output/ubuntu_noble_amd64_sysroot.sha256)\","
|
||||
echo " urls = [\"https://eagle0-sysroot.sfo3.digitaloceanspaces.com/${{ inputs.version }}/ubuntu_noble_amd64_sysroot.tar.xz\"],"
|
||||
echo ")"
|
||||
|
||||
build-sysroot-arm64:
|
||||
if: ${{ inputs.architecture == 'arm64' || inputs.architecture == 'both' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
@@ -88,20 +36,22 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up QEMU for ARM64 emulation
|
||||
if: ${{ matrix.architecture == 'arm64' }}
|
||||
uses: docker/setup-qemu-action@v4
|
||||
with:
|
||||
platforms: arm64
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
if: ${{ matrix.architecture == 'arm64' }}
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Build ARM64 sysroot
|
||||
run: ./tools/sysroot/build_sysroot_arm64.sh
|
||||
- name: Build sysroot
|
||||
run: ${{ matrix.build_script }}
|
||||
|
||||
- name: Upload sysroot artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ubuntu-noble-sysroot-arm64
|
||||
name: ${{ matrix.artifact_name }}
|
||||
path: tools/sysroot/output/
|
||||
retention-days: 1
|
||||
|
||||
@@ -119,25 +69,25 @@ jobs:
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.SECRET_KEY }}
|
||||
run: |
|
||||
# Upload sysroot tarball to DO Spaces
|
||||
aws s3 cp tools/sysroot/output/ubuntu_noble_arm64_sysroot.tar.xz \
|
||||
s3://eagle0-sysroot/${{ inputs.version }}/ubuntu_noble_arm64_sysroot.tar.xz \
|
||||
aws s3 cp "tools/sysroot/output/${{ matrix.archive_name }}" \
|
||||
"s3://eagle0-sysroot/${{ inputs.version }}/${{ matrix.archive_name }}" \
|
||||
--endpoint-url https://sfo3.digitaloceanspaces.com \
|
||||
--acl public-read
|
||||
|
||||
# Upload sha256 file
|
||||
aws s3 cp tools/sysroot/output/ubuntu_noble_arm64_sysroot.sha256 \
|
||||
s3://eagle0-sysroot/${{ inputs.version }}/ubuntu_noble_arm64_sysroot.sha256 \
|
||||
aws s3 cp "tools/sysroot/output/${{ matrix.checksum_name }}" \
|
||||
"s3://eagle0-sysroot/${{ inputs.version }}/${{ matrix.checksum_name }}" \
|
||||
--endpoint-url https://sfo3.digitaloceanspaces.com \
|
||||
--acl public-read
|
||||
|
||||
echo ""
|
||||
echo "=== ARM64 Sysroot uploaded ==="
|
||||
echo "URL: https://eagle0-sysroot.sfo3.digitaloceanspaces.com/${{ inputs.version }}/ubuntu_noble_arm64_sysroot.tar.xz"
|
||||
echo "SHA256: $(cat tools/sysroot/output/ubuntu_noble_arm64_sysroot.sha256)"
|
||||
echo "=== ${{ matrix.architecture }} Sysroot uploaded ==="
|
||||
echo "URL: https://eagle0-sysroot.sfo3.digitaloceanspaces.com/${{ inputs.version }}/${{ matrix.archive_name }}"
|
||||
echo "SHA256: $(cat "tools/sysroot/output/${{ matrix.checksum_name }}")"
|
||||
echo ""
|
||||
echo "Update MODULE.bazel with:"
|
||||
echo "sysroot("
|
||||
echo " name = \"linux_sysroot_arm64\","
|
||||
echo " sha256 = \"$(cat tools/sysroot/output/ubuntu_noble_arm64_sysroot.sha256)\","
|
||||
echo " urls = [\"https://eagle0-sysroot.sfo3.digitaloceanspaces.com/${{ inputs.version }}/ubuntu_noble_arm64_sysroot.tar.xz\"],"
|
||||
echo " name = \"${{ matrix.module_name }}\","
|
||||
echo " sha256 = \"$(cat "tools/sysroot/output/${{ matrix.checksum_name }}")\","
|
||||
echo " urls = [\"https://eagle0-sysroot.sfo3.digitaloceanspaces.com/${{ inputs.version }}/${{ matrix.archive_name }}\"],"
|
||||
echo ")"
|
||||
|
||||
@@ -19,6 +19,31 @@ on:
|
||||
- 'src/main/resources/**'
|
||||
- 'ci/BUILD.bazel'
|
||||
- 'ci/github_actions/ensure_bazel_installed.sh'
|
||||
- '.github/actions/setup-bazel/**'
|
||||
- 'MODULE.bazel'
|
||||
- 'MODULE.bazel.lock'
|
||||
- '.bazelrc'
|
||||
- 'docker-compose.prod.yml'
|
||||
- 'nginx/**'
|
||||
- '.github/workflows/docker_build.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
# Note: Auth changes trigger auth_build.yml instead
|
||||
# Note: Windows installer changes trigger installer_build.yml instead
|
||||
- 'src/main/go/net/eagle0/admin_server/**'
|
||||
- 'src/main/go/net/eagle0/common/**'
|
||||
- 'src/main/go/net/eagle0/util/**'
|
||||
- 'src/main/scala/**'
|
||||
- 'src/main/protobuf/**'
|
||||
- '!src/main/protobuf/net/eagle0/eagle/api/auth.proto'
|
||||
- '!src/main/protobuf/net/eagle0/eagle/internal/auth_internal.proto'
|
||||
- 'src/main/resources/**'
|
||||
- 'ci/BUILD.bazel'
|
||||
- 'ci/github_actions/ensure_bazel_installed.sh'
|
||||
- 'ci/github_actions/fetch_lfs.sh'
|
||||
- '.github/actions/setup-bazel/**'
|
||||
- 'go.mod'
|
||||
- 'go.sum'
|
||||
- 'MODULE.bazel'
|
||||
- 'MODULE.bazel.lock'
|
||||
- '.bazelrc'
|
||||
@@ -37,8 +62,8 @@ on:
|
||||
# The build-all job checks if it's still the latest commit on main and skips if not,
|
||||
# so intermediate commits don't waste time building when a newer one is already queued.
|
||||
concurrency:
|
||||
group: docker-build-deploy
|
||||
cancel-in-progress: false
|
||||
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || 'deploy' }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -54,8 +79,10 @@ jobs:
|
||||
eagle_image_tag: ${{ steps.push-images.outputs.eagle_image_tag }}
|
||||
admin_image_tag: ${{ steps.push-images.outputs.admin_image_tag }}
|
||||
jfr_sidecar_image_tag: ${{ steps.push-images.outputs.jfr_sidecar_image_tag }}
|
||||
skip_build: ${{ steps.check-latest.outputs.skip }}
|
||||
steps:
|
||||
- name: Skip if superseded
|
||||
if: github.event_name != 'pull_request'
|
||||
id: check-latest
|
||||
run: |
|
||||
# Check if there's a newer run of this workflow waiting in the queue.
|
||||
@@ -96,7 +123,7 @@ jobs:
|
||||
|
||||
- name: Ensure Bazel installed
|
||||
if: steps.check-latest.outputs.skip != 'true'
|
||||
run: ./ci/github_actions/ensure_bazel_installed.sh
|
||||
uses: ./.github/actions/setup-bazel
|
||||
|
||||
- name: Fetch LFS files needed for admin server
|
||||
if: steps.check-latest.outputs.skip != 'true'
|
||||
@@ -142,7 +169,7 @@ jobs:
|
||||
echo "JFR Sidecar: $JFR_PATH"
|
||||
|
||||
- name: Upload warmup binary
|
||||
if: steps.check-latest.outputs.skip != 'true'
|
||||
if: steps.check-latest.outputs.skip != 'true' && github.event_name != 'pull_request'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: warmup-binary
|
||||
@@ -162,7 +189,7 @@ jobs:
|
||||
|
||||
- name: Push all images to DO registry
|
||||
id: push-images
|
||||
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
|
||||
if: steps.check-latest.outputs.skip != 'true' && (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true'))
|
||||
env:
|
||||
DOCKER_CONFIG: ${{ github.workspace }}/.docker
|
||||
run: |
|
||||
@@ -230,7 +257,7 @@ jobs:
|
||||
deploy:
|
||||
runs-on: [self-hosted, bazel]
|
||||
needs: [build-all]
|
||||
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
|
||||
if: needs.build-all.outputs.skip_build != 'true' && (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true'))
|
||||
environment: production
|
||||
env:
|
||||
EAGLE_IMAGE: ${{ needs.build-all.outputs.eagle_image_tag }}
|
||||
@@ -241,6 +268,7 @@ jobs:
|
||||
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
||||
GPT_MODEL_NAME: ${{ secrets.GPT_MODEL_NAME }}
|
||||
EAGLE_ENABLE_S3: ${{ secrets.EAGLE_ENABLE_S3 }}
|
||||
DO_SPACES_ENDPOINT: ${{ secrets.DO_SPACES_ENDPOINT }}
|
||||
DO_SPACES_ACCESS_KEY: ${{ secrets.DO_SPACES_ACCESS_KEY }}
|
||||
DO_SPACES_SECRET_KEY: ${{ secrets.DO_SPACES_SECRET_KEY }}
|
||||
# Admin server uses different credentials (for eagle0-assets bucket)
|
||||
@@ -547,7 +575,7 @@ jobs:
|
||||
|
||||
cleanup:
|
||||
needs: [build-all, deploy]
|
||||
if: always()
|
||||
if: always() && github.event_name != 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Delete warmup-binary artifact
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
name: Eagle Build
|
||||
|
||||
on:
|
||||
# Main pushes are covered by docker_build.yml which builds the same target
|
||||
pull_request:
|
||||
paths:
|
||||
- 'src/main/scala/**'
|
||||
- 'src/main/protobuf/net/eagle0/eagle/**'
|
||||
- 'src/main/protobuf/net/eagle0/common/**'
|
||||
- 'src/main/go/net/eagle0/build/**'
|
||||
- 'go.mod'
|
||||
- 'go.sum'
|
||||
- 'WORKSPACE'
|
||||
- 'MODULE.bazel'
|
||||
- 'MODULE.bazel.lock'
|
||||
- 'BUILD.bazel'
|
||||
- '.bazelrc'
|
||||
- 'ci/github_actions/ensure_bazel_installed.sh'
|
||||
- 'scripts/build_eagle_ci.sh'
|
||||
- '.github/workflows/eagle_build.yml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: [self-hosted, bazel]
|
||||
|
||||
steps:
|
||||
- name: Ensure Git LFS available for checkout
|
||||
run: |
|
||||
COMMON_PATHS=(/opt/homebrew/bin /usr/local/bin)
|
||||
for path in "${COMMON_PATHS[@]}"; do
|
||||
if [ -d "$path" ]; then
|
||||
echo "$path" >> "$GITHUB_PATH"
|
||||
export PATH="$path:$PATH"
|
||||
fi
|
||||
done
|
||||
|
||||
if command -v git-lfs >/dev/null 2>&1; then
|
||||
git-lfs version
|
||||
exit 0
|
||||
fi
|
||||
|
||||
brew install git-lfs
|
||||
git-lfs version
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
lfs: false
|
||||
- name: Ensure Bazel installed
|
||||
run: ./ci/github_actions/ensure_bazel_installed.sh
|
||||
- name: Build Eagle server
|
||||
run: ./scripts/build_eagle_ci.sh
|
||||
@@ -1,66 +0,0 @@
|
||||
name: Go Services Build
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'src/main/go/net/eagle0/admin_server/**'
|
||||
- 'src/main/go/net/eagle0/authcli/**'
|
||||
- 'src/main/go/net/eagle0/authservice/**'
|
||||
- 'src/main/go/net/eagle0/common/**'
|
||||
- 'src/main/go/net/eagle0/util/**'
|
||||
- 'src/main/protobuf/net/eagle0/eagle/api/auth.proto'
|
||||
- 'src/main/protobuf/net/eagle0/eagle/api/admin/**'
|
||||
- 'src/main/protobuf/net/eagle0/eagle/internal/auth_internal.proto'
|
||||
- 'src/main/resources/net/eagle0/attributions.json'
|
||||
- 'ci/BUILD.bazel'
|
||||
- 'ci/github_actions/ensure_bazel_installed.sh'
|
||||
- 'ci/github_actions/fetch_lfs.sh'
|
||||
- 'go.mod'
|
||||
- 'go.sum'
|
||||
- 'MODULE.bazel'
|
||||
- 'MODULE.bazel.lock'
|
||||
- '.bazelrc'
|
||||
- '.github/workflows/go_services_build.yml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: [self-hosted, bazel]
|
||||
|
||||
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
|
||||
rm -f .git/hooks/post-checkout .git/hooks/post-merge .git/hooks/pre-push
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
env:
|
||||
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig-no-lfs
|
||||
GIT_CONFIG_NOSYSTEM: "1"
|
||||
with:
|
||||
persist-credentials: false
|
||||
lfs: false
|
||||
|
||||
- name: Ensure Bazel installed
|
||||
run: ./ci/github_actions/ensure_bazel_installed.sh
|
||||
|
||||
- name: Fetch LFS files needed for admin server
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: ./ci/github_actions/fetch_lfs.sh --include="src/main/go/net/eagle0/admin_server/static/tiles/*"
|
||||
|
||||
- name: Build Go service Docker images
|
||||
run: bazel build --stamp //ci:admin_server_image //ci:auth_server_image
|
||||
@@ -5,12 +5,14 @@ on:
|
||||
branches: [ "main" ]
|
||||
paths:
|
||||
- ".github/workflows/installer_build.yml"
|
||||
- ".github/actions/setup-bazel/**"
|
||||
- "ci/github_actions/ensure_bazel_installed.sh"
|
||||
- ".bazelrc"
|
||||
- "src/main/go/net/eagle0/clients/win/installer/**"
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/installer_build.yml"
|
||||
- ".github/actions/setup-bazel/**"
|
||||
- "ci/github_actions/ensure_bazel_installed.sh"
|
||||
- ".bazelrc"
|
||||
- "src/main/go/net/eagle0/clients/win/installer/**"
|
||||
@@ -48,8 +50,8 @@ jobs:
|
||||
lfs: false
|
||||
clean: false
|
||||
|
||||
- name: Ensure Bazel installed
|
||||
run: ./ci/github_actions/ensure_bazel_installed.sh
|
||||
- name: Setup Bazel
|
||||
uses: ./.github/actions/setup-bazel
|
||||
|
||||
- name: Build Go installer for Windows
|
||||
env:
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
steps:
|
||||
- name: Check for relevant changes since last successful run
|
||||
id: check
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
if (context.eventName === 'workflow_dispatch') {
|
||||
@@ -160,20 +160,27 @@ jobs:
|
||||
- name: Ensure Unity version installed
|
||||
run: ./ci/github_actions/ensure_unity_installed.sh ios
|
||||
|
||||
- name: Restore Unity Library cache
|
||||
run: ./ci/github_actions/unity_library_cache.sh restore ios
|
||||
|
||||
- name: Build iOS Unity Project
|
||||
id: build
|
||||
run: |
|
||||
./ci/github_actions/build_unity_ios.sh "$EAGLE0_BUILD_DIR/eagle0iOS"
|
||||
|
||||
- name: Upload Addressables to CDN
|
||||
- name: Save Unity Library cache
|
||||
if: success()
|
||||
run: ./ci/github_actions/unity_library_cache.sh save ios
|
||||
|
||||
- name: Upload Addressables to CDN
|
||||
if: success() && github.event.inputs.skip_upload != 'true'
|
||||
env:
|
||||
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
run: ./ci/github_actions/upload_addressables.sh iOS
|
||||
|
||||
- name: Purge CDN cache for iOS addressables
|
||||
if: success()
|
||||
if: success() && github.event.inputs.skip_upload != 'true'
|
||||
env:
|
||||
DO_CDN_PAT: ${{ secrets.DO_CDN_PAT }}
|
||||
run: |
|
||||
@@ -249,16 +256,28 @@ jobs:
|
||||
tar -xzf "$RUNNER_TEMP/eagle0-ios-project.tar.gz" -C "$EAGLE0_BUILD_DIR"
|
||||
|
||||
- name: Delete generated iOS project artifact
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
artifact_id=$(gh api "repos/${{ github.repository }}/actions/artifacts" \
|
||||
--paginate \
|
||||
--jq '.artifacts[] | select(.name == "eagle0-ios-project-${{ github.run_id }}") | .id')
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const artifactName = 'eagle0-ios-project-${{ github.run_id }}';
|
||||
const artifacts = await github.paginate(github.rest.actions.listWorkflowRunArtifacts, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: context.runId,
|
||||
});
|
||||
|
||||
if [ -n "$artifact_id" ]; then
|
||||
gh api -X DELETE "repos/${{ github.repository }}/actions/artifacts/$artifact_id"
|
||||
fi
|
||||
const artifact = artifacts.find(a => a.name === artifactName);
|
||||
if (artifact === undefined) {
|
||||
console.log(`Artifact not found: ${artifactName}`);
|
||||
return;
|
||||
}
|
||||
|
||||
await github.rest.actions.deleteArtifact({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
artifact_id: artifact.id,
|
||||
});
|
||||
console.log(`Deleted artifact: ${artifactName} (${artifact.id})`);
|
||||
|
||||
- name: Install Signing Certificate
|
||||
env:
|
||||
|
||||
+218
-152
@@ -14,6 +14,7 @@ on:
|
||||
- "src/main/go/net/eagle0/build/mac_build_handler/**"
|
||||
- "scripts/build_protos.sh"
|
||||
- "scripts/build_mac_plugin.sh"
|
||||
- "scripts/build_sparkle_plugin.sh"
|
||||
- "scripts/inject_sparkle.sh"
|
||||
- "scripts/codesign_mac_app.sh"
|
||||
- "scripts/notarize_submit.sh"
|
||||
@@ -21,11 +22,14 @@ on:
|
||||
- "ci/github_actions/build_mac.sh"
|
||||
- "ci/github_actions/build_unity_mac.sh"
|
||||
- "ci/github_actions/ensure_bazel_installed.sh"
|
||||
- ".github/actions/setup-bazel/**"
|
||||
- "ci/github_actions/detect_addressables_changes.sh"
|
||||
- "ci/github_actions/upload_addressables.sh"
|
||||
- "ci/github_actions/ensure_unity_installed.sh"
|
||||
- "src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectVersion.txt"
|
||||
- ".bazelrc"
|
||||
- "ci/mac/**"
|
||||
- "src/main/objc/net/eagle0/clients/unity/sparkle/**"
|
||||
pull_request:
|
||||
# On PRs, only build Mac when Mac-specific files change. Shared C#/proto
|
||||
# changes are covered by the Windows build — if it passes, Mac will too.
|
||||
@@ -34,6 +38,11 @@ on:
|
||||
- "src/main/go/net/eagle0/build/mac_build_handler/**"
|
||||
- "scripts/build_mac_plugin.sh"
|
||||
- "scripts/build_sparkle_plugin.sh"
|
||||
- "src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Editor/BuildScript.cs"
|
||||
- "src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Eagle/SparkleInitializer.cs"
|
||||
- "src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Eagle/SparkleUpdater.cs"
|
||||
- "src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/common/UpdateNotification/UpdateNotificationManager.cs"
|
||||
- "src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/common/WindowFocusManager.cs"
|
||||
- "src/main/objc/net/eagle0/clients/unity/sparkle/**"
|
||||
- "scripts/inject_sparkle.sh"
|
||||
- "scripts/codesign_mac_app.sh"
|
||||
@@ -41,7 +50,12 @@ on:
|
||||
- "scripts/notarize_wait.sh"
|
||||
- "ci/github_actions/build_mac.sh"
|
||||
- "ci/github_actions/build_unity_mac.sh"
|
||||
- "ci/github_actions/ensure_unity_installed.sh"
|
||||
- "ci/github_actions/ensure_bazel_installed.sh"
|
||||
- ".github/actions/setup-bazel/**"
|
||||
- "ci/github_actions/detect_addressables_changes.sh"
|
||||
- "src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectVersion.txt"
|
||||
- "src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectSettings.asset"
|
||||
- ".bazelrc"
|
||||
- "ci/mac/**"
|
||||
workflow_dispatch:
|
||||
@@ -52,10 +66,6 @@ on:
|
||||
default: 'false'
|
||||
type: boolean
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: write # Required to delete artifacts after deploy
|
||||
@@ -67,11 +77,14 @@ env:
|
||||
KEYCHAIN_NAME: build-${{ github.run_id }}.keychain
|
||||
|
||||
jobs:
|
||||
build-and-sign:
|
||||
build-mac:
|
||||
runs-on: [self-hosted, macOS, unity-mac]
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-build-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
outputs:
|
||||
submission_id: ${{ steps.notarize-submit.outputs.submission_id }}
|
||||
should_deploy: ${{ steps.check-deploy.outputs.should_deploy }}
|
||||
addressables_changed: ${{ steps.addressables.outputs.changed }}
|
||||
|
||||
steps:
|
||||
- name: Prune stale PR refs
|
||||
@@ -112,11 +125,25 @@ jobs:
|
||||
- name: Clean stale files
|
||||
run: |
|
||||
git clean -ffd
|
||||
LIBRARY_DIR="src/main/csharp/net/eagle0/clients/unity/eagle0/Library"
|
||||
VERSION_CACHE="$LIBRARY_DIR/.last_unity_version"
|
||||
PROJECT_VERSION_FILE="src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectVersion.txt"
|
||||
CURRENT_VERSION=$(grep "m_EditorVersion:" "$PROJECT_VERSION_FILE" | head -1 | sed 's/m_EditorVersion: //')
|
||||
|
||||
# Nuke Library/ when Unity version changes to avoid import loops
|
||||
if [ -f "$VERSION_CACHE" ]; then
|
||||
CACHED_VERSION=$(cat "$VERSION_CACHE")
|
||||
if [ "$CACHED_VERSION" != "$CURRENT_VERSION" ]; then
|
||||
echo "Unity version changed ($CACHED_VERSION -> $CURRENT_VERSION) -- clearing Library/"
|
||||
rm -rf "$LIBRARY_DIR"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Only clear Bee/ when C# files were added/deleted/renamed (structural
|
||||
# changes that stale the DAG). Content-only modifications are handled by
|
||||
# Bee's incremental compilation. See persist_library.sh for background.
|
||||
BEE_DIR="src/main/csharp/net/eagle0/clients/unity/eagle0/Library/Bee"
|
||||
SHA_FILE="src/main/csharp/net/eagle0/clients/unity/eagle0/Library/.last_built_sha"
|
||||
BEE_DIR="$LIBRARY_DIR/Bee"
|
||||
SHA_FILE="$LIBRARY_DIR/.last_built_sha"
|
||||
if [ -f "$SHA_FILE" ] && [ -d "$BEE_DIR" ]; then
|
||||
LAST_SHA=$(cat "$SHA_FILE")
|
||||
if git diff --diff-filter=ADR --name-only "$LAST_SHA" HEAD -- '*.cs' '*.csproj' '*.asmdef' 2>/dev/null | grep -q .; then
|
||||
@@ -138,14 +165,34 @@ jobs:
|
||||
- name: Ensure Unity version installed
|
||||
run: ./ci/github_actions/ensure_unity_installed.sh mac
|
||||
|
||||
- name: Ensure .NET SDK installed
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
- name: Ensure Bazel installed
|
||||
run: ./ci/github_actions/ensure_bazel_installed.sh
|
||||
uses: ./.github/actions/setup-bazel
|
||||
|
||||
- name: Sync Bazel Xcode config
|
||||
run: ./scripts/sync_bazel_xcode.sh
|
||||
|
||||
- name: Detect Addressables changes
|
||||
id: addressables
|
||||
if: success()
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
||||
BASE_SHA="${{ github.event.pull_request.base.sha }}"
|
||||
else
|
||||
BASE_SHA="${{ github.event.before }}"
|
||||
fi
|
||||
|
||||
./ci/github_actions/detect_addressables_changes.sh "$BASE_SHA" "${{ github.sha }}"
|
||||
|
||||
- name: Build Mac Unity
|
||||
run: ./ci/github_actions/build_unity_mac.sh "${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC"
|
||||
run: ./ci/github_actions/build_unity_mac.sh "${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC" "${{ steps.addressables.outputs.changed }}" "${{ steps.addressables.outputs.changed }}"
|
||||
|
||||
- name: Save build SHA for Bee/ cache invalidation
|
||||
if: success()
|
||||
@@ -153,20 +200,11 @@ jobs:
|
||||
mkdir -p src/main/csharp/net/eagle0/clients/unity/eagle0/Library
|
||||
git rev-parse HEAD > src/main/csharp/net/eagle0/clients/unity/eagle0/Library/.last_built_sha
|
||||
|
||||
- name: Upload Addressables to CDN
|
||||
if: success() && github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
|
||||
env:
|
||||
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
run: ./ci/github_actions/upload_addressables.sh StandaloneOSX
|
||||
|
||||
- name: Inject Sparkle Framework
|
||||
- name: Save Unity version for Library/ cache invalidation
|
||||
if: success()
|
||||
env:
|
||||
SPARKLE_EDDSA_PUBLIC_KEY: ${{ secrets.SPARKLE_EDDSA_PUBLIC_KEY }}
|
||||
run: |
|
||||
chmod +x ./scripts/inject_sparkle.sh
|
||||
./scripts/inject_sparkle.sh "${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC/eagle0.app"
|
||||
mkdir -p src/main/csharp/net/eagle0/clients/unity/eagle0/Library
|
||||
grep "m_EditorVersion:" src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectVersion.txt | head -1 | sed 's/m_EditorVersion: //' > src/main/csharp/net/eagle0/clients/unity/eagle0/Library/.last_unity_version
|
||||
|
||||
- name: Check if should deploy
|
||||
id: check-deploy
|
||||
@@ -181,8 +219,145 @@ jobs:
|
||||
echo "should_deploy=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Import Code Signing Certificate
|
||||
- name: Zip unsigned app for deploy
|
||||
if: success() && steps.check-deploy.outputs.should_deploy == 'true'
|
||||
run: |
|
||||
cd ${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC
|
||||
ditto -c -k --keepParent eagle0.app eagle0.app.zip
|
||||
|
||||
- name: Upload unsigned app
|
||||
if: success() && steps.check-deploy.outputs.should_deploy == 'true'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: unsigned-mac-app-${{ github.run_id }}
|
||||
path: ${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC/eagle0.app.zip
|
||||
retention-days: 1
|
||||
|
||||
- name: Upload Mac Addressables for deploy
|
||||
if: success() && steps.check-deploy.outputs.should_deploy == 'true' && steps.addressables.outputs.changed == 'true'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: mac-addressables-${{ github.run_id }}
|
||||
path: src/main/csharp/net/eagle0/clients/unity/eagle0/ServerData/StandaloneOSX
|
||||
if-no-files-found: ignore
|
||||
retention-days: 1
|
||||
|
||||
- name: Archive Build Log
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: editor_mac.log
|
||||
path: ${{ env.EAGLE0_BUILD_DIR }}/editor_mac.log
|
||||
retention-days: 3
|
||||
- name: Archive Addressables build reports
|
||||
if: (success() || failure()) && steps.addressables.outputs.changed == 'true'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: mac-addressables-build-reports
|
||||
path: src/main/csharp/net/eagle0/clients/unity/eagle0/Library/com.unity.addressables/BuildReports
|
||||
if-no-files-found: ignore
|
||||
retention-days: 3
|
||||
- name: Cleanup build directory
|
||||
if: always()
|
||||
run: rm -rf "${{ env.EAGLE0_BUILD_DIR }}"
|
||||
|
||||
deploy-mac:
|
||||
needs: build-mac
|
||||
if: needs.build-mac.outputs.should_deploy == 'true'
|
||||
runs-on: [self-hosted, macOS, notarize]
|
||||
outputs:
|
||||
deployed_version: ${{ steps.deploy-mac.outputs.deployed_version }}
|
||||
concurrency:
|
||||
group: mac-deploy-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
steps:
|
||||
- name: Ensure Git LFS available for checkout
|
||||
run: |
|
||||
COMMON_PATHS="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
|
||||
export PATH="${COMMON_PATHS}:${PATH}"
|
||||
for path in /opt/homebrew/bin /usr/local/bin; do
|
||||
if [ -d "$path" ]; then
|
||||
echo "$path" >> "$GITHUB_PATH"
|
||||
fi
|
||||
done
|
||||
if ! command -v git-lfs >/dev/null 2>&1; then
|
||||
brew install git-lfs
|
||||
fi
|
||||
git-lfs --version
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
env:
|
||||
GIT_LFS_SKIP_SMUDGE: 1
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0 # For version numbering
|
||||
|
||||
- name: Check if run is still latest main
|
||||
id: check-latest
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" == "refs/heads/main" ]]; then
|
||||
LATEST_SHA=$(curl -fsSL \
|
||||
-H "Authorization: Bearer ${{ github.token }}" \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
"https://api.github.com/repos/${{ github.repository }}/commits/main" \
|
||||
| python3 -c 'import json, sys; print(json.load(sys.stdin)["sha"])')
|
||||
if [[ "$LATEST_SHA" != "${{ github.sha }}" ]]; then
|
||||
echo "Skipping deploy because ${{ github.sha }} is no longer latest main ($LATEST_SHA is latest)"
|
||||
echo "should_deploy=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
echo "should_deploy=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Clean download directory
|
||||
if: steps.check-latest.outputs.should_deploy == 'true'
|
||||
run: rm -rf ${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC
|
||||
|
||||
- name: Download unsigned app
|
||||
if: steps.check-latest.outputs.should_deploy == 'true'
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: unsigned-mac-app-${{ github.run_id }}
|
||||
path: ${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC
|
||||
|
||||
- name: Unzip unsigned app
|
||||
if: steps.check-latest.outputs.should_deploy == 'true'
|
||||
run: |
|
||||
cd ${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC
|
||||
ditto -x -k eagle0.app.zip .
|
||||
rm eagle0.app.zip
|
||||
ls -la ${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC/eagle0.app/
|
||||
|
||||
- name: Clean Addressables directory
|
||||
if: steps.check-latest.outputs.should_deploy == 'true' && needs.build-mac.outputs.addressables_changed == 'true'
|
||||
run: rm -rf src/main/csharp/net/eagle0/clients/unity/eagle0/ServerData/StandaloneOSX
|
||||
|
||||
- name: Download Mac Addressables
|
||||
if: steps.check-latest.outputs.should_deploy == 'true' && needs.build-mac.outputs.addressables_changed == 'true'
|
||||
uses: actions/download-artifact@v8
|
||||
continue-on-error: true
|
||||
with:
|
||||
name: mac-addressables-${{ github.run_id }}
|
||||
path: src/main/csharp/net/eagle0/clients/unity/eagle0/ServerData/StandaloneOSX
|
||||
|
||||
- name: Upload Addressables to CDN
|
||||
if: steps.check-latest.outputs.should_deploy == 'true' && needs.build-mac.outputs.addressables_changed == 'true'
|
||||
env:
|
||||
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
run: ./ci/github_actions/upload_addressables.sh StandaloneOSX
|
||||
|
||||
- name: Inject Sparkle Framework
|
||||
if: steps.check-latest.outputs.should_deploy == 'true'
|
||||
env:
|
||||
SPARKLE_EDDSA_PUBLIC_KEY: ${{ secrets.SPARKLE_EDDSA_PUBLIC_KEY }}
|
||||
run: |
|
||||
chmod +x ./scripts/inject_sparkle.sh
|
||||
./scripts/inject_sparkle.sh "${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC/eagle0.app"
|
||||
|
||||
- name: Import Code Signing Certificate
|
||||
if: steps.check-latest.outputs.should_deploy == 'true'
|
||||
env:
|
||||
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
|
||||
MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }}
|
||||
@@ -225,7 +400,7 @@ jobs:
|
||||
rm certificate.p12
|
||||
|
||||
- name: Code Sign App
|
||||
if: success() && steps.check-deploy.outputs.should_deploy == 'true'
|
||||
if: steps.check-latest.outputs.should_deploy == 'true'
|
||||
env:
|
||||
SIGNING_IDENTITY: ${{ secrets.SIGNING_IDENTITY }}
|
||||
run: |
|
||||
@@ -234,7 +409,7 @@ jobs:
|
||||
|
||||
- name: Submit for Notarization
|
||||
id: notarize-submit
|
||||
if: success() && steps.check-deploy.outputs.should_deploy == 'true'
|
||||
if: steps.check-latest.outputs.should_deploy == 'true'
|
||||
env:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APP_SPECIFIC_PASSWORD: ${{ secrets.APP_SPECIFIC_PASSWORD }}
|
||||
@@ -243,156 +418,47 @@ jobs:
|
||||
chmod +x ./scripts/notarize_submit.sh
|
||||
./scripts/notarize_submit.sh "${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC/eagle0.app" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cleanup Keychain
|
||||
if: always()
|
||||
run: |
|
||||
security delete-keychain "$KEYCHAIN_NAME" 2>/dev/null || true
|
||||
|
||||
- name: Zip signed app for artifact
|
||||
if: success() && steps.check-deploy.outputs.should_deploy == 'true'
|
||||
run: |
|
||||
cd ${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC
|
||||
ditto -c -k --keepParent eagle0.app eagle0.app.zip
|
||||
|
||||
- name: Upload signed app
|
||||
if: success() && steps.check-deploy.outputs.should_deploy == 'true'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: signed-mac-app-${{ github.run_id }}
|
||||
path: ${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC/eagle0.app.zip
|
||||
retention-days: 1
|
||||
|
||||
- name: Archive Build Log
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: editor_mac.log
|
||||
path: ${{ env.EAGLE0_BUILD_DIR }}/editor_mac.log
|
||||
retention-days: 3
|
||||
- name: Cleanup build directory
|
||||
if: always()
|
||||
run: rm -rf "${{ env.EAGLE0_BUILD_DIR }}"
|
||||
|
||||
wait-notarization:
|
||||
needs: build-and-sign
|
||||
if: needs.build-and-sign.outputs.should_deploy == 'true'
|
||||
runs-on: [self-hosted, macOS, notarize]
|
||||
|
||||
steps:
|
||||
- name: Ensure Git LFS available for checkout
|
||||
run: |
|
||||
COMMON_PATHS="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
|
||||
export PATH="${COMMON_PATHS}:${PATH}"
|
||||
for path in /opt/homebrew/bin /usr/local/bin; do
|
||||
if [ -d "$path" ]; then
|
||||
echo "$path" >> "$GITHUB_PATH"
|
||||
fi
|
||||
done
|
||||
if ! command -v git-lfs >/dev/null 2>&1; then
|
||||
brew install git-lfs
|
||||
fi
|
||||
git-lfs --version
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
env:
|
||||
GIT_LFS_SKIP_SMUDGE: 1
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: scripts
|
||||
|
||||
- name: Clean download directory
|
||||
run: rm -rf ${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC
|
||||
|
||||
- name: Download signed app
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: signed-mac-app-${{ github.run_id }}
|
||||
path: ${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC
|
||||
|
||||
- name: Unzip signed app
|
||||
run: |
|
||||
cd ${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC
|
||||
ditto -x -k eagle0.app.zip .
|
||||
rm eagle0.app.zip
|
||||
ls -la ${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC/eagle0.app/
|
||||
|
||||
- name: Wait for Notarization and Staple
|
||||
if: steps.check-latest.outputs.should_deploy == 'true'
|
||||
env:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APP_SPECIFIC_PASSWORD: ${{ secrets.APP_SPECIFIC_PASSWORD }}
|
||||
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
run: |
|
||||
chmod +x ./scripts/notarize_wait.sh
|
||||
./scripts/notarize_wait.sh "${{ needs.build-and-sign.outputs.submission_id }}" "${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC/eagle0.app"
|
||||
./scripts/notarize_wait.sh "${{ steps.notarize-submit.outputs.submission_id }}" "${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC/eagle0.app"
|
||||
|
||||
- name: Cleanup Keychain
|
||||
if: always()
|
||||
run: |
|
||||
security delete-keychain "$KEYCHAIN_NAME" 2>/dev/null || true
|
||||
|
||||
- name: Zip notarized app for artifact
|
||||
if: steps.check-latest.outputs.should_deploy == 'true'
|
||||
run: |
|
||||
cd ${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC
|
||||
rm -f eagle0.app.zip
|
||||
ditto -c -k --keepParent eagle0.app eagle0.app.zip
|
||||
|
||||
- name: Upload notarized app
|
||||
if: steps.check-latest.outputs.should_deploy == 'true'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: notarized-mac-app-${{ github.run_id }}
|
||||
path: ${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC/eagle0.app.zip
|
||||
retention-days: 1
|
||||
- name: Cleanup build directory
|
||||
if: always()
|
||||
run: rm -rf "${{ env.EAGLE0_BUILD_DIR }}"
|
||||
|
||||
deploy:
|
||||
needs: [build-and-sign, wait-notarization]
|
||||
if: needs.build-and-sign.outputs.should_deploy == 'true'
|
||||
runs-on: [self-hosted, macOS, notarize]
|
||||
outputs:
|
||||
deployed_version: ${{ steps.deploy-mac.outputs.deployed_version }}
|
||||
|
||||
steps:
|
||||
- name: Ensure Git LFS available for checkout
|
||||
run: |
|
||||
COMMON_PATHS="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
|
||||
export PATH="${COMMON_PATHS}:${PATH}"
|
||||
for path in /opt/homebrew/bin /usr/local/bin; do
|
||||
if [ -d "$path" ]; then
|
||||
echo "$path" >> "$GITHUB_PATH"
|
||||
fi
|
||||
done
|
||||
if ! command -v git-lfs >/dev/null 2>&1; then
|
||||
brew install git-lfs
|
||||
fi
|
||||
git-lfs --version
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
env:
|
||||
GIT_LFS_SKIP_SMUDGE: 1
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0 # For version numbering
|
||||
|
||||
- name: Clean download directory
|
||||
run: rm -rf ${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC
|
||||
|
||||
- name: Download notarized app
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: notarized-mac-app-${{ github.run_id }}
|
||||
path: ${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC
|
||||
|
||||
- name: Unzip notarized app
|
||||
run: |
|
||||
cd ${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC
|
||||
ditto -x -k eagle0.app.zip .
|
||||
rm eagle0.app.zip
|
||||
|
||||
- name: Ensure Bazel installed
|
||||
run: ./ci/github_actions/ensure_bazel_installed.sh
|
||||
if: steps.check-latest.outputs.should_deploy == 'true'
|
||||
uses: ./.github/actions/setup-bazel
|
||||
|
||||
- name: Sync Bazel Xcode config
|
||||
if: steps.check-latest.outputs.should_deploy == 'true'
|
||||
run: ./scripts/sync_bazel_xcode.sh
|
||||
|
||||
- name: Deploy Mac Build
|
||||
id: deploy-mac
|
||||
if: steps.check-latest.outputs.should_deploy == 'true'
|
||||
env:
|
||||
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
@@ -436,8 +502,8 @@ jobs:
|
||||
run: rm -rf "${{ env.EAGLE0_BUILD_DIR }}"
|
||||
|
||||
notify-mac:
|
||||
needs: deploy
|
||||
if: needs.deploy.outputs.deployed_version != ''
|
||||
needs: deploy-mac
|
||||
if: needs.deploy-mac.outputs.deployed_version != ''
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Purge CDN cache and notify clients
|
||||
@@ -452,13 +518,13 @@ jobs:
|
||||
-d '{"files": ["mac/*", "addressables/StandaloneOSX/*"]}' \
|
||||
--fail || echo "Warning: CDN purge failed (non-fatal)"
|
||||
|
||||
curl -X POST "https://admin.eagle0.net/notify-update?platform=mac&version=${{ needs.deploy.outputs.deployed_version }}&required=false" \
|
||||
curl -X POST "https://admin.eagle0.net/notify-update?platform=mac&version=${{ needs.deploy-mac.outputs.deployed_version }}&required=false" \
|
||||
-H "X-Notify-Secret: $NOTIFY_SECRET" \
|
||||
--fail --silent --show-error || echo "Warning: Failed to notify clients (non-fatal)"
|
||||
|
||||
# Cleanup job runs regardless of success/failure to prevent artifact accumulation
|
||||
cleanup:
|
||||
needs: [build-and-sign, wait-notarization, deploy, notify-mac]
|
||||
needs: [build-mac, deploy-mac, notify-mac]
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -468,7 +534,7 @@ jobs:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
# Delete this run's artifacts (names include run ID to avoid conflicts)
|
||||
for artifact_name in signed-mac-app-${{ github.run_id }} notarized-mac-app-${{ github.run_id }}; do
|
||||
for artifact_name in unsigned-mac-app-${{ github.run_id }} notarized-mac-app-${{ github.run_id }} mac-addressables-${{ github.run_id }}; do
|
||||
echo "Deleting artifact: $artifact_name"
|
||||
artifact_id=$(gh api "repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts" \
|
||||
-q ".artifacts[] | select(.name == \"$artifact_name\") | .id")
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
name: Cleanup Old Container Images
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run daily at 3am UTC
|
||||
- cron: '0 3 * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry_run:
|
||||
description: 'Dry run (show what would be deleted without deleting)'
|
||||
required: true
|
||||
default: 'true'
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install doctl
|
||||
uses: digitalocean/action-doctl@v2
|
||||
with:
|
||||
token: ${{ secrets.DO_REGISTRY_TOKEN }}
|
||||
|
||||
- name: Cleanup old images
|
||||
env:
|
||||
DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.dry_run == 'true' }}
|
||||
run: |
|
||||
set -e
|
||||
|
||||
RETENTION_DAYS=5
|
||||
CUTOFF_DATE=$(date -d "-${RETENTION_DAYS} days" +%s)
|
||||
REGISTRY="eagle0"
|
||||
|
||||
echo "Cleaning up images older than ${RETENTION_DAYS} days"
|
||||
echo "Cutoff date: $(date -d "@${CUTOFF_DATE}" -Iseconds)"
|
||||
echo "Dry run: ${DRY_RUN}"
|
||||
echo ""
|
||||
|
||||
# List repositories via JSON. Text output can wrap table rows and make
|
||||
# headers/tags/digests look like repository names.
|
||||
REPOS=$(doctl registry repository list-v2 "${REGISTRY}" --output json | jq -r '.[] | .name // .Name // empty')
|
||||
|
||||
for REPO in $REPOS; do
|
||||
echo "=== Processing repository: ${REPO} ==="
|
||||
|
||||
# Get all manifests with their tags and dates using JSON output for reliable parsing
|
||||
MANIFESTS_JSON=$(doctl registry repository list-manifests "${REPO}" --output json 2>/dev/null || echo "[]")
|
||||
|
||||
if [ "$MANIFESTS_JSON" = "[]" ] || [ -z "$MANIFESTS_JSON" ]; then
|
||||
echo " No manifests found"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Parse JSON and process each manifest
|
||||
echo "$MANIFESTS_JSON" | jq -r '.[] | "\(.digest) \(.updated_at) \(.tags // [] | join(","))"' | while read -r DIGEST UPDATED_AT TAGS; do
|
||||
# Skip if no digest or if it doesn't look like a valid digest (sha256:...)
|
||||
if [ -z "$DIGEST" ] || ! echo "$DIGEST" | grep -q '^sha256:'; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Parse the date (ISO 8601 format from JSON)
|
||||
MANIFEST_DATE=$(date -d "$UPDATED_AT" +%s 2>/dev/null || echo "0")
|
||||
|
||||
# Skip protected tags (latest, arm64-latest)
|
||||
if echo ",$TAGS," | grep -qE ',(latest|arm64-latest),'; then
|
||||
echo " KEEP: ${DIGEST:0:20}... (protected tag: $TAGS)"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Check if older than cutoff
|
||||
if [ "$MANIFEST_DATE" -lt "$CUTOFF_DATE" ]; then
|
||||
echo " DELETE: ${DIGEST:0:20}... (updated: $UPDATED_AT, tags: $TAGS)"
|
||||
if [ "$DRY_RUN" != "true" ]; then
|
||||
doctl registry repository delete-manifest "${REPO}" "$DIGEST" --force
|
||||
fi
|
||||
else
|
||||
echo " KEEP: ${DIGEST:0:20}... (updated: $UPDATED_AT, tags: $TAGS)"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
done
|
||||
|
||||
- name: Run garbage collection
|
||||
if: github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.event.inputs.dry_run == 'false')
|
||||
run: |
|
||||
echo "Starting garbage collection..."
|
||||
set +e
|
||||
GC_OUTPUT=$(doctl registry garbage-collection start --force 2>&1)
|
||||
GC_STATUS=$?
|
||||
set -e
|
||||
|
||||
echo "$GC_OUTPUT"
|
||||
if [ "$GC_STATUS" -eq 0 ]; then
|
||||
echo "Garbage collection started. It may take a few minutes to complete."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if echo "$GC_OUTPUT" | grep -q "automated garbage collection is enabled"; then
|
||||
echo "Automated garbage collection is enabled for this registry; skipping manual garbage collection."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
exit "$GC_STATUS"
|
||||
@@ -0,0 +1,219 @@
|
||||
name: Renovate Dependency Artifacts
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize, ready_for_review]
|
||||
paths:
|
||||
- 'MODULE.bazel'
|
||||
- 'MODULE.bazel.lock'
|
||||
- 'maven_install.json'
|
||||
- 'renovate.json'
|
||||
- 'ci/github_actions/ensure_bazel_installed.sh'
|
||||
- '.github/actions/setup-bazel/**'
|
||||
- '.github/workflows/renovate_dependency_artifacts.yml'
|
||||
|
||||
pull_request_target:
|
||||
types: [opened, reopened, synchronize, ready_for_review]
|
||||
paths:
|
||||
- 'MODULE.bazel'
|
||||
- 'MODULE.bazel.lock'
|
||||
- 'maven_install.json'
|
||||
- 'go.mod'
|
||||
- 'go.sum'
|
||||
- 'renovate.json'
|
||||
- 'ci/github_actions/ensure_bazel_installed.sh'
|
||||
- '.github/actions/setup-bazel/**'
|
||||
- '.github/workflows/renovate_dependency_artifacts.yml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.head.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
bazel: ${{ steps.changed-files.outputs.bazel }}
|
||||
go: ${{ steps.changed-files.outputs.go }}
|
||||
update_matrix: ${{ steps.changed-files.outputs.update_matrix }}
|
||||
steps:
|
||||
- name: Detect changed dependency files
|
||||
id: changed-files
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const files = await github.paginate(
|
||||
github.rest.pulls.listFiles,
|
||||
{
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: context.payload.pull_request.number,
|
||||
per_page: 100,
|
||||
},
|
||||
);
|
||||
|
||||
const changed = files.map((file) => file.filename);
|
||||
const bazelPatterns = [
|
||||
/^MODULE\.bazel$/,
|
||||
/^MODULE\.bazel\.lock$/,
|
||||
/^maven_install\.json$/,
|
||||
/^renovate\.json$/,
|
||||
/^ci\/github_actions\/ensure_bazel_installed\.sh$/,
|
||||
/^\.github\/actions\/setup-bazel\//,
|
||||
/^\.github\/workflows\/renovate_dependency_artifacts\.yml$/,
|
||||
];
|
||||
const goPatterns = [
|
||||
/^go\.mod$/,
|
||||
/^go\.sum$/,
|
||||
/^renovate\.json$/,
|
||||
/^\.github\/workflows\/renovate_dependency_artifacts\.yml$/,
|
||||
];
|
||||
|
||||
core.setOutput(
|
||||
'bazel',
|
||||
changed.some((file) => bazelPatterns.some((pattern) => pattern.test(file))),
|
||||
);
|
||||
core.setOutput(
|
||||
'go',
|
||||
changed.some((file) => goPatterns.some((pattern) => pattern.test(file))),
|
||||
);
|
||||
|
||||
const include = [];
|
||||
if (changed.some((file) => bazelPatterns.some((pattern) => pattern.test(file)))) {
|
||||
include.push({
|
||||
artifact: 'bazel',
|
||||
name: 'Bazel lockfiles',
|
||||
changed_files: 'MODULE.bazel.lock maven_install.json',
|
||||
commit_message: 'Update generated dependency lockfiles',
|
||||
});
|
||||
}
|
||||
if (changed.some((file) => goPatterns.some((pattern) => pattern.test(file)))) {
|
||||
include.push({
|
||||
artifact: 'go',
|
||||
name: 'Go artifacts',
|
||||
changed_files: 'go.sum',
|
||||
commit_message: 'Update generated Go artifacts',
|
||||
});
|
||||
}
|
||||
core.setOutput('update_matrix', JSON.stringify({include}));
|
||||
|
||||
check-lockfile:
|
||||
if: >-
|
||||
github.event_name == 'pull_request' &&
|
||||
github.event.pull_request.user.login != 'renovate[bot]'
|
||||
runs-on: [self-hosted, bazel]
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
lfs: false
|
||||
|
||||
- name: Ensure Bazel installed
|
||||
uses: ./.github/actions/setup-bazel
|
||||
|
||||
- name: Update Maven lockfile
|
||||
env:
|
||||
REPIN: "1"
|
||||
run: bazel run @maven//:pin
|
||||
|
||||
- name: Refresh Bazel lockfile
|
||||
run: bazel build --nobuild //src/main/scala/net/eagle0/eagle:eagle_server //src/main/cpp/net/eagle0/shardok:shardok-server
|
||||
|
||||
- name: Verify generated lockfiles are current
|
||||
run: |
|
||||
if git diff --quiet -- 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 " 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
|
||||
exit 1
|
||||
|
||||
update-artifacts:
|
||||
name: Update ${{ matrix.name }}
|
||||
needs: changes
|
||||
if: >-
|
||||
github.event_name == 'pull_request_target' &&
|
||||
github.event.pull_request.user.login == 'renovate[bot]' &&
|
||||
github.event.pull_request.head.repo.full_name == github.repository &&
|
||||
startsWith(github.event.pull_request.head.ref, 'renovate/') &&
|
||||
needs.changes.outputs.update_matrix != '{"include":[]}'
|
||||
runs-on: [self-hosted, bazel]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJSON(needs.changes.outputs.update_matrix) }}
|
||||
|
||||
steps:
|
||||
- name: Ensure Git LFS available for checkout
|
||||
run: |
|
||||
COMMON_PATHS=(/opt/homebrew/bin /usr/local/bin)
|
||||
for path in "${COMMON_PATHS[@]}"; do
|
||||
if [ -d "$path" ]; then
|
||||
echo "$path" >> "$GITHUB_PATH"
|
||||
export PATH="$path:$PATH"
|
||||
fi
|
||||
done
|
||||
|
||||
if command -v git-lfs >/dev/null 2>&1; then
|
||||
git-lfs version
|
||||
exit 0
|
||||
fi
|
||||
|
||||
brew install git-lfs
|
||||
git-lfs version
|
||||
|
||||
- name: Checkout Renovate branch
|
||||
uses: actions/checkout@v6
|
||||
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: Ensure Bazel installed
|
||||
if: matrix.artifact == 'bazel'
|
||||
uses: ./.github/actions/setup-bazel
|
||||
|
||||
- name: Update Maven lockfile
|
||||
if: matrix.artifact == 'bazel'
|
||||
env:
|
||||
REPIN: "1"
|
||||
run: bazel run @maven//:pin
|
||||
|
||||
- name: Refresh Bazel lockfile
|
||||
if: matrix.artifact == 'bazel'
|
||||
run: bazel build --nobuild //src/main/scala/net/eagle0/eagle:eagle_server //src/main/cpp/net/eagle0/shardok:shardok-server
|
||||
|
||||
- name: Set up Go
|
||||
if: matrix.artifact == 'go'
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: false
|
||||
|
||||
- name: Update Go module sums
|
||||
if: matrix.artifact == 'go'
|
||||
run: go mod download all
|
||||
|
||||
- name: Commit generated artifact updates
|
||||
run: |
|
||||
if git diff --quiet -- ${{ matrix.changed_files }}; then
|
||||
echo "${{ matrix.name }} are already up to date"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git add ${{ matrix.changed_files }}
|
||||
git commit -m "${{ matrix.commit_message }}"
|
||||
git push origin "HEAD:${{ github.event.pull_request.head.ref }}"
|
||||
@@ -0,0 +1,189 @@
|
||||
name: Repository Cleanup
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Registry cleanup: run daily at 03:00 UTC
|
||||
- cron: '0 3 * * *'
|
||||
# Artifact cleanup and storage check: run every 6 hours
|
||||
- cron: '0 */6 * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
target:
|
||||
description: 'Cleanup target'
|
||||
required: true
|
||||
default: 'all'
|
||||
type: choice
|
||||
options:
|
||||
- all
|
||||
- artifacts
|
||||
- registry
|
||||
registry_dry_run:
|
||||
description: 'Dry run registry cleanup'
|
||||
required: true
|
||||
default: 'true'
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: write
|
||||
|
||||
jobs:
|
||||
cleanup-and-check-artifacts:
|
||||
if: >-
|
||||
github.event_name == 'workflow_dispatch' &&
|
||||
(github.event.inputs.target == 'all' || github.event.inputs.target == 'artifacts') ||
|
||||
github.event_name == 'schedule' &&
|
||||
github.event.schedule == '0 */6 * * *'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Delete expired artifacts and artifacts older than 3 days
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
echo "Fetching all artifacts..."
|
||||
gh api "repos/${{ github.repository }}/actions/artifacts" \
|
||||
--paginate -q '.artifacts[] | "\(.id)\t\(.created_at)\t\(.expired)\t\(.name)"' > /tmp/all_artifacts.txt
|
||||
|
||||
total=$(wc -l < /tmp/all_artifacts.txt)
|
||||
echo "Found $total total artifacts"
|
||||
|
||||
cutoff=$(date -u -d '3 days ago' '+%Y-%m-%dT%H:%M:%SZ')
|
||||
echo "Deleting expired artifacts and artifacts created before $cutoff"
|
||||
|
||||
deleted=0
|
||||
while IFS=$'\t' read -r id created_at expired name; do
|
||||
if [[ "$expired" == "true" || "$created_at" < "$cutoff" ]]; then
|
||||
gh api -X DELETE "repos/${{ github.repository }}/actions/artifacts/$id" 2>/dev/null && deleted=$((deleted + 1))
|
||||
if [ $((deleted % 50)) -eq 0 ]; then
|
||||
echo "Deleted $deleted artifacts so far..."
|
||||
fi
|
||||
fi
|
||||
done < /tmp/all_artifacts.txt
|
||||
|
||||
echo "Cleanup complete. Deleted $deleted artifacts out of $total total."
|
||||
rm -f /tmp/all_artifacts.txt
|
||||
|
||||
- name: Check artifact storage size
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
# Calculate active artifact storage. The artifacts API can list expired
|
||||
# artifacts until they are explicitly deleted, so do not count them.
|
||||
total_bytes=$(gh api "repos/${{ github.repository }}/actions/artifacts" \
|
||||
--paginate -q '.artifacts[] | select(.expired == false) | .size_in_bytes' | awk '{sum+=$1} END {print sum}')
|
||||
|
||||
total_mb=$(( ${total_bytes:-0} / 1024 / 1024 ))
|
||||
echo "Total artifact storage: ${total_mb} MB"
|
||||
|
||||
# Fail if over 500MB
|
||||
if [ "$total_mb" -gt 500 ]; then
|
||||
echo "::error::Artifact storage is ${total_mb} MB, which exceeds the 500 MB threshold!"
|
||||
echo ""
|
||||
echo "Largest artifacts:"
|
||||
# Save to temp file to avoid SIGPIPE/broken pipe errors with head
|
||||
gh api "repos/${{ github.repository }}/actions/artifacts" \
|
||||
--paginate -q '.artifacts[] | select(.expired == false) | "\(.size_in_bytes)\t\(.name)\t\(.created_at)"' > /tmp/artifacts.txt
|
||||
sort -rn /tmp/artifacts.txt | head -20 | \
|
||||
awk -F'\t' '{printf "%d MB\t%s\t%s\n", $1/1024/1024, $2, $3}'
|
||||
rm -f /tmp/artifacts.txt
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Storage is within acceptable limits."
|
||||
|
||||
cleanup-registry:
|
||||
if: >-
|
||||
github.event_name == 'workflow_dispatch' &&
|
||||
(github.event.inputs.target == 'all' || github.event.inputs.target == 'registry') ||
|
||||
github.event_name == 'schedule' &&
|
||||
github.event.schedule == '0 3 * * *'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Install doctl
|
||||
uses: digitalocean/action-doctl@v2
|
||||
with:
|
||||
token: ${{ secrets.DO_REGISTRY_TOKEN }}
|
||||
|
||||
- name: Cleanup old images
|
||||
env:
|
||||
DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.registry_dry_run == 'true' }}
|
||||
run: |
|
||||
set -e
|
||||
|
||||
RETENTION_DAYS=5
|
||||
CUTOFF_DATE=$(date -d "-${RETENTION_DAYS} days" +%s)
|
||||
REGISTRY="eagle0"
|
||||
|
||||
echo "Cleaning up images older than ${RETENTION_DAYS} days"
|
||||
echo "Cutoff date: $(date -d "@${CUTOFF_DATE}" -Iseconds)"
|
||||
echo "Dry run: ${DRY_RUN}"
|
||||
echo ""
|
||||
|
||||
# List repositories via JSON. Text output can wrap table rows and make
|
||||
# headers/tags/digests look like repository names.
|
||||
REPOS=$(doctl registry repository list-v2 "${REGISTRY}" --output json | jq -r '.[] | .name // .Name // empty')
|
||||
|
||||
for REPO in $REPOS; do
|
||||
echo "=== Processing repository: ${REPO} ==="
|
||||
|
||||
# Get all manifests with their tags and dates using JSON output for reliable parsing
|
||||
MANIFESTS_JSON=$(doctl registry repository list-manifests "${REPO}" --output json 2>/dev/null || echo "[]")
|
||||
|
||||
if [ "$MANIFESTS_JSON" = "[]" ] || [ -z "$MANIFESTS_JSON" ]; then
|
||||
echo " No manifests found"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Parse JSON and process each manifest
|
||||
echo "$MANIFESTS_JSON" | jq -r '.[] | "\(.digest) \(.updated_at) \(.tags // [] | join(","))"' | while read -r DIGEST UPDATED_AT TAGS; do
|
||||
# Skip if no digest or if it doesn't look like a valid digest (sha256:...)
|
||||
if [ -z "$DIGEST" ] || ! echo "$DIGEST" | grep -q '^sha256:'; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Parse the date (ISO 8601 format from JSON)
|
||||
MANIFEST_DATE=$(date -d "$UPDATED_AT" +%s 2>/dev/null || echo "0")
|
||||
|
||||
# Skip protected tags (latest, arm64-latest)
|
||||
if echo ",$TAGS," | grep -qE ',(latest|arm64-latest),'; then
|
||||
echo " KEEP: ${DIGEST:0:20}... (protected tag: $TAGS)"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Check if older than cutoff
|
||||
if [ "$MANIFEST_DATE" -lt "$CUTOFF_DATE" ]; then
|
||||
echo " DELETE: ${DIGEST:0:20}... (updated: $UPDATED_AT, tags: $TAGS)"
|
||||
if [ "$DRY_RUN" != "true" ]; then
|
||||
doctl registry repository delete-manifest "${REPO}" "$DIGEST" --force
|
||||
fi
|
||||
else
|
||||
echo " KEEP: ${DIGEST:0:20}... (updated: $UPDATED_AT, tags: $TAGS)"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
done
|
||||
|
||||
- name: Run garbage collection
|
||||
if: github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.event.inputs.registry_dry_run == 'false')
|
||||
run: |
|
||||
echo "Starting garbage collection..."
|
||||
set +e
|
||||
GC_OUTPUT=$(doctl registry garbage-collection start --force 2>&1)
|
||||
GC_STATUS=$?
|
||||
set -e
|
||||
|
||||
echo "$GC_OUTPUT"
|
||||
if [ "$GC_STATUS" -eq 0 ]; then
|
||||
echo "Garbage collection started. It may take a few minutes to complete."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if echo "$GC_OUTPUT" | grep -q "automated garbage collection is enabled"; then
|
||||
echo "Automated garbage collection is enabled for this registry; skipping manual garbage collection."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
exit "$GC_STATUS"
|
||||
@@ -1,86 +0,0 @@
|
||||
name: S3 Archive Cleanup
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run weekly on Sunday at 05:00 UTC
|
||||
- cron: '0 5 * * 0'
|
||||
workflow_dispatch: # Allow manual trigger
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
runs-on: [self-hosted, bazel]
|
||||
|
||||
steps:
|
||||
- name: Ensure AWS CLI is available
|
||||
run: |
|
||||
if ! command -v aws &> /dev/null; then
|
||||
brew install awscli
|
||||
fi
|
||||
|
||||
- name: Delete old archived game folders
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.DO_SPACES_ACCESS_KEY }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.DO_SPACES_SECRET_KEY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
S3_ENDPOINT="https://sfo3.digitaloceanspaces.com"
|
||||
BUCKET="s3://eagle0/eagle/archived/"
|
||||
|
||||
# macOS date syntax
|
||||
CUTOFF=$(date -v-1m +%s)
|
||||
DELETED=0
|
||||
SKIPPED=0
|
||||
NO_DIR_FILE=0
|
||||
|
||||
echo "Cutoff date: $(date -r ${CUTOFF} '+%Y-%m-%dT%H:%M:%S')"
|
||||
echo ""
|
||||
|
||||
FOLDERS=$(aws s3 ls "$BUCKET" --endpoint-url "$S3_ENDPOINT" \
|
||||
| awk '/PRE/{gsub(/\/$/,"",$2); print $2}')
|
||||
|
||||
if [ -z "$FOLDERS" ]; then
|
||||
echo "No archived folders found."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
TOTAL=$(echo "$FOLDERS" | wc -l | tr -d ' ')
|
||||
CURRENT=0
|
||||
|
||||
for game_id in $FOLDERS; do
|
||||
CURRENT=$((CURRENT + 1))
|
||||
|
||||
DIR_INFO=$(aws s3 ls "${BUCKET}${game_id}/directory.e0i" \
|
||||
--endpoint-url "$S3_ENDPOINT" 2>/dev/null || true)
|
||||
|
||||
if [ -z "$DIR_INFO" ]; then
|
||||
echo "[$CURRENT/$TOTAL] DELETE $game_id (no directory.e0i)"
|
||||
aws s3 rm --recursive "${BUCKET}${game_id}/" \
|
||||
--endpoint-url "$S3_ENDPOINT" > /dev/null 2>&1
|
||||
DELETED=$((DELETED + 1))
|
||||
NO_DIR_FILE=$((NO_DIR_FILE + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
FILE_DATE=$(echo "$DIR_INFO" | awk '{print $1 " " $2}')
|
||||
FILE_EPOCH=$(date -j -f '%Y-%m-%d %H:%M:%S' "$FILE_DATE" +%s 2>/dev/null || echo "0")
|
||||
|
||||
if [ "$FILE_EPOCH" -lt "$CUTOFF" ]; then
|
||||
echo "[$CURRENT/$TOTAL] DELETE $game_id (directory.e0i from $FILE_DATE)"
|
||||
aws s3 rm --recursive "${BUCKET}${game_id}/" \
|
||||
--endpoint-url "$S3_ENDPOINT" > /dev/null 2>&1
|
||||
DELETED=$((DELETED + 1))
|
||||
else
|
||||
echo "[$CURRENT/$TOTAL] KEEP $game_id (directory.e0i from $FILE_DATE)"
|
||||
SKIPPED=$((SKIPPED + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=== Summary ==="
|
||||
echo "Total folders: $TOTAL"
|
||||
echo "Deleted: $DELETED"
|
||||
echo "Kept (recent): $SKIPPED"
|
||||
@@ -0,0 +1,153 @@
|
||||
name: Server Build
|
||||
|
||||
on:
|
||||
# Main pushes are covered by deploy workflows that build these targets.
|
||||
pull_request:
|
||||
paths:
|
||||
- 'src/main/scala/**'
|
||||
- 'src/main/cpp/**'
|
||||
- 'src/main/protobuf/net/eagle0/eagle/**'
|
||||
- 'src/main/protobuf/net/eagle0/shardok/**'
|
||||
- 'src/main/protobuf/net/eagle0/common/**'
|
||||
- 'src/main/go/net/eagle0/build/**'
|
||||
- 'go.mod'
|
||||
- 'go.sum'
|
||||
- 'WORKSPACE'
|
||||
- 'MODULE.bazel'
|
||||
- 'MODULE.bazel.lock'
|
||||
- 'BUILD.bazel'
|
||||
- '.bazelrc'
|
||||
- 'ci/github_actions/ensure_bazel_installed.sh'
|
||||
- 'scripts/build_eagle_ci.sh'
|
||||
- 'scripts/build_shardok_ci.sh'
|
||||
- 'scripts/build_shardok_linux_arm64_ci.sh'
|
||||
- '.github/actions/setup-bazel/**'
|
||||
- '.github/workflows/server_build.yml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
eagle: ${{ steps.filter.outputs.eagle }}
|
||||
shardok: ${{ steps.filter.outputs.shardok }}
|
||||
build_matrix: ${{ steps.filter.outputs.build_matrix }}
|
||||
steps:
|
||||
- name: Detect changed server areas
|
||||
id: filter
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const files = await github.paginate(github.rest.pulls.listFiles, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: context.payload.pull_request.number,
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
const changed = files.map(file => file.filename);
|
||||
const matches = patterns => changed.some(file =>
|
||||
patterns.some(pattern =>
|
||||
pattern.endsWith('/') ? file.startsWith(pattern) : file === pattern
|
||||
)
|
||||
);
|
||||
|
||||
const sharedPatterns = [
|
||||
'src/main/protobuf/net/eagle0/common/',
|
||||
'src/main/go/net/eagle0/build/',
|
||||
'go.mod',
|
||||
'go.sum',
|
||||
'WORKSPACE',
|
||||
'MODULE.bazel',
|
||||
'MODULE.bazel.lock',
|
||||
'BUILD.bazel',
|
||||
'.bazelrc',
|
||||
'ci/github_actions/ensure_bazel_installed.sh',
|
||||
'.github/actions/setup-bazel/',
|
||||
'.github/workflows/server_build.yml',
|
||||
];
|
||||
|
||||
const eaglePatterns = [
|
||||
'src/main/scala/',
|
||||
'src/main/protobuf/net/eagle0/eagle/',
|
||||
'scripts/build_eagle_ci.sh',
|
||||
];
|
||||
|
||||
const shardokPatterns = [
|
||||
'src/main/cpp/',
|
||||
'src/main/protobuf/net/eagle0/shardok/',
|
||||
'scripts/build_shardok_ci.sh',
|
||||
'scripts/build_shardok_linux_arm64_ci.sh',
|
||||
];
|
||||
|
||||
const shared = matches(sharedPatterns);
|
||||
const eagle = shared || matches(eaglePatterns);
|
||||
const shardok = shared || matches(shardokPatterns);
|
||||
|
||||
core.info(`Changed files:\n${changed.join('\n')}`);
|
||||
core.info(`Run Eagle build: ${eagle}`);
|
||||
core.info(`Run Shardok build: ${shardok}`);
|
||||
core.setOutput('eagle', String(eagle));
|
||||
core.setOutput('shardok', String(shardok));
|
||||
const include = [];
|
||||
if (eagle) {
|
||||
include.push({
|
||||
name: 'Eagle server',
|
||||
command: './scripts/build_eagle_ci.sh',
|
||||
});
|
||||
}
|
||||
if (shardok) {
|
||||
include.push({
|
||||
name: 'Shardok server',
|
||||
command: './scripts/build_shardok_ci.sh',
|
||||
});
|
||||
include.push({
|
||||
name: 'Shardok Linux ARM64 server',
|
||||
command: './scripts/build_shardok_linux_arm64_ci.sh',
|
||||
});
|
||||
}
|
||||
core.setOutput('build_matrix', JSON.stringify({include}));
|
||||
|
||||
server-build:
|
||||
name: Build ${{ matrix.name }}
|
||||
needs: changes
|
||||
if: needs.changes.outputs.build_matrix != '{"include":[]}'
|
||||
runs-on: [self-hosted, bazel]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJSON(needs.changes.outputs.build_matrix) }}
|
||||
|
||||
steps:
|
||||
- name: Ensure Git LFS available for checkout
|
||||
run: |
|
||||
COMMON_PATHS=(/opt/homebrew/bin /usr/local/bin)
|
||||
for path in "${COMMON_PATHS[@]}"; do
|
||||
if [ -d "$path" ]; then
|
||||
echo "$path" >> "$GITHUB_PATH"
|
||||
export PATH="$path:$PATH"
|
||||
fi
|
||||
done
|
||||
|
||||
if command -v git-lfs >/dev/null 2>&1; then
|
||||
git-lfs version
|
||||
exit 0
|
||||
fi
|
||||
|
||||
brew install git-lfs
|
||||
git-lfs version
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
lfs: false
|
||||
- name: Setup Bazel
|
||||
uses: ./.github/actions/setup-bazel
|
||||
- name: Build server
|
||||
run: ${{ matrix.command }}
|
||||
@@ -11,6 +11,7 @@ on:
|
||||
- 'src/main/resources/net/eagle0/shardok/**'
|
||||
- 'ci/BUILD.bazel'
|
||||
- 'ci/github_actions/ensure_bazel_installed.sh'
|
||||
- '.github/actions/setup-bazel/**'
|
||||
- 'go.mod'
|
||||
- 'go.sum'
|
||||
- 'MODULE.bazel'
|
||||
@@ -47,6 +48,12 @@ jobs:
|
||||
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
|
||||
@@ -59,7 +66,7 @@ jobs:
|
||||
lfs: false
|
||||
|
||||
- name: Ensure Bazel installed
|
||||
run: ./ci/github_actions/ensure_bazel_installed.sh
|
||||
uses: ./.github/actions/setup-bazel
|
||||
|
||||
- name: Build Shardok ARM64 binary (cross-compile for Linux ARM64)
|
||||
run: |
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
name: Shardok Build
|
||||
|
||||
on:
|
||||
# Main pushes are covered by shardok_arm64_build.yml which builds the same target
|
||||
pull_request:
|
||||
paths:
|
||||
- 'src/main/cpp/**'
|
||||
- 'src/main/protobuf/net/eagle0/shardok/**'
|
||||
- 'src/main/protobuf/net/eagle0/common/**'
|
||||
- 'src/main/go/net/eagle0/build/**'
|
||||
- 'go.mod'
|
||||
- 'go.sum'
|
||||
- 'WORKSPACE'
|
||||
- 'MODULE.bazel'
|
||||
- 'MODULE.bazel.lock'
|
||||
- 'BUILD.bazel'
|
||||
- '.bazelrc'
|
||||
- 'ci/github_actions/ensure_bazel_installed.sh'
|
||||
- 'scripts/build_shardok_ci.sh'
|
||||
- '.github/workflows/shardok_build.yml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: [self-hosted, bazel]
|
||||
|
||||
steps:
|
||||
- name: Ensure Git LFS available for checkout
|
||||
run: |
|
||||
COMMON_PATHS=(/opt/homebrew/bin /usr/local/bin)
|
||||
for path in "${COMMON_PATHS[@]}"; do
|
||||
if [ -d "$path" ]; then
|
||||
echo "$path" >> "$GITHUB_PATH"
|
||||
export PATH="$path:$PATH"
|
||||
fi
|
||||
done
|
||||
|
||||
if command -v git-lfs >/dev/null 2>&1; then
|
||||
git-lfs version
|
||||
exit 0
|
||||
fi
|
||||
|
||||
brew install git-lfs
|
||||
git-lfs version
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
lfs: false
|
||||
- name: Ensure Bazel installed
|
||||
run: ./ci/github_actions/ensure_bazel_installed.sh
|
||||
- name: Build Shardok server
|
||||
run: ./scripts/build_shardok_ci.sh
|
||||
@@ -0,0 +1,74 @@
|
||||
name: Storage Cleanup
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Blob cleanup: run daily at 04:00 UTC
|
||||
- cron: '0 4 * * *'
|
||||
# S3 archive cleanup: run weekly on Sunday at 05:00 UTC
|
||||
- cron: '0 5 * * 0'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
target:
|
||||
description: 'Cleanup target'
|
||||
required: true
|
||||
default: 'all'
|
||||
type: choice
|
||||
options:
|
||||
- all
|
||||
- blob
|
||||
- archive
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
name: ${{ matrix.name }}
|
||||
runs-on: [self-hosted, bazel]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJSON(github.event_name == 'workflow_dispatch' && github.event.inputs.target == 'all' && '{"include":[{"name":"Blob cleanup","command":"bazel run //src/main/go/net/eagle0/build/blob_cleanup:blob_cleanup -- --min-age=1h"},{"name":"S3 archive cleanup","command":"bazel run //src/main/go/net/eagle0/build/s3_archive_cleanup:s3_archive_cleanup -- --min-age=720h"}]}' || github.event_name == 'workflow_dispatch' && github.event.inputs.target == 'archive' && '{"include":[{"name":"S3 archive cleanup","command":"bazel run //src/main/go/net/eagle0/build/s3_archive_cleanup:s3_archive_cleanup -- --min-age=720h"}]}' || github.event_name == 'schedule' && github.event.schedule == '0 5 * * 0' && '{"include":[{"name":"S3 archive cleanup","command":"bazel run //src/main/go/net/eagle0/build/s3_archive_cleanup:s3_archive_cleanup -- --min-age=720h"}]}' || '{"include":[{"name":"Blob cleanup","command":"bazel run //src/main/go/net/eagle0/build/blob_cleanup:blob_cleanup -- --min-age=1h"}]}') }}
|
||||
|
||||
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@v6
|
||||
env:
|
||||
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig-no-lfs
|
||||
GIT_CONFIG_NOSYSTEM: "1"
|
||||
with:
|
||||
persist-credentials: false
|
||||
lfs: false
|
||||
clean: false
|
||||
|
||||
- name: Ensure Bazel installed
|
||||
uses: ./.github/actions/setup-bazel
|
||||
|
||||
- name: Run blob cleanup
|
||||
if: matrix.name == 'Blob cleanup'
|
||||
env:
|
||||
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
run: ${{ matrix.command }}
|
||||
|
||||
- name: Run S3 archive cleanup
|
||||
if: matrix.name == 'S3 archive cleanup'
|
||||
env:
|
||||
DO_SPACES_ACCESS_KEY: ${{ secrets.DO_SPACES_ACCESS_KEY }}
|
||||
DO_SPACES_SECRET_KEY: ${{ secrets.DO_SPACES_SECRET_KEY }}
|
||||
run: ${{ matrix.command }}
|
||||
@@ -17,6 +17,8 @@ on:
|
||||
- "ci/github_actions/build_unity.sh"
|
||||
- "ci/github_actions/test_unity_editmode.sh"
|
||||
- "ci/github_actions/ensure_bazel_installed.sh"
|
||||
- ".github/actions/setup-bazel/**"
|
||||
- "ci/github_actions/detect_addressables_changes.sh"
|
||||
- "ci/github_actions/upload_addressables.sh"
|
||||
- "ci/github_actions/ensure_unity_installed.sh"
|
||||
- "src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectVersion.txt"
|
||||
@@ -40,6 +42,8 @@ on:
|
||||
- "ci/github_actions/build_unity.sh"
|
||||
- "ci/github_actions/test_unity_editmode.sh"
|
||||
- "ci/github_actions/ensure_bazel_installed.sh"
|
||||
- ".github/actions/setup-bazel/**"
|
||||
- "ci/github_actions/detect_addressables_changes.sh"
|
||||
- "ci/github_actions/upload_addressables.sh"
|
||||
- "ci/github_actions/ensure_unity_installed.sh"
|
||||
- "src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectVersion.txt"
|
||||
@@ -48,22 +52,20 @@ on:
|
||||
- ".bazelrc"
|
||||
- "WORKSPACE"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
permissions:
|
||||
actions: write
|
||||
contents: read
|
||||
|
||||
env:
|
||||
# Runner-specific build directory to allow parallel builds on multiple runners
|
||||
EAGLE0_BUILD_DIR: /tmp/eagle0-${{ github.run_id }}
|
||||
EAGLE0_BUILD_DIR: /tmp/eagle0-${{ github.run_id }}-${{ github.job }}
|
||||
|
||||
jobs:
|
||||
windows-unity:
|
||||
unity-editmode-tests:
|
||||
runs-on: [self-hosted, macOS, unity-windows]
|
||||
outputs:
|
||||
deployed_version: ${{ steps.get-version.outputs.deployed_version }}
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-tests-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
steps:
|
||||
- name: Prune stale PR refs
|
||||
@@ -99,10 +101,17 @@ jobs:
|
||||
persist-credentials: false
|
||||
lfs: false # Fetch LFS after checkout to avoid stale ref issues
|
||||
clean: false # Library/ persists between runs on self-hosted runners
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Clean stale files
|
||||
run: |
|
||||
git clean -ffd
|
||||
UNITY_ROOT="src/main/csharp/net/eagle0/clients/unity/eagle0"
|
||||
# These ignored proto outputs are mutually exclusive across proto-generation
|
||||
# layouts. Clean both before regenerating to avoid stale DLL/source duplicates
|
||||
# when a self-hosted runner switches between main and PR branches.
|
||||
rm -rf "$UNITY_ROOT/Assets/GeneratedProtos"
|
||||
rm -rf "$UNITY_ROOT/Assets/Plugins/Eagle0Protos"
|
||||
LIBRARY_DIR="src/main/csharp/net/eagle0/clients/unity/eagle0/Library"
|
||||
VERSION_CACHE="$LIBRARY_DIR/.last_unity_version"
|
||||
PROJECT_VERSION_FILE="src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectVersion.txt"
|
||||
@@ -141,12 +150,144 @@ jobs:
|
||||
run: ./ci/github_actions/fetch_lfs.sh
|
||||
- name: Ensure Unity version installed
|
||||
run: ./ci/github_actions/ensure_unity_installed.sh windows
|
||||
- name: Ensure .NET SDK installed
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
- name: Ensure Bazel installed
|
||||
run: ./ci/github_actions/ensure_bazel_installed.sh
|
||||
uses: ./.github/actions/setup-bazel
|
||||
|
||||
- name: Run Unity EditMode tests
|
||||
run: ./ci/github_actions/test_unity_editmode.sh
|
||||
|
||||
- name: Archive EditMode test artifacts
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: unity-editmode-tests
|
||||
path: |
|
||||
${{ env.EAGLE0_BUILD_DIR }}/editor_editmode_tests.log
|
||||
${{ env.EAGLE0_BUILD_DIR }}/editmode-test-results.xml
|
||||
retention-days: 3
|
||||
- name: Cleanup build directory
|
||||
if: always()
|
||||
run: rm -rf "${{ env.EAGLE0_BUILD_DIR }}"
|
||||
|
||||
windows-unity:
|
||||
runs-on: [self-hosted, macOS, unity-windows]
|
||||
outputs:
|
||||
addressables_changed: ${{ steps.addressables.outputs.changed }}
|
||||
deployed_version: ${{ steps.get-version.outputs.deployed_version }}
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-build-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
steps:
|
||||
- name: Prune stale PR refs
|
||||
run: |
|
||||
# Self-hosted runners persist .git between runs. When a PR is updated,
|
||||
# old local refs (refs/remotes/pull/*/merge) may point to commits whose
|
||||
# objects were never fetched or have been pruned. Remove these stale refs
|
||||
# before checkout to prevent "missing object" errors.
|
||||
if [ -d ".git" ]; then
|
||||
echo "Pruning stale PR refs..."
|
||||
git for-each-ref --format='%(refname)' refs/remotes/pull/ 2>/dev/null | \
|
||||
xargs -r git update-ref -d 2>/dev/null || true
|
||||
fi
|
||||
|
||||
- name: Ensure Git LFS available for checkout
|
||||
run: |
|
||||
COMMON_PATHS="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
|
||||
export PATH="${COMMON_PATHS}:${PATH}"
|
||||
for path in /opt/homebrew/bin /usr/local/bin; do
|
||||
if [ -d "$path" ]; then
|
||||
echo "$path" >> "$GITHUB_PATH"
|
||||
fi
|
||||
done
|
||||
if ! command -v git-lfs >/dev/null 2>&1; then
|
||||
brew install git-lfs
|
||||
fi
|
||||
git-lfs --version
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
env:
|
||||
GIT_LFS_SKIP_SMUDGE: 1
|
||||
with:
|
||||
persist-credentials: false
|
||||
lfs: false # Fetch LFS after checkout to avoid stale ref issues
|
||||
clean: false # Library/ persists between runs on self-hosted runners
|
||||
fetch-depth: 2 # Keep the previous main commit available for Addressables change detection
|
||||
|
||||
- name: Clean stale files
|
||||
run: |
|
||||
git clean -ffd
|
||||
UNITY_ROOT="src/main/csharp/net/eagle0/clients/unity/eagle0"
|
||||
# These ignored proto outputs are mutually exclusive across proto-generation
|
||||
# layouts. Clean both before regenerating to avoid stale DLL/source duplicates
|
||||
# when a self-hosted runner switches between main and PR branches.
|
||||
rm -rf "$UNITY_ROOT/Assets/GeneratedProtos"
|
||||
rm -rf "$UNITY_ROOT/Assets/Plugins/Eagle0Protos"
|
||||
LIBRARY_DIR="src/main/csharp/net/eagle0/clients/unity/eagle0/Library"
|
||||
VERSION_CACHE="$LIBRARY_DIR/.last_unity_version"
|
||||
PROJECT_VERSION_FILE="src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectVersion.txt"
|
||||
CURRENT_VERSION=$(grep "m_EditorVersion:" "$PROJECT_VERSION_FILE" | head -1 | sed 's/m_EditorVersion: //')
|
||||
|
||||
# Nuke Library/ when Unity version changes to avoid import loops
|
||||
if [ -f "$VERSION_CACHE" ]; then
|
||||
CACHED_VERSION=$(cat "$VERSION_CACHE")
|
||||
if [ "$CACHED_VERSION" != "$CURRENT_VERSION" ]; then
|
||||
echo "Unity version changed ($CACHED_VERSION -> $CURRENT_VERSION) -- clearing Library/"
|
||||
rm -rf "$LIBRARY_DIR"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Only clear Bee/ when C# files were added/deleted/renamed (structural
|
||||
# changes that stale the DAG). Content-only modifications are handled by
|
||||
# Bee's incremental compilation. See persist_library.sh for background.
|
||||
BEE_DIR="$LIBRARY_DIR/Bee"
|
||||
SHA_FILE="$LIBRARY_DIR/.last_built_sha"
|
||||
if [ -f "$SHA_FILE" ] && [ -d "$BEE_DIR" ]; then
|
||||
LAST_SHA=$(cat "$SHA_FILE")
|
||||
if git diff --diff-filter=ADR --name-only "$LAST_SHA" HEAD -- '*.cs' '*.csproj' '*.asmdef' 2>/dev/null | grep -q .; then
|
||||
echo "C# files added/deleted/renamed since $LAST_SHA -- clearing Bee/"
|
||||
rm -rf "$BEE_DIR"
|
||||
else
|
||||
echo "No structural C# changes since $LAST_SHA -- keeping Bee/"
|
||||
fi
|
||||
else
|
||||
echo "No previous build SHA or no Bee/ -- clearing Bee/ as safe default"
|
||||
rm -rf "$BEE_DIR"
|
||||
fi
|
||||
|
||||
- name: Fetch LFS files
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: ./ci/github_actions/fetch_lfs.sh
|
||||
- name: Ensure Unity version installed
|
||||
run: ./ci/github_actions/ensure_unity_installed.sh windows
|
||||
- name: Ensure .NET SDK installed
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
- name: Ensure Bazel installed
|
||||
uses: ./.github/actions/setup-bazel
|
||||
|
||||
- name: Detect Addressables changes
|
||||
id: addressables
|
||||
if: success()
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
||||
BASE_SHA="${{ github.event.pull_request.base.sha }}"
|
||||
else
|
||||
BASE_SHA="${{ github.event.before }}"
|
||||
fi
|
||||
|
||||
./ci/github_actions/detect_addressables_changes.sh "$BASE_SHA" "${{ github.sha }}"
|
||||
- name: Build Windows unity
|
||||
run: ./ci/github_actions/build_unity.sh "${{ env.EAGLE0_BUILD_DIR }}/eagle0WIN"
|
||||
run: ./ci/github_actions/build_unity.sh "${{ env.EAGLE0_BUILD_DIR }}/eagle0WIN" true "${{ steps.addressables.outputs.changed }}"
|
||||
|
||||
- name: Save build SHA for Bee/ cache invalidation
|
||||
if: success()
|
||||
@@ -160,21 +301,89 @@ jobs:
|
||||
mkdir -p src/main/csharp/net/eagle0/clients/unity/eagle0/Library
|
||||
grep "m_EditorVersion:" src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectVersion.txt | head -1 | sed 's/m_EditorVersion: //' > src/main/csharp/net/eagle0/clients/unity/eagle0/Library/.last_unity_version
|
||||
|
||||
- name: Upload Addressables to CDN
|
||||
- name: Check if run is still latest main for Addressables
|
||||
id: check-addressables-latest
|
||||
if: success() && github.ref == 'refs/heads/main' && github.event_name != 'pull_request' && steps.addressables.outputs.changed == 'true'
|
||||
run: |
|
||||
LATEST_SHA=$(curl -fsSL \
|
||||
-H "Authorization: Bearer ${{ github.token }}" \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
"https://api.github.com/repos/${{ github.repository }}/commits/main" \
|
||||
| python3 -c 'import json, sys; print(json.load(sys.stdin)["sha"])')
|
||||
if [[ "$LATEST_SHA" != "${{ github.sha }}" ]]; then
|
||||
echo "Skipping Addressables upload because ${{ github.sha }} is no longer latest main ($LATEST_SHA is latest)"
|
||||
echo "should_upload=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
echo "should_upload=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Wait for EditMode tests before publishing
|
||||
if: success() && github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
deadline=$((SECONDS + 600))
|
||||
while [ "$SECONDS" -lt "$deadline" ]; do
|
||||
result=$(curl -fsSL \
|
||||
-H "Authorization: Bearer $GH_TOKEN" \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
"https://api.github.com/repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/jobs" \
|
||||
| python3 -c 'import json, sys; job = next(j for j in json.load(sys.stdin)["jobs"] if j["name"] == "unity-editmode-tests"); print("{} {}".format(job["status"], job["conclusion"] or ""))')
|
||||
status="${result%% *}"
|
||||
conclusion="${result#* }"
|
||||
if [ "$status" = "completed" ]; then
|
||||
if [ "$conclusion" = "success" ]; then
|
||||
echo "EditMode tests passed; publishing may continue."
|
||||
exit 0
|
||||
fi
|
||||
echo "EditMode tests completed with conclusion: $conclusion"
|
||||
exit 1
|
||||
fi
|
||||
echo "EditMode tests are still $status; waiting..."
|
||||
sleep 5
|
||||
done
|
||||
echo "Timed out waiting for EditMode tests."
|
||||
exit 1
|
||||
|
||||
- name: Check if run is still latest main for publish
|
||||
id: check-publish-latest
|
||||
if: success() && github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
|
||||
run: |
|
||||
LATEST_SHA=$(curl -fsSL \
|
||||
-H "Authorization: Bearer ${{ github.token }}" \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
"https://api.github.com/repos/${{ github.repository }}/commits/main" \
|
||||
| python3 -c 'import json, sys; print(json.load(sys.stdin)["sha"])')
|
||||
if [[ "$LATEST_SHA" != "${{ github.sha }}" ]]; then
|
||||
echo "Skipping publish because ${{ github.sha }} is no longer latest main ($LATEST_SHA is latest)"
|
||||
echo "should_publish=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
echo "should_publish=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Stage Windows blobs for publish
|
||||
if: success() && steps.check-publish-latest.outputs.should_publish == 'true'
|
||||
env:
|
||||
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
run: bazel run //src/main/go/net/eagle0/build/unity3d_windows_build_handler:unity3d_windows_build_handler -- --skip-previous-manifest "${{ env.EAGLE0_BUILD_DIR }}/eagle0WIN" "${{ env.EAGLE0_BUILD_DIR }}/unity_manifest.txt"
|
||||
|
||||
- name: Upload Windows Addressables to CDN
|
||||
if: success() && steps.check-publish-latest.outputs.should_publish == 'true' && steps.check-addressables-latest.outputs.should_upload == 'true'
|
||||
env:
|
||||
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
run: ./ci/github_actions/upload_addressables.sh StandaloneWindows64
|
||||
- name: Deploy Windows unity
|
||||
if: success() && github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
|
||||
|
||||
- name: Publish previous Windows manifest
|
||||
if: success() && steps.check-publish-latest.outputs.should_publish == 'true'
|
||||
env:
|
||||
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
run: bazel run //src/main/go/net/eagle0/build/unity3d_windows_build_handler:unity3d_windows_build_handler -- "${{ env.EAGLE0_BUILD_DIR }}/eagle0WIN" "/tmp/unity_manifest.txt"
|
||||
run: bazel run //src/main/go/net/eagle0/build/unity3d_windows_build_handler:unity3d_windows_build_handler -- --publish-previous-manifest "${{ env.EAGLE0_BUILD_DIR }}/unity_manifest.txt"
|
||||
|
||||
- name: Update unified manifest
|
||||
if: success() && github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
|
||||
if: success() && steps.check-publish-latest.outputs.should_publish == 'true'
|
||||
env:
|
||||
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
@@ -192,16 +401,16 @@ jobs:
|
||||
fi
|
||||
|
||||
# Update the v2 manifest at installer/v2/eagle0_manifest.txt
|
||||
bazel run //src/main/go/net/eagle0/build/manifest_manager:manifest_manager -- unity3d-v2 /tmp/unity_manifest.txt $SIGNING_ARGS
|
||||
bazel run //src/main/go/net/eagle0/build/manifest_manager:manifest_manager -- unity3d-v2 "${{ env.EAGLE0_BUILD_DIR }}/unity_manifest.txt" $SIGNING_ARGS
|
||||
|
||||
# Cleanup
|
||||
rm -f /tmp/manifest_signing_key
|
||||
|
||||
- name: Export deployed version
|
||||
id: get-version
|
||||
if: success() && github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
|
||||
if: success() && steps.check-publish-latest.outputs.should_publish == 'true'
|
||||
run: |
|
||||
VERSION=$(grep "^version=" /tmp/unity_manifest.txt | cut -d= -f2 || date +%Y.%m.%d)
|
||||
VERSION=$(grep "^version=" "${{ env.EAGLE0_BUILD_DIR }}/unity_manifest.txt" | cut -d= -f2 || date +%Y.%m.%d)
|
||||
echo "deployed_version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Archive build log
|
||||
@@ -211,14 +420,13 @@ jobs:
|
||||
name: editor_win.log
|
||||
path: ${{ env.EAGLE0_BUILD_DIR }}/editor_win.log
|
||||
retention-days: 3
|
||||
- name: Archive EditMode test artifacts
|
||||
if: success() || failure()
|
||||
- name: Archive Addressables build reports
|
||||
if: (success() || failure()) && steps.addressables.outputs.changed == 'true'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: unity_editmode_tests
|
||||
path: |
|
||||
${{ env.EAGLE0_BUILD_DIR }}/editor_editmode_tests.log
|
||||
${{ env.EAGLE0_BUILD_DIR }}/editmode-test-results.xml
|
||||
name: windows-addressables-build-reports
|
||||
path: src/main/csharp/net/eagle0/clients/unity/eagle0/Library/com.unity.addressables/BuildReports
|
||||
if-no-files-found: ignore
|
||||
retention-days: 3
|
||||
- name: Cleanup build directory
|
||||
if: always()
|
||||
|
||||
@@ -40,6 +40,7 @@ scripts/refresh_name_layers/refresh_name_layers.zip
|
||||
api_keys.txt
|
||||
|
||||
src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/GeneratedProtos/
|
||||
src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Plugins/Eagle0Protos.meta
|
||||
src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Plugins/Eagle0Protos/
|
||||
src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/Packages/com.unity.dedicated-server/
|
||||
node_modules/
|
||||
|
||||
@@ -6,6 +6,11 @@
|
||||
|
||||
**NEVER prefix a command with `cd`.** Run `git`, `gh`, `bazel`, etc. directly from whatever the cwd already is. The cwd is a subdirectory of the worktree; `git`/`gh` find the repo via `.git` discovery and `bazel` finds the workspace via MODULE.bazel discovery — walking up from a subdir works fine. The shell resets cwd after every command anyway, so a `cd` never persists. (And `cd <dir> && <cmd>` violates the no-chaining rule above and forces an approval prompt every time.) Only `cd` in the rare case a tool genuinely cannot locate its root, and even then as its own Bash call, never a chain.
|
||||
|
||||
## CRITICAL UNITY PROCESS RULES (NEVER VIOLATE)
|
||||
|
||||
**NEVER kill Unity without asking the user first.** The user might be actively using the Unity Editor. Do not kill,
|
||||
force-quit, terminate, or otherwise stop Unity processes unless the user explicitly approves that specific action.
|
||||
|
||||
## CRITICAL GIT RULES (NEVER VIOLATE)
|
||||
|
||||
**NEVER use `git -C`.** Just run `git` directly from the cwd — it finds the repo via `.git` discovery. Do not `cd` to the repo root either (see the no-`cd` bash rule above).
|
||||
@@ -60,9 +65,17 @@ with `gh pr create --body-file <path>` or `gh pr edit <number> --body-file <path
|
||||
shell quoting for PR bodies; Codex's permission matching may treat that as a complex shell command instead of a clean
|
||||
`gh pr` invocation, causing unnecessary approval prompts.
|
||||
|
||||
Write these temporary body files, and any other scratch files needed for commands, under a known writable location such
|
||||
as `/private/tmp`, `$TMPDIR`, or the current repository workspace. Do not use protected paths that require additional
|
||||
user permission just to create or edit temporary files.
|
||||
Write these temporary body files under `/private/tmp/eagle0-pr-bodies/`, creating that directory first when needed. This
|
||||
directory is the canonical writable scratch location for PR descriptions. Do not write PR body files to protected paths,
|
||||
repo-tracked paths, or ad hoc locations that might require additional user permission.
|
||||
|
||||
Use ordinary Bash file-writing commands for these temporary PR body files. First run a standalone command to create the
|
||||
directory. Then run a standalone `printf` command that writes to `/private/tmp/eagle0-pr-bodies/<name>.md`. **Do not use
|
||||
`apply_patch` for temporary PR body files**, because patch tools are for tracked workspace edits and may incorrectly
|
||||
request user approval for scratch paths outside the repository.
|
||||
|
||||
Write other scratch files needed for commands under `/private/tmp/eagle0-scratch/`, creating that directory first when
|
||||
needed. Do not use protected paths that require additional user permission just to create or edit temporary files.
|
||||
|
||||
---
|
||||
|
||||
@@ -281,7 +294,7 @@ to be used for different players or game situations within the same server proce
|
||||
**C# (Unity Client):**
|
||||
|
||||
- Located in `/src/main/csharp/net/eagle0/clients/unity/eagle0/`
|
||||
- Uses Unity 6 (6000.4.10f1) with comprehensive protobuf integration (100+ .proto files)
|
||||
- Uses Unity 6.5 (6000.5.0f1) with comprehensive protobuf integration (100+ .proto files)
|
||||
- Key components: `EagleConnection.cs` (gRPC client), `EagleGameController.cs` (main game logic)
|
||||
- Real-time bidirectional streaming with server via `PersistentClientConnection.cs`
|
||||
- Strategic map UI in `Assets/Eagle/`, tactical battle UI in `Assets/Shardok/`
|
||||
|
||||
@@ -11,6 +11,7 @@ platform(
|
||||
"@platforms//os:linux",
|
||||
"@platforms//cpu:x86_64",
|
||||
],
|
||||
missing_toolchain_error = "No Linux x86_64 toolchain matched. Docker/Eagle image builds should pass --platforms=//:linux_x86_64 and --extra_toolchains=@llvm_toolchain_linux//:cc-toolchain-x86_64-linux when compiling C++ code.",
|
||||
)
|
||||
|
||||
# Platform for cross-compiling to Linux ARM64
|
||||
@@ -20,6 +21,7 @@ platform(
|
||||
"@platforms//os:linux",
|
||||
"@platforms//cpu:aarch64",
|
||||
],
|
||||
missing_toolchain_error = "No Linux ARM64 toolchain matched. Shardok ARM64 builds should pass --platforms=//:linux_arm64 and --extra_toolchains=@llvm_toolchain_linux_arm64//:cc-toolchain-aarch64-linux.",
|
||||
)
|
||||
|
||||
gazelle(name = "gazelle")
|
||||
|
||||
+11
-9
@@ -53,7 +53,7 @@ scala_deps.scala_proto()
|
||||
# Language Support - C++
|
||||
#
|
||||
|
||||
bazel_dep(name = "toolchains_llvm", version = "1.7.0")
|
||||
bazel_dep(name = "toolchains_llvm", version = "1.8.0")
|
||||
|
||||
llvm = use_extension("@toolchains_llvm//toolchain/extensions:llvm.bzl", "llvm")
|
||||
|
||||
@@ -253,19 +253,19 @@ oci = use_extension("@rules_oci//oci:extensions.bzl", "oci")
|
||||
# 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:db3011b348ad87ced055afe5145238d87a3d8f0e8f23eb4d0cd8cecb4659b085",
|
||||
digest = "sha256:0beb258e10ad28d2bae0f74349a170bdfa67da0ef87c028cbf3d95c3e1170120",
|
||||
image = "docker.io/library/eclipse-temurin",
|
||||
platforms = ["linux/amd64"],
|
||||
tag = "25-jre",
|
||||
tag = "26-jre",
|
||||
)
|
||||
|
||||
# Base image for JFR sidecar (Java 25 JDK - includes jcmd for JFR dumps)
|
||||
oci.pull(
|
||||
name = "eclipse_temurin_25_jdk",
|
||||
digest = "sha256:98c06ec4ce7915e354d6845276c6901bd5ab075d5fcac0224fdb31b8314329b1",
|
||||
digest = "sha256:41c3a5b8a607d226c73e81538cd94d0e66f746f41f057561447eb47b40a91a65",
|
||||
image = "docker.io/library/eclipse-temurin",
|
||||
platforms = ["linux/amd64"],
|
||||
tag = "25-jdk",
|
||||
tag = "26-jdk",
|
||||
)
|
||||
|
||||
# Base image for Shardok (Ubuntu 24.04 for C++ runtime)
|
||||
@@ -276,16 +276,16 @@ oci.pull(
|
||||
"linux/amd64",
|
||||
"linux/arm64/v8",
|
||||
],
|
||||
tag = "24.04",
|
||||
tag = "26.04",
|
||||
)
|
||||
|
||||
# Base image for Admin Server (Alpine for lightweight Go binary)
|
||||
oci.pull(
|
||||
name = "alpine_linux",
|
||||
digest = "sha256:5b10f432ef3da1b8d4c7eb6c487f2f5a8f096bc91145e68878dd4a5019afde11",
|
||||
digest = "sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b",
|
||||
image = "docker.io/library/alpine",
|
||||
platforms = ["linux/amd64"],
|
||||
tag = "3.23",
|
||||
tag = "3.24",
|
||||
)
|
||||
use_repo(oci, "alpine_linux", "alpine_linux_linux_amd64", "eclipse_temurin_25_jdk", "eclipse_temurin_25_jdk_linux_amd64", "eclipse_temurin_25_jre", "eclipse_temurin_25_jre_linux_amd64", "ubuntu_24_04", "ubuntu_24_04_linux_amd64", "ubuntu_24_04_linux_arm64_v8")
|
||||
|
||||
@@ -405,7 +405,7 @@ maven.artifact(
|
||||
artifact = "guava",
|
||||
force_version = True,
|
||||
group = "com.google.guava",
|
||||
version = "33.6.0-android",
|
||||
version = "33.6.0-jre",
|
||||
)
|
||||
use_repo(maven, "maven", "unpinned_maven")
|
||||
|
||||
@@ -425,6 +425,8 @@ GTL_SHA = "1969c45dd76eac0dd87e9e2b65cffe358617f4fe1bcd203f72f427742537913a"
|
||||
http_archive(
|
||||
name = "gtl",
|
||||
build_file_content = """
|
||||
load("@rules_cc//cc:defs.bzl", "cc_library")
|
||||
|
||||
cc_library(
|
||||
name = "gtl",
|
||||
hdrs = glob(["include/gtl/*.hpp"]),
|
||||
|
||||
Generated
+382
-50
File diff suppressed because one or more lines are too long
+1
-1
@@ -212,7 +212,7 @@ oci_push(
|
||||
)
|
||||
|
||||
#
|
||||
# Shardok Server ARM64 Docker Image (for Hetzner on-demand compute)
|
||||
# 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
|
||||
# Load: bazel run //ci:shardok_server_load_arm64
|
||||
|
||||
@@ -5,8 +5,7 @@ set -x
|
||||
|
||||
COMMAND_LOG="$(bazel info command_log)"
|
||||
bazel build \
|
||||
//src/main/cpp/net/eagle0/shardok:shardok-server \
|
||||
//src/main/cpp/net/eagle0/shardok:shardok-inspector
|
||||
//src/main/cpp/net/eagle0/shardok:shardok-server
|
||||
STATUS="$?"
|
||||
|
||||
if [ $STATUS -eq 0 ]
|
||||
@@ -26,7 +25,6 @@ echo "Setting up Shardok files..."
|
||||
/bin/cp -R ./bazel-bin/src/main/cpp/net/eagle0/shardok/shardok-server ./shardok/
|
||||
/bin/mkdir -p ./shardok/shardok-server.runfiles/net_eagle0/src/main/
|
||||
/bin/cp -R ./bazel-bin/src/main/cpp/net/eagle0/shardok/shardok-server.runfiles/net_eagle0/src/main/resources ./shardok/shardok-server.runfiles/net_eagle0/src/main/
|
||||
/bin/cp -R ./bazel-bin/src/main/cpp/net/eagle0/shardok/shardok-inspector ./shardok/
|
||||
|
||||
echo "tar & zip shardok files..."
|
||||
/bin/tar cfh - shardok | /usr/bin/pigz > ./deploy/shardok.tar.gz
|
||||
|
||||
@@ -9,13 +9,23 @@ WORKSPACE=$(pwd)
|
||||
UNITY_INSTALL_PATH="/Applications/Unity/Hub/Editor"
|
||||
BUILD_DIR=$1
|
||||
LOG_PATH=$2
|
||||
BUILD_ADDRESSABLES=${3:-true}
|
||||
ADDRESSABLES_BUILD_LAYOUT=${4:-$BUILD_ADDRESSABLES}
|
||||
|
||||
echo "Building Mac in $BUILD_DIR"
|
||||
echo "Build Addressables: $BUILD_ADDRESSABLES"
|
||||
echo "Generate Addressables build layout: $ADDRESSABLES_BUILD_LAYOUT"
|
||||
|
||||
echo "Cleaning up $BUILD_DIR"
|
||||
/bin/rm -rf "$BUILD_DIR"
|
||||
/bin/mkdir -p "$BUILD_DIR"
|
||||
|
||||
if [ -n "${DOTNET_ROOT:-}" ]; then
|
||||
export PATH="$DOTNET_ROOT:$PATH"
|
||||
export DOTNET_HOST_PATH="$DOTNET_ROOT/dotnet"
|
||||
fi
|
||||
dotnet --list-sdks
|
||||
|
||||
# Use custom build script that builds Addressables before the player
|
||||
# Capture exit code to show log on failure
|
||||
set +e
|
||||
@@ -25,6 +35,8 @@ ${UNITY_INSTALL_PATH}/${UNITY_VERSION}/Unity.app/Contents/MacOS/Unity \
|
||||
-quit \
|
||||
-executeMethod BuildScript.BuildMacPlayer \
|
||||
-buildPath "$BUILD_DIR/eagle0.app" \
|
||||
-buildAddressables "$BUILD_ADDRESSABLES" \
|
||||
-addressablesBuildLayout "$ADDRESSABLES_BUILD_LAYOUT" \
|
||||
-logFile "$LOG_PATH" \
|
||||
-projectPath "$WORKSPACE/src/main/csharp/net/eagle0/clients/unity/eagle0"
|
||||
UNITY_EXIT_CODE=$?
|
||||
|
||||
@@ -15,6 +15,7 @@ git log -3
|
||||
/bin/echo "build Windows"
|
||||
LOG_PATH="${BUILD_BASE}/editor_win.log"
|
||||
BUILD_DIR=$1
|
||||
BUILD_ADDRESSABLES=${2:-true}
|
||||
ADDRESSABLES_BUILD_LAYOUT=${3:-$BUILD_ADDRESSABLES}
|
||||
|
||||
./ci/github_actions/build_windows.sh $BUILD_DIR $LOG_PATH
|
||||
|
||||
./ci/github_actions/build_windows.sh "$BUILD_DIR" "$LOG_PATH" "$BUILD_ADDRESSABLES" "$ADDRESSABLES_BUILD_LAYOUT"
|
||||
|
||||
@@ -12,6 +12,8 @@ BUILD_BASE="${EAGLE0_BUILD_DIR:-/tmp/eagle0}"
|
||||
|
||||
WORKSPACE=$(pwd)
|
||||
BUILD_PATH=${1:-"${BUILD_BASE}/eagle0iOS"}
|
||||
BUILD_ADDRESSABLES=${2:-true}
|
||||
ADDRESSABLES_BUILD_LAYOUT=${3:-$BUILD_ADDRESSABLES}
|
||||
LOG_PATH="${BUILD_BASE}/editor_ios.log"
|
||||
|
||||
# Generate unique build number from git commit count
|
||||
@@ -25,6 +27,8 @@ echo "Building protos"
|
||||
UNITY_INSTALL_PATH="/Applications/Unity/Hub/Editor"
|
||||
|
||||
echo "Building iOS Unity player to: $BUILD_PATH"
|
||||
echo "Build Addressables: $BUILD_ADDRESSABLES"
|
||||
echo "Generate Addressables build layout: $ADDRESSABLES_BUILD_LAYOUT"
|
||||
|
||||
mkdir -p "$(dirname "$LOG_PATH")"
|
||||
mkdir -p "$BUILD_PATH"
|
||||
@@ -39,6 +43,8 @@ ${UNITY_INSTALL_PATH}/${UNITY_VERSION}/Unity.app/Contents/MacOS/Unity \
|
||||
-executeMethod BuildScript.BuildiOSPlayer \
|
||||
-buildPath "$BUILD_PATH" \
|
||||
-buildNumber "$BUILD_NUMBER" \
|
||||
-buildAddressables "$BUILD_ADDRESSABLES" \
|
||||
-addressablesBuildLayout "$ADDRESSABLES_BUILD_LAYOUT" \
|
||||
-logFile "$LOG_PATH" \
|
||||
-projectPath "$WORKSPACE/src/main/csharp/net/eagle0/clients/unity/eagle0"
|
||||
UNITY_EXIT_CODE=$?
|
||||
@@ -53,6 +59,20 @@ if [ $UNITY_EXIT_CODE -ne 0 ]; then
|
||||
exit $UNITY_EXIT_CODE
|
||||
fi
|
||||
|
||||
if [ ! -f "$LOG_PATH" ]; then
|
||||
echo ""
|
||||
echo "ERROR: Unity exited successfully but did not write an editor log at $LOG_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$BUILD_PATH/Data/Raw/aa/settings.json" ]; then
|
||||
echo ""
|
||||
echo "ERROR: iOS player is missing Addressables runtime data at:"
|
||||
echo " $BUILD_PATH/Data/Raw/aa/settings.json"
|
||||
echo "Build Addressables before packaging the player so Addressables can initialize at runtime."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Fail the build if any prefab references are broken — this produces a player
|
||||
# that launches but has null Inspector fields, which is hard to debug.
|
||||
if grep -q "Missing Prefab" "$LOG_PATH"; then
|
||||
|
||||
@@ -18,5 +18,7 @@ git log -3
|
||||
/bin/echo "build Mac"
|
||||
LOG_PATH="${BUILD_BASE}/editor_mac.log"
|
||||
BUILD_DIR=$1
|
||||
BUILD_ADDRESSABLES=${2:-true}
|
||||
ADDRESSABLES_BUILD_LAYOUT=${3:-$BUILD_ADDRESSABLES}
|
||||
|
||||
./ci/github_actions/build_mac.sh "$BUILD_DIR" "$LOG_PATH"
|
||||
./ci/github_actions/build_mac.sh "$BUILD_DIR" "$LOG_PATH" "$BUILD_ADDRESSABLES" "$ADDRESSABLES_BUILD_LAYOUT"
|
||||
|
||||
@@ -9,13 +9,23 @@ WORKSPACE=`pwd`
|
||||
UNITY_INSTALL_PATH="/Applications/Unity/Hub/Editor"
|
||||
BUILD_DIR=$1
|
||||
LOG_PATH=$2
|
||||
BUILD_ADDRESSABLES=${3:-true}
|
||||
ADDRESSABLES_BUILD_LAYOUT=${4:-$BUILD_ADDRESSABLES}
|
||||
|
||||
echo "Building in $1"
|
||||
echo "Build Addressables: $BUILD_ADDRESSABLES"
|
||||
echo "Generate Addressables build layout: $ADDRESSABLES_BUILD_LAYOUT"
|
||||
|
||||
echo "Cleaning up $1"
|
||||
/bin/rm -rf $1
|
||||
/bin/mkdir -p $1
|
||||
|
||||
if [ -n "${DOTNET_ROOT:-}" ]; then
|
||||
export PATH="$DOTNET_ROOT:$PATH"
|
||||
export DOTNET_HOST_PATH="$DOTNET_ROOT/dotnet"
|
||||
fi
|
||||
dotnet --list-sdks
|
||||
|
||||
# Use custom build script that builds Addressables before the player
|
||||
# Capture exit code to show log on failure
|
||||
set +e
|
||||
@@ -25,6 +35,8 @@ ${UNITY_INSTALL_PATH}/${UNITY_VERSION}/Unity.app/Contents/MacOS/Unity \
|
||||
-quit \
|
||||
-executeMethod BuildScript.BuildWindowsPlayer \
|
||||
-buildPath "$BUILD_DIR/eagle0.exe" \
|
||||
-buildAddressables "$BUILD_ADDRESSABLES" \
|
||||
-addressablesBuildLayout "$ADDRESSABLES_BUILD_LAYOUT" \
|
||||
-logFile $LOG_PATH \
|
||||
-projectPath "$WORKSPACE/src/main/csharp/net/eagle0/clients/unity/eagle0"
|
||||
UNITY_EXIT_CODE=$?
|
||||
@@ -45,6 +57,14 @@ if [ ! -f "$LOG_PATH" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$BUILD_DIR/eagle0_Data/StreamingAssets/aa/settings.json" ]; then
|
||||
echo ""
|
||||
echo "ERROR: Windows player is missing Addressables runtime data at:"
|
||||
echo " $BUILD_DIR/eagle0_Data/StreamingAssets/aa/settings.json"
|
||||
echo "Build Addressables before packaging the player so Addressables can initialize at runtime."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Fail the build if any prefab references are broken — this produces a player
|
||||
# that launches but has null Inspector fields, which is hard to debug.
|
||||
if grep -q "Missing Prefab" "$LOG_PATH"; then
|
||||
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
BASE_SHA=${1:-}
|
||||
HEAD_SHA=${2:-HEAD}
|
||||
OUTPUT_FILE=${GITHUB_OUTPUT:-}
|
||||
|
||||
if [ -z "$BASE_SHA" ] || [[ "$BASE_SHA" =~ ^0+$ ]]; then
|
||||
echo "No usable base SHA for this run; building Addressables as a safe default."
|
||||
changed=true
|
||||
elif ! git cat-file -e "$BASE_SHA^{commit}"; then
|
||||
echo "Base SHA $BASE_SHA is not available locally; building Addressables as a safe default."
|
||||
changed=true
|
||||
else
|
||||
UNITY_PROJECT="src/main/csharp/net/eagle0/clients/unity/eagle0"
|
||||
ADDRESSABLE_PREFIXES=(
|
||||
"$UNITY_PROJECT/Assets/AddressableAssetsData/"
|
||||
"$UNITY_PROJECT/Assets/Eagle/Effects/"
|
||||
"$UNITY_PROJECT/Assets/Hex Tiles/"
|
||||
"$UNITY_PROJECT/Assets/TileableBridgePack/"
|
||||
"$UNITY_PROJECT/Assets/Shardok/LargeFlames.prefab"
|
||||
"$UNITY_PROJECT/Assets/Shardok/soundEffects/"
|
||||
"$UNITY_PROJECT/Assets/Shardok/Sounds/"
|
||||
"$UNITY_PROJECT/Assets/Medieval Combat Sounds/"
|
||||
"$UNITY_PROJECT/Assets/Magic Spells Sound Effects LITE/"
|
||||
"$UNITY_PROJECT/Assets/Fantasy Interface Sounds/"
|
||||
"$UNITY_PROJECT/Assets/AustraliaAnimalsPackv1/"
|
||||
"$UNITY_PROJECT/Assets/Music/"
|
||||
"$UNITY_PROJECT/Assets/Editor/BuildScript.cs"
|
||||
)
|
||||
|
||||
is_addressables_input() {
|
||||
local file="$1"
|
||||
for prefix in "${ADDRESSABLE_PREFIXES[@]}"; do
|
||||
if [[ "$file" == "$prefix"* ]]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
changed=false
|
||||
while IFS= read -r file; do
|
||||
if is_addressables_input "$file"; then
|
||||
echo "Addressables-impacting change: $file"
|
||||
changed=true
|
||||
break
|
||||
fi
|
||||
done < <(git diff --name-only "$BASE_SHA" "$HEAD_SHA")
|
||||
fi
|
||||
|
||||
if [ "$changed" = "true" ]; then
|
||||
echo "Building Addressables because their inputs changed."
|
||||
else
|
||||
echo "Skipping Addressables build; no Addressables inputs changed."
|
||||
fi
|
||||
|
||||
if [ -n "$OUTPUT_FILE" ]; then
|
||||
echo "changed=$changed" >> "$OUTPUT_FILE"
|
||||
else
|
||||
echo "changed=$changed"
|
||||
fi
|
||||
@@ -5,8 +5,26 @@ set -euo pipefail
|
||||
COMMON_PATHS="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
|
||||
export PATH="${COMMON_PATHS}:${PATH}"
|
||||
|
||||
write_developer_dir_override() {
|
||||
repo_contents_cache_dir() {
|
||||
if [ "$(uname -s)" = "Darwin" ]; then
|
||||
printf '%s\n' "${HOME}/Library/Caches/eagle0-bazel-repo-contents"
|
||||
else
|
||||
printf '%s\n' "${XDG_CACHE_HOME:-${HOME}/.cache}/eagle0-bazel-repo-contents"
|
||||
fi
|
||||
}
|
||||
|
||||
write_bazel_local_config() {
|
||||
local repo_contents_cache
|
||||
repo_contents_cache="$(repo_contents_cache_dir)"
|
||||
|
||||
if [ "$(uname -s)" != "Darwin" ]; then
|
||||
if [ -n "${CI:-}" ]; then
|
||||
mkdir -p "$repo_contents_cache"
|
||||
{
|
||||
echo "# Generated by ci/github_actions/ensure_bazel_installed.sh; do not edit."
|
||||
printf 'common --repo_contents_cache=%s\n' "$repo_contents_cache"
|
||||
} > .bazelrc.local
|
||||
fi
|
||||
return
|
||||
fi
|
||||
|
||||
@@ -30,8 +48,10 @@ write_developer_dir_override() {
|
||||
if [ -f .bazelrc.local ] && [ -z "${CI:-}" ]; then
|
||||
echo "Keeping existing .bazelrc.local outside CI"
|
||||
else
|
||||
mkdir -p "$repo_contents_cache"
|
||||
{
|
||||
echo "# Generated by ci/github_actions/ensure_bazel_installed.sh; do not edit."
|
||||
printf 'common --repo_contents_cache=%s\n' "$repo_contents_cache"
|
||||
printf 'common:macos --repo_env=DEVELOPER_DIR=%s\n' "$developer_dir"
|
||||
} > .bazelrc.local
|
||||
fi
|
||||
@@ -43,7 +63,7 @@ write_developer_dir_override() {
|
||||
echo "Using Apple developer directory at $developer_dir"
|
||||
}
|
||||
|
||||
write_developer_dir_override
|
||||
write_bazel_local_config
|
||||
|
||||
if [ -n "${GITHUB_PATH:-}" ]; then
|
||||
for path in /opt/homebrew/bin /usr/local/bin; do
|
||||
|
||||
@@ -49,16 +49,12 @@ check_modules_installed() {
|
||||
fi
|
||||
;;
|
||||
mac)
|
||||
# Mac IL2CPP module
|
||||
if [ ! -d "${unity_app_contents_path}/PlaybackEngines/MacStandaloneSupport/Source/Player/MacPlayer/GameAssembly.xcodeproj" ] && \
|
||||
[ ! -d "${unity_path}/PlaybackEngines/MacStandaloneSupport/Variations/macos_development_il2cpp" ] && \
|
||||
[ ! -d "${unity_path}/PlaybackEngines/MacStandaloneSupport" ]; then
|
||||
if ! has_mac_il2cpp_support "$unity_path" "$unity_app_contents_path"; then
|
||||
echo "✗ Mac IL2CPP module not installed for Unity ${UNITY_VERSION}"
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
windows)
|
||||
# Windows mono module
|
||||
if [ ! -d "${unity_path}/PlaybackEngines/WindowsStandaloneSupport" ]; then
|
||||
echo "✗ Windows module not installed for Unity ${UNITY_VERSION}"
|
||||
return 1
|
||||
@@ -71,9 +67,8 @@ check_modules_installed() {
|
||||
echo "✗ iOS module missing"
|
||||
missing=1
|
||||
fi
|
||||
if [ ! -d "${unity_app_contents_path}/PlaybackEngines/MacStandaloneSupport/Source/Player/MacPlayer/GameAssembly.xcodeproj" ] && \
|
||||
[ ! -d "${unity_path}/PlaybackEngines/MacStandaloneSupport" ]; then
|
||||
echo "✗ Mac module missing"
|
||||
if ! has_mac_il2cpp_support "$unity_path" "$unity_app_contents_path"; then
|
||||
echo "✗ Mac IL2CPP module missing"
|
||||
missing=1
|
||||
fi
|
||||
if [ ! -d "${unity_path}/PlaybackEngines/WindowsStandaloneSupport" ]; then
|
||||
@@ -89,6 +84,27 @@ check_modules_installed() {
|
||||
return 0
|
||||
}
|
||||
|
||||
has_mac_il2cpp_support() {
|
||||
local unity_path="$1"
|
||||
local unity_app_contents_path="$2"
|
||||
|
||||
has_il2cpp_variation "${unity_app_contents_path}/PlaybackEngines/MacStandaloneSupport/Variations" ||
|
||||
has_il2cpp_variation "${unity_path}/PlaybackEngines/MacStandaloneSupport/Variations"
|
||||
}
|
||||
|
||||
has_il2cpp_variation() {
|
||||
local variations_path="$1"
|
||||
local variation
|
||||
|
||||
for variation in "${variations_path}"/*il2cpp*; do
|
||||
if [ -d "$variation" ]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Check if already installed with required modules
|
||||
if check_modules_installed; then
|
||||
echo "✓ Unity ${UNITY_VERSION} is already installed with ${PLATFORM} support"
|
||||
|
||||
@@ -38,15 +38,23 @@ fi
|
||||
echo "Using git-lfs at $(command -v git-lfs)"
|
||||
git lfs version
|
||||
|
||||
git lfs install --force
|
||||
git lfs install --local --force
|
||||
|
||||
echo "LFS objects before pull:"
|
||||
git lfs ls-files | wc -l
|
||||
|
||||
GIT_LFS_PULL=(git lfs pull)
|
||||
FALLBACK_GITHUB_LFS_PULL=()
|
||||
if [ -n "${GITHUB_TOKEN:-}" ]; then
|
||||
BASIC_AUTH=$(printf 'x-access-token:%s' "$GITHUB_TOKEN" | base64 | tr -d '\n')
|
||||
GIT_LFS_PULL=(git -c "http.https://github.com/.extraheader=AUTHORIZATION: basic $BASIC_AUTH" lfs pull)
|
||||
FALLBACK_GITHUB_LFS_PULL=(
|
||||
git
|
||||
-c "http.https://github.com/.extraheader=AUTHORIZATION: basic $BASIC_AUTH"
|
||||
-c "lfs.url=https://github.com/nolen777/eagle0.git/info/lfs"
|
||||
lfs
|
||||
pull
|
||||
)
|
||||
fi
|
||||
|
||||
for i in $(seq 1 $MAX_ATTEMPTS); do
|
||||
@@ -61,5 +69,14 @@ for i in $(seq 1 $MAX_ATTEMPTS); do
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "${#FALLBACK_GITHUB_LFS_PULL[@]}" -gt 0 ]; then
|
||||
echo "LFS mirror pull failed after $MAX_ATTEMPTS attempts; trying GitHub LFS fallback"
|
||||
if "${FALLBACK_GITHUB_LFS_PULL[@]}" "$@"; then
|
||||
echo "LFS objects after GitHub fallback pull:"
|
||||
git lfs ls-files | wc -l
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "LFS pull failed after $MAX_ATTEMPTS attempts"
|
||||
exit 1
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Print a compact timing summary from a Bazel build event JSON file."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from collections import Counter, defaultdict
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
def duration_to_ms(value: Any) -> float | None:
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, (int, float)):
|
||||
return float(value)
|
||||
if isinstance(value, str):
|
||||
if value.endswith("s"):
|
||||
try:
|
||||
return float(value[:-1]) * 1000
|
||||
except ValueError:
|
||||
return None
|
||||
try:
|
||||
return float(value)
|
||||
except ValueError:
|
||||
return None
|
||||
if isinstance(value, dict):
|
||||
seconds = value.get("seconds", 0)
|
||||
nanos = value.get("nanos", 0)
|
||||
try:
|
||||
return float(seconds) * 1000 + float(nanos) / 1_000_000
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def format_ms(milliseconds: float | None) -> str | None:
|
||||
if milliseconds is None:
|
||||
return None
|
||||
if milliseconds >= 1000:
|
||||
return f"{milliseconds / 1000:.2f}s"
|
||||
return f"{milliseconds:.0f}ms"
|
||||
|
||||
|
||||
def nested_get(data: dict[str, Any], *keys: str) -> Any:
|
||||
current: Any = data
|
||||
for key in keys:
|
||||
if not isinstance(current, dict):
|
||||
return None
|
||||
current = current.get(key)
|
||||
return current
|
||||
|
||||
|
||||
def read_events(path: Path) -> list[dict[str, Any]]:
|
||||
events = []
|
||||
with path.open() as file:
|
||||
for line_number, line in enumerate(file, start=1):
|
||||
stripped = line.strip()
|
||||
if not stripped:
|
||||
continue
|
||||
try:
|
||||
event = json.loads(stripped)
|
||||
except json.JSONDecodeError as error:
|
||||
print(f"Ignoring malformed BEP line {line_number}: {error}")
|
||||
continue
|
||||
if isinstance(event, dict):
|
||||
events.append(event)
|
||||
return events
|
||||
|
||||
|
||||
def collect_metric_events(events: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
metric_events = []
|
||||
for event in events:
|
||||
if isinstance(event.get("buildMetrics"), dict):
|
||||
metric_events.append(event["buildMetrics"])
|
||||
return metric_events
|
||||
|
||||
|
||||
def first_present(data: dict[str, Any], keys: list[str]) -> Any:
|
||||
for key in keys:
|
||||
value = data.get(key)
|
||||
if value is not None:
|
||||
return value
|
||||
return None
|
||||
|
||||
|
||||
def summarize_timing(build_metrics: dict[str, Any]) -> list[str]:
|
||||
timing = build_metrics.get("timingMetrics")
|
||||
if not isinstance(timing, dict):
|
||||
return []
|
||||
|
||||
labels = [
|
||||
("Wall time", ["wallTimeInMs", "wallTime", "elapsedTime"]),
|
||||
("Loading phase", ["loadingPhaseTimeInMs", "loadingPhaseTime"]),
|
||||
("Analysis phase", ["analysisPhaseTimeInMs", "analysisPhaseTime"]),
|
||||
("Execution phase", ["executionPhaseTimeInMs", "executionPhaseTime"]),
|
||||
("Critical path", ["criticalPathTimeInMs", "criticalPathTime"]),
|
||||
]
|
||||
|
||||
lines = []
|
||||
for label, keys in labels:
|
||||
value = format_ms(duration_to_ms(first_present(timing, keys)))
|
||||
if value is not None:
|
||||
lines.append(f" {label}: {value}")
|
||||
return lines
|
||||
|
||||
|
||||
def summarize_counts(build_metrics: dict[str, Any]) -> list[str]:
|
||||
summaries: list[tuple[str, Any]] = []
|
||||
|
||||
action_summary = build_metrics.get("actionSummary")
|
||||
if isinstance(action_summary, dict):
|
||||
summaries.extend(
|
||||
[
|
||||
("Actions created", action_summary.get("actionsCreated")),
|
||||
("Actions executed", action_summary.get("actionsExecuted")),
|
||||
("Action cache hits", action_summary.get("actionCacheHits")),
|
||||
("Remote cache hits", action_summary.get("remoteCacheHits")),
|
||||
]
|
||||
)
|
||||
|
||||
target_metrics = build_metrics.get("targetMetrics")
|
||||
if isinstance(target_metrics, dict):
|
||||
summaries.extend(
|
||||
[
|
||||
("Targets configured", target_metrics.get("targetsConfigured")),
|
||||
("Targets loaded", target_metrics.get("targetsLoaded")),
|
||||
]
|
||||
)
|
||||
|
||||
package_metrics = build_metrics.get("packageMetrics")
|
||||
if isinstance(package_metrics, dict):
|
||||
summaries.extend(
|
||||
[
|
||||
("Packages loaded", package_metrics.get("packagesLoaded")),
|
||||
("Packages successfully loaded", package_metrics.get("packagesSuccessfullyLoaded")),
|
||||
]
|
||||
)
|
||||
|
||||
return [f" {label}: {value}" for label, value in summaries if value is not None]
|
||||
|
||||
|
||||
def worker_mnemonic(metric: dict[str, Any]) -> str:
|
||||
for path in [
|
||||
("mnemonic",),
|
||||
("workerKey", "mnemonic"),
|
||||
("workerKeyInfo", "mnemonic"),
|
||||
]:
|
||||
value = nested_get(metric, *path)
|
||||
if value:
|
||||
return str(value)
|
||||
return "unknown"
|
||||
|
||||
|
||||
def worker_actions(metric: dict[str, Any]) -> int | None:
|
||||
for key in ["actionsExecuted", "executedActionCount", "actionCount", "actions"]:
|
||||
value = metric.get(key)
|
||||
if isinstance(value, int):
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
return int(value)
|
||||
except ValueError:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def summarize_workers(build_metrics: dict[str, Any]) -> list[str]:
|
||||
worker_metrics = build_metrics.get("workerMetrics")
|
||||
if not isinstance(worker_metrics, list) or not worker_metrics:
|
||||
return []
|
||||
|
||||
counts = Counter()
|
||||
actions_by_mnemonic: defaultdict[str, int] = defaultdict(int)
|
||||
saw_action_counts = False
|
||||
|
||||
for metric in worker_metrics:
|
||||
if not isinstance(metric, dict):
|
||||
continue
|
||||
mnemonic = worker_mnemonic(metric)
|
||||
counts[mnemonic] += 1
|
||||
actions = worker_actions(metric)
|
||||
if actions is not None:
|
||||
saw_action_counts = True
|
||||
actions_by_mnemonic[mnemonic] += actions
|
||||
|
||||
if not counts:
|
||||
return []
|
||||
|
||||
lines = [" Worker metric entries:"]
|
||||
for mnemonic, count in counts.most_common():
|
||||
suffix = ""
|
||||
if saw_action_counts:
|
||||
suffix = f", actions={actions_by_mnemonic[mnemonic]}"
|
||||
lines.append(f" {mnemonic}: workers={count}{suffix}")
|
||||
return lines
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) != 2:
|
||||
print("Usage: summarize_bazel_bep.py <build_event_json_file>")
|
||||
return 0
|
||||
|
||||
path = Path(sys.argv[1])
|
||||
if not path.exists():
|
||||
print(f"Bazel BEP summary: {path} does not exist")
|
||||
return 0
|
||||
|
||||
events = read_events(path)
|
||||
metric_events = collect_metric_events(events)
|
||||
if not metric_events:
|
||||
print("Bazel BEP summary: no buildMetrics event found")
|
||||
return 0
|
||||
|
||||
build_metrics = metric_events[-1]
|
||||
sections = [
|
||||
summarize_timing(build_metrics),
|
||||
summarize_counts(build_metrics),
|
||||
summarize_workers(build_metrics),
|
||||
]
|
||||
lines = [line for section in sections for line in section]
|
||||
|
||||
if not lines:
|
||||
print("Bazel BEP summary: buildMetrics event had no recognized metrics")
|
||||
return 0
|
||||
|
||||
print("Bazel BEP summary:")
|
||||
for line in lines:
|
||||
print(line)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Executable
+105
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
COMMAND=${1:?Usage: unity_library_cache.sh restore|save <build-target>}
|
||||
BUILD_TARGET=${2:?Usage: unity_library_cache.sh restore|save <build-target>}
|
||||
|
||||
PROJECT_DIR="src/main/csharp/net/eagle0/clients/unity/eagle0"
|
||||
LIBRARY_DIR="$PROJECT_DIR/Library"
|
||||
MARKER_FILE="$LIBRARY_DIR/.eagle0-library-cache-key"
|
||||
COMPLETE_FILE=".complete"
|
||||
|
||||
CACHE_ROOT=${UNITY_LIBRARY_CACHE_RSYNC_URL:-rsync://lappy/unity-library-cache}
|
||||
|
||||
UNITY_VERSION=$(grep "m_EditorVersion:" "$PROJECT_DIR/ProjectSettings/ProjectVersion.txt" | head -1 | sed 's/m_EditorVersion: //')
|
||||
PROJECT_HASH=$(
|
||||
git ls-files -s -- \
|
||||
"$PROJECT_DIR/Assets" \
|
||||
"$PROJECT_DIR/Packages" \
|
||||
"$PROJECT_DIR/ProjectSettings" \
|
||||
| shasum -a 256 \
|
||||
| awk '{print $1}'
|
||||
)
|
||||
CACHE_KEY="macos-${BUILD_TARGET}-${UNITY_VERSION}-${PROJECT_HASH}"
|
||||
CACHE_URL="${CACHE_ROOT%/}/${CACHE_KEY}"
|
||||
|
||||
restore_cache() {
|
||||
echo "Unity Library cache key: $CACHE_KEY"
|
||||
|
||||
if [ -d "$LIBRARY_DIR" ]; then
|
||||
if [ ! -f "$MARKER_FILE" ]; then
|
||||
echo "Existing Unity Library has no cache marker; removing it before restore."
|
||||
rm -rf "$LIBRARY_DIR"
|
||||
elif [ "$(cat "$MARKER_FILE")" != "$CACHE_KEY" ]; then
|
||||
echo "Existing Unity Library cache key differs; removing it before restore."
|
||||
echo "Existing: $(cat "$MARKER_FILE")"
|
||||
echo "Current: $CACHE_KEY"
|
||||
rm -rf "$LIBRARY_DIR"
|
||||
else
|
||||
echo "Existing Unity Library already matches cache key."
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
local complete_marker
|
||||
complete_marker=$(mktemp)
|
||||
if ! rsync -q "${CACHE_URL}/${COMPLETE_FILE}" "$complete_marker"; then
|
||||
echo "No complete Unity Library cache found at $CACHE_URL"
|
||||
rm -f "$complete_marker"
|
||||
return 0
|
||||
fi
|
||||
rm -f "$complete_marker"
|
||||
|
||||
echo "Restoring Unity Library from $CACHE_URL"
|
||||
mkdir -p "$LIBRARY_DIR"
|
||||
if ! rsync -a --delete "${CACHE_URL}/" "$LIBRARY_DIR/"; then
|
||||
echo "Unity Library cache restore failed; removing partial restore."
|
||||
rm -rf "$LIBRARY_DIR"
|
||||
return 0
|
||||
fi
|
||||
|
||||
printf '%s\n' "$CACHE_KEY" > "$MARKER_FILE"
|
||||
echo "Unity Library cache restore complete."
|
||||
}
|
||||
|
||||
save_cache() {
|
||||
if [ ! -d "$LIBRARY_DIR" ]; then
|
||||
echo "Unity Library does not exist; nothing to cache."
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "Saving Unity Library cache to $CACHE_URL"
|
||||
printf '%s\n' "$CACHE_KEY" > "$MARKER_FILE"
|
||||
rm -f "$LIBRARY_DIR/$COMPLETE_FILE"
|
||||
|
||||
if ! rsync -a --delete "$LIBRARY_DIR/" "${CACHE_URL}/"; then
|
||||
echo "WARNING: Unity Library cache save failed."
|
||||
return 0
|
||||
fi
|
||||
|
||||
local complete_marker
|
||||
complete_marker=$(mktemp)
|
||||
printf '%s\n' "$CACHE_KEY" > "$complete_marker"
|
||||
if ! rsync -q "$complete_marker" "${CACHE_URL}/${COMPLETE_FILE}"; then
|
||||
echo "WARNING: Unity Library cache completion marker upload failed."
|
||||
rm -f "$complete_marker"
|
||||
return 0
|
||||
fi
|
||||
rm -f "$complete_marker"
|
||||
|
||||
echo "Unity Library cache save complete."
|
||||
}
|
||||
|
||||
case "$COMMAND" in
|
||||
restore)
|
||||
restore_cache
|
||||
;;
|
||||
save)
|
||||
save_cache
|
||||
;;
|
||||
*)
|
||||
echo "Usage: unity_library_cache.sh restore|save <build-target>"
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
@@ -18,10 +18,150 @@ SERVER_DATA="$UNITY_PROJECT/ServerData/$BUILD_TARGET"
|
||||
# DigitalOcean Spaces configuration (same region as other eagle0 buckets)
|
||||
DO_ENDPOINT="https://sfo3.digitaloceanspaces.com"
|
||||
DO_BUCKET="eagle0-assets"
|
||||
DEFAULT_MAX_UPLOAD_BYTES=$((1024 * 1024 * 1024))
|
||||
DEFAULT_MAX_SINGLE_FILE_BYTES=$((512 * 1024 * 1024))
|
||||
MAX_UPLOAD_BYTES=${ADDRESSABLES_MAX_UPLOAD_BYTES:-$DEFAULT_MAX_UPLOAD_BYTES}
|
||||
MAX_SINGLE_FILE_BYTES=${ADDRESSABLES_MAX_SINGLE_FILE_BYTES:-$DEFAULT_MAX_SINGLE_FILE_BYTES}
|
||||
TOP_UPLOAD_COUNT=${ADDRESSABLES_TOP_UPLOAD_COUNT:-20}
|
||||
|
||||
COMMON_PATHS="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
|
||||
export PATH="${COMMON_PATHS}:${PATH}"
|
||||
|
||||
format_bytes() {
|
||||
local bytes=$1
|
||||
local unit="B"
|
||||
local value=$bytes
|
||||
|
||||
if [ "$bytes" -ge $((1024 * 1024 * 1024)) ]; then
|
||||
unit="GiB"
|
||||
value=$(awk "BEGIN { printf \"%.2f\", $bytes / 1024 / 1024 / 1024 }")
|
||||
elif [ "$bytes" -ge $((1024 * 1024)) ]; then
|
||||
unit="MiB"
|
||||
value=$(awk "BEGIN { printf \"%.2f\", $bytes / 1024 / 1024 }")
|
||||
elif [ "$bytes" -ge 1024 ]; then
|
||||
unit="KiB"
|
||||
value=$(awk "BEGIN { printf \"%.2f\", $bytes / 1024 }")
|
||||
fi
|
||||
|
||||
echo "$value $unit"
|
||||
}
|
||||
|
||||
file_size() {
|
||||
local path=$1
|
||||
if stat -f%z "$path" >/dev/null 2>&1; then
|
||||
stat -f%z "$path"
|
||||
else
|
||||
stat -c%s "$path"
|
||||
fi
|
||||
}
|
||||
|
||||
append_summary() {
|
||||
if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then
|
||||
echo "$1" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
}
|
||||
|
||||
budget_enabled() {
|
||||
local value=$1
|
||||
[ "$value" -gt 0 ]
|
||||
}
|
||||
|
||||
summarize_pending_uploads() {
|
||||
local sync_args_file
|
||||
sync_args_file=$(mktemp)
|
||||
local uploads_file
|
||||
uploads_file=$(mktemp)
|
||||
local sorted_uploads_file
|
||||
sorted_uploads_file=$(mktemp)
|
||||
trap 'rm -f "$sync_args_file" "$uploads_file" "$sorted_uploads_file"' RETURN
|
||||
|
||||
aws s3 sync "$SERVER_DATA" "s3://$DO_BUCKET/addressables/$BUILD_TARGET/" \
|
||||
--endpoint-url "$DO_ENDPOINT" \
|
||||
--acl public-read \
|
||||
--size-only \
|
||||
--delete \
|
||||
--dryrun > "$sync_args_file"
|
||||
|
||||
while IFS= read -r line; do
|
||||
case "$line" in
|
||||
*upload:*)
|
||||
local source_path=${line#*upload: }
|
||||
source_path=${source_path% to s3://*}
|
||||
if [ -f "$source_path" ]; then
|
||||
local size
|
||||
size=$(file_size "$source_path")
|
||||
printf "%s\t%s\n" "$size" "$source_path" >> "$uploads_file"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done < "$sync_args_file"
|
||||
|
||||
local upload_count=0
|
||||
local upload_bytes=0
|
||||
if [ -s "$uploads_file" ]; then
|
||||
while IFS=$'\t' read -r size _path; do
|
||||
upload_count=$((upload_count + 1))
|
||||
upload_bytes=$((upload_bytes + size))
|
||||
done < "$uploads_file"
|
||||
fi
|
||||
|
||||
echo "Addressables upload preflight: $upload_count changed files, $(format_bytes "$upload_bytes") to upload."
|
||||
append_summary "## Addressables upload preflight"
|
||||
append_summary ""
|
||||
append_summary "- Target: \`$BUILD_TARGET\`"
|
||||
append_summary "- Changed files: \`$upload_count\`"
|
||||
append_summary "- Changed upload bytes: \`$(format_bytes "$upload_bytes")\`"
|
||||
append_summary "- Upload byte budget: \`$(format_bytes "$MAX_UPLOAD_BYTES")\`"
|
||||
append_summary "- Single-file budget: \`$(format_bytes "$MAX_SINGLE_FILE_BYTES")\`"
|
||||
|
||||
local guardrail_failed=0
|
||||
|
||||
if [ -s "$uploads_file" ]; then
|
||||
echo "Largest pending Addressables uploads:"
|
||||
append_summary ""
|
||||
append_summary "### Largest pending uploads"
|
||||
append_summary ""
|
||||
append_summary "| Size | Path |"
|
||||
append_summary "| ---: | --- |"
|
||||
sort -nr "$uploads_file" > "$sorted_uploads_file"
|
||||
local listed_uploads=0
|
||||
while IFS=$'\t' read -r size path; do
|
||||
if [ "$listed_uploads" -ge "$TOP_UPLOAD_COUNT" ]; then
|
||||
break
|
||||
fi
|
||||
local relative_path=${path#"$SERVER_DATA"/}
|
||||
echo " $(format_bytes "$size") $relative_path"
|
||||
append_summary "| $(format_bytes "$size") | \`$relative_path\` |"
|
||||
listed_uploads=$((listed_uploads + 1))
|
||||
done < "$sorted_uploads_file"
|
||||
|
||||
if budget_enabled "$MAX_SINGLE_FILE_BYTES"; then
|
||||
while IFS=$'\t' read -r size path; do
|
||||
if [ "$size" -gt "$MAX_SINGLE_FILE_BYTES" ]; then
|
||||
local relative_path=${path#"$SERVER_DATA"/}
|
||||
echo "ERROR: Pending Addressables upload '$relative_path' is $(format_bytes "$size"), exceeding single-file budget $(format_bytes "$MAX_SINGLE_FILE_BYTES")."
|
||||
append_summary ""
|
||||
append_summary ":x: \`$relative_path\` exceeds the single-file budget at \`$(format_bytes "$size")\`."
|
||||
guardrail_failed=1
|
||||
fi
|
||||
done < "$uploads_file"
|
||||
fi
|
||||
fi
|
||||
|
||||
if budget_enabled "$MAX_UPLOAD_BYTES" && [ "$upload_bytes" -gt "$MAX_UPLOAD_BYTES" ]; then
|
||||
echo "ERROR: Addressables upload is expected to send $(format_bytes "$upload_bytes"), exceeding budget $(format_bytes "$MAX_UPLOAD_BYTES")."
|
||||
echo "A small asset change may have invalidated an oversized bundle; inspect the largest pending uploads above."
|
||||
append_summary ""
|
||||
append_summary ":x: Changed upload bytes exceed budget: \`$(format_bytes "$upload_bytes")\` > \`$(format_bytes "$MAX_UPLOAD_BYTES")\`."
|
||||
guardrail_failed=1
|
||||
fi
|
||||
|
||||
if [ "$guardrail_failed" -ne 0 ]; then
|
||||
echo "Refusing to upload Addressables. Raise ADDRESSABLES_MAX_UPLOAD_BYTES or ADDRESSABLES_MAX_SINGLE_FILE_BYTES only after confirming the upload is intentional."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
if ! command -v aws >/dev/null 2>&1; then
|
||||
if command -v brew >/dev/null 2>&1; then
|
||||
brew install awscli
|
||||
@@ -44,13 +184,28 @@ echo "Target: s3://$DO_BUCKET/addressables/$BUILD_TARGET/"
|
||||
export AWS_ACCESS_KEY_ID="$ACCESS_KEY_ID"
|
||||
export AWS_SECRET_ACCESS_KEY="$SECRET_KEY"
|
||||
|
||||
# Sync bundles to Spaces
|
||||
# --delete removes files in destination that don't exist in source
|
||||
# --acl public-read makes files publicly accessible
|
||||
summarize_pending_uploads
|
||||
|
||||
# Sync bundles to Spaces. Addressable bundle filenames include content hashes, so a
|
||||
# same-sized existing bundle is already the same content. Use --size-only to avoid
|
||||
# re-uploading every rebuilt bundle just because Unity gave it a fresh local mtime.
|
||||
# Metadata files keep stable names, so sync them again without --size-only below.
|
||||
#
|
||||
# --delete removes files in destination that don't exist in source.
|
||||
# --acl public-read makes files publicly accessible.
|
||||
aws s3 sync "$SERVER_DATA" "s3://$DO_BUCKET/addressables/$BUILD_TARGET/" \
|
||||
--endpoint-url "$DO_ENDPOINT" \
|
||||
--acl public-read \
|
||||
--size-only \
|
||||
--delete
|
||||
|
||||
aws s3 sync "$SERVER_DATA" "s3://$DO_BUCKET/addressables/$BUILD_TARGET/" \
|
||||
--endpoint-url "$DO_ENDPOINT" \
|
||||
--acl public-read \
|
||||
--exclude "*" \
|
||||
--include "*.bin" \
|
||||
--include "*.hash" \
|
||||
--include "*.json"
|
||||
|
||||
echo "Addressables upload complete"
|
||||
echo "Files available at: https://assets.eagle0.net/addressables/$BUILD_TARGET/"
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
# Hetzner Setup Guide
|
||||
|
||||
This guide walks through setting up Hetzner Cloud infrastructure for running Shardok on-demand compute.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- All code PRs merged (#4990, #4996, #4998, #5001, #5009)
|
||||
- Access to DigitalOcean Container Registry (for pulling Shardok ARM64 image)
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Create Hetzner Cloud Account
|
||||
|
||||
1. Go to https://console.hetzner.cloud/
|
||||
2. Sign up and add payment method
|
||||
3. Create a new project (e.g., "eagle0")
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Generate Hetzner API Token
|
||||
|
||||
1. In Hetzner Console → Security → API Tokens
|
||||
2. Click "Generate API Token"
|
||||
3. Give it **Read & Write** permissions
|
||||
4. Copy the token (you'll only see it once)
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Generate Shardok Auth Token
|
||||
|
||||
Generate a 256-bit random token for Eagle-Shardok authentication:
|
||||
|
||||
```bash
|
||||
openssl rand -hex 32
|
||||
```
|
||||
|
||||
Save this output - it's the shared secret between Eagle and Shardok.
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Store Secrets in GitHub Actions
|
||||
|
||||
Add these secrets in GitHub → Settings → Secrets and variables → Actions:
|
||||
|
||||
| Secret Name | Description |
|
||||
|-------------|-------------|
|
||||
| `HETZNER_API_TOKEN` | From Step 2 - for Hetzner API calls |
|
||||
| `SHARDOK_AUTH_TOKEN` | From Step 3 - shared secret for gRPC auth |
|
||||
|
||||
Note: `DO_REGISTRY_TOKEN` already exists and will be used for Hetzner to pull container images.
|
||||
|
||||
These secrets will be passed to Eagle at runtime via `docker_build.yml`, similar to how `OPENAI_API_KEY` and other secrets are handled.
|
||||
|
||||
---
|
||||
|
||||
## Step 5: DNS Setup (for Let's Encrypt)
|
||||
|
||||
You need a domain pointing to the Shardok instance for TLS certificates.
|
||||
|
||||
### Option A: Floating IP (Recommended)
|
||||
|
||||
1. In Hetzner Console → Networking → Floating IPs
|
||||
2. Create a **Floating IPv6** in **Hillsboro, Oregon (hil)** region
|
||||
- IPv6 costs €1/month vs €3/month for IPv4
|
||||
- Hillsboro has better latency to DigitalOcean SFO than Ashburn
|
||||
- Server-to-server communication works fine with IPv6-only
|
||||
3. Point `shardok.prod.eagle0.net` to this IP via AAAA record
|
||||
4. The ShardokInstanceManager will attach this IP to instances on spin-up
|
||||
|
||||
**Location choice**: Hillsboro, OR (`hil`) is recommended for US West Coast. Same pricing as Ashburn (`ash`).
|
||||
|
||||
### Option B: Dynamic DNS
|
||||
|
||||
Update DNS programmatically when instance spins up. More complex but avoids floating IP cost.
|
||||
|
||||
---
|
||||
|
||||
## Step 6: Upload SSH Key to Hetzner
|
||||
|
||||
For debugging access to instances:
|
||||
|
||||
1. In Hetzner Console → Security → SSH Keys
|
||||
2. Click "Add SSH Key"
|
||||
3. Paste your public key (e.g., `~/.ssh/id_rsa.pub`)
|
||||
4. Give it a name (e.g., "eagle-deploy")
|
||||
|
||||
---
|
||||
|
||||
## Step 7: Wire Security Config into Eagle
|
||||
|
||||
Update Eagle's startup code to use the security config when connecting to remote Shardok:
|
||||
|
||||
```scala
|
||||
val securityConfig = ShardokSecurityConfig(
|
||||
useTls = true,
|
||||
authToken = Some(sys.env("SHARDOK_AUTH_TOKEN"))
|
||||
)
|
||||
|
||||
val channel = ServerSetupHelpers.newChannel(
|
||||
"shardok.prod.eagle0.net",
|
||||
50051,
|
||||
securityConfig
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
### Manual Instance Spin-up
|
||||
|
||||
Test the Hetzner integration by triggering instance creation:
|
||||
|
||||
```scala
|
||||
val manager = new ShardokInstanceManager(
|
||||
hetznerApiToken = sys.env("HETZNER_API_TOKEN"),
|
||||
// ... other config
|
||||
)
|
||||
|
||||
manager.ensureInstanceRunning()
|
||||
```
|
||||
|
||||
### Verify TLS and Auth
|
||||
|
||||
1. Instance spins up and gets Let's Encrypt certificate
|
||||
2. Eagle connects via TLS
|
||||
3. Auth token is validated on each request
|
||||
|
||||
---
|
||||
|
||||
## Cost Estimate
|
||||
|
||||
| Component | Cost |
|
||||
|-----------|------|
|
||||
| CAX41 (16 ARM cores) | ~$0.04/hour |
|
||||
| Floating IP | ~$4/month |
|
||||
| Typical usage (20 hrs/week) | ~$3.50/month compute |
|
||||
|
||||
**Total: ~$7-8/month** for typical usage.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Instance won't start
|
||||
- Check Hetzner API token has Read & Write permissions
|
||||
- Verify you're using the correct region (`hil` for Hillsboro OR, or `ash` for Ashburn VA)
|
||||
|
||||
### TLS certificate fails
|
||||
- Ensure DNS points to the instance IP before certbot runs
|
||||
- Check port 80 is open for Let's Encrypt HTTP-01 challenge
|
||||
|
||||
### Auth failures
|
||||
- Verify `SHARDOK_AUTH_TOKEN` matches on both Eagle and Shardok
|
||||
- Check the token file is readable by Shardok container
|
||||
|
||||
### Can't pull container image
|
||||
- Ensure `DO_REGISTRY_TOKEN` is passed to cloud-init
|
||||
- Verify the ARM64 image exists: `registry.digitalocean.com/eagle0/shardok-server:arm64-latest`
|
||||
@@ -1,23 +1,121 @@
|
||||
# New Profession Proposals
|
||||
|
||||
This document proposes 5 new hero professions for Eagle0. Each profession has abilities for both the Eagle (strategic) and Shardok (tactical) game layers.
|
||||
This document proposes new hero professions for Eagle0. Each profession should matter in both the Eagle strategic layer
|
||||
and the Shardok tactical layer, and should be earned through a prime stat in the same way existing professions are.
|
||||
|
||||
## Current Professions Reference
|
||||
|
||||
| Profession | Eagle Ability | Shardok Abilities |
|
||||
|------------|---------------|-------------------|
|
||||
| **Mage** | Control Weather | Lightning Bolt, Meteor, Freeze Water, Start Fire (enhanced) |
|
||||
| **Necromancer** | Start Epidemic | Raise Dead, Fear |
|
||||
| **Engineer** | (general) | Repair, Fortify, Build Bridge, Reduce (siege) |
|
||||
| **Paladin** | Alms (prioritized) | Holy Wave |
|
||||
| **Ranger** | Recon | Scout, Hide (enhanced), Brave Water (enhanced) |
|
||||
| **Champion** | (general) | Challenge Duel |
|
||||
Professions are currently gained when a no-profession hero crosses the profession stat threshold and wins a profession
|
||||
roll. The current stat mapping is:
|
||||
|
||||
| Prime Stat | Professions |
|
||||
|------------|-------------|
|
||||
| **Strength** | Champion |
|
||||
| **Agility** | Engineer, Ranger |
|
||||
| **Wisdom** | Mage |
|
||||
| **Charisma** | Necromancer, Paladin |
|
||||
| **Constitution** | None |
|
||||
|
||||
The current profession capabilities are:
|
||||
|
||||
| Profession | Eagle Ability | Shardok Abilities | Current Niche |
|
||||
|------------|---------------|-------------------|---------------|
|
||||
| **Mage** | Control Weather: start/end blizzards and droughts in the current or neighboring province | Lightning Bolt, Meteor, Freeze Water, Start Fire access/enhancement | Strategic weather control and high-impact elemental battlefield effects |
|
||||
| **Necromancer** | Start Epidemic in the current or neighboring province | Raise Dead, Fear | Attrition pressure, undead creation, morale attack |
|
||||
| **Engineer** | Improve command recommendation and bonus improvement output | Repair, Fortify, Build Bridge, Reduce siege | Infrastructure, battlefield construction, mechanical support |
|
||||
| **Paladin** | Alms priority and increased support from food | Holy Wave | Public support, holy area effect, anti-undead flavor |
|
||||
| **Ranger** | Recon against enemy provinces | Scout, Hide access/enhancement, Brave Water enhancement | Information, stealth, terrain crossing |
|
||||
| **Champion** | Train command recommendation and training bonus | Challenge Duel | Martial excellence, battalion training, direct hero confrontation |
|
||||
|
||||
## Recommendation
|
||||
|
||||
Add **Warden** first.
|
||||
|
||||
Warden is the cleanest next profession because Constitution is the only hero stat that does not currently unlock any
|
||||
profession. A Constitution profession also creates a distinctive tactical identity: not another caster, scout, or damage
|
||||
dealer, but a durable hero who keeps important units alive. That fills a real mechanical gap without forcing a large
|
||||
rewrite of the profession system.
|
||||
|
||||
The first version should be intentionally simple:
|
||||
|
||||
- **Prime stat:** Constitution
|
||||
- **Eagle ability:** Garrison, a defensive province action or passive defense bonus when a Warden is present with enough
|
||||
vigor; or Custody, a prisoner-control ability for captured heroes
|
||||
- **Shardok ability:** Guard, a command that marks an adjacent friendly unit until the Warden's next turn; the first
|
||||
attack against that unit is redirected to the Warden's unit or reduced by a fixed amount
|
||||
- **Battalion suitability:** Optimal with durable melee infantry; suboptimal with fragile archers or stealth-focused
|
||||
units if we want a stricter identity
|
||||
- **Balance direction:** Spend action points and/or vigor to protect another unit, rather than adding free passive
|
||||
prevention every round
|
||||
|
||||
This keeps the profession legible: high-Constitution heroes become the people who can hold formation, anchor a province,
|
||||
and protect fragile specialists.
|
||||
|
||||
---
|
||||
|
||||
## Proposed New Professions
|
||||
|
||||
### 1. HERALD (Morale & Communication Specialist)
|
||||
### 1. WARDEN (Defense & Protection Specialist)
|
||||
|
||||
**Fantasy:** The stalwart defender who holds the line and protects allies.
|
||||
|
||||
**Why it fits now:**
|
||||
- Gives Constitution a profession path
|
||||
- Adds a defensive/tank identity that no current profession owns
|
||||
- Creates counterplay to burst damage, duel pressure, and fragile high-value units
|
||||
|
||||
**Eagle Ability: "Garrison"**
|
||||
- A province with a Warden-led unit gets a defense bonus when attacked
|
||||
- Alternative active version: spend vigor to fortify the province until next round
|
||||
- Synergy: works well in border provinces, choke points, and provinces recovering after losses
|
||||
|
||||
**Alternative Eagle Ability: "Custody"**
|
||||
- A Warden can handle captured heroes with extra authority: execute them immediately or move them to a neighboring ruled
|
||||
province before normal prisoner management
|
||||
- Passive custody version: each new round, prisoners in a Warden-guarded province become more likely to join the ruling
|
||||
faction
|
||||
- Prefer implementing this as a small positive addition to the prisoner's existing `factionBiases` entry for the ruling
|
||||
faction, rather than changing the prisoner `roundsInType` multiplier from `+5/round` to `+10/round`
|
||||
- Rationale: changing the multiplier would be retroactive. A prisoner held for ten rounds would immediately receive a
|
||||
large odds jump the moment a Warden arrived. Accumulating a Warden-specific bias only rewards rounds actually spent
|
||||
under Warden custody, and the existing faction-bias decay makes the effect fade naturally if the Warden leaves
|
||||
- Stronger version: if the Warden's side captures enemy heroes during battle but still loses the province, the Warden can
|
||||
evacuate one captured hero to a neighboring ruled province instead of losing custody
|
||||
- This should be limited to one captured hero per battle or require a high-vigor Warden, because keeping prisoners after
|
||||
losing the battle is a major strategic swing
|
||||
- Best flavor: Wardens are not just defenders of walls, but keepers of oaths, chains, and battlefield custody
|
||||
|
||||
**Shardok Ability: "Guard"**
|
||||
- Target adjacent friendly unit is protected until the Warden's next turn
|
||||
- First incoming attack against the protected unit is redirected to the Warden's unit or reduced
|
||||
- Costs action points and cannot target self
|
||||
- Creates a clear positioning puzzle without introducing another long-range damage button
|
||||
|
||||
**Implementation Notes:**
|
||||
- Add `Warden` to the Scala profession enum and common/client profession enums
|
||||
- Map Constitution to `Vector(Profession.Warden)` in `HeroStatGainAction`
|
||||
- Add display names, profession-gained notification copy, tutorial trigger mapping, and headshot bucket support
|
||||
- Add strategic availability and command handling only after deciding whether Warden uses Garrison, Custody, or both
|
||||
- For Custody, prefer extending the existing captured-hero/prisoner-management flow instead of creating a parallel
|
||||
prisoner system
|
||||
- For passive prisoner conversion, update `NewRoundAction` near the existing unaffiliated-hero `roundsInType` and
|
||||
`factionBiases` maintenance. If the province is ruled, contains a ruling-faction Warden, and the unaffiliated hero is
|
||||
a prisoner, add the Warden custody bonus to that prisoner's `factionBiases(rulingFactionId)`.
|
||||
- For Shardok, start with a single-turn guard status before attempting more complex interception chains
|
||||
|
||||
**Current Follow-Up Priorities:**
|
||||
- Teach the Shardok AI when to use the Warden evacuation command. The command is valuable when the Warden's side has
|
||||
captured enemy heroes and can secure them by leaving the battle, especially when the battle outcome is uncertain or
|
||||
trending against that side.
|
||||
- Add the strategic passive custody bonus: each round, prisoners in a province guarded by a ruling-faction Warden should
|
||||
get a small accumulated positive bias toward joining that faction. Prefer storing this as an addition to the existing
|
||||
`factionBiases` entry rather than changing the global `roundsInType` multiplier, so the bonus only reflects rounds
|
||||
actually spent under Warden custody.
|
||||
- Make tactical AI more protective of heroes at risk of capture when a Warden may be present. Warden evacuation makes
|
||||
captured heroes harder to recover, so the AI should treat exposing high-value heroes to capture as more dangerous than
|
||||
before. This matters even when the AI does not have perfect knowledge that a Warden is in the battle.
|
||||
|
||||
### 2. HERALD (Morale & Communication Specialist)
|
||||
|
||||
**Fantasy:** The inspiring leader who rallies troops and carries messages across the battlefield.
|
||||
|
||||
@@ -32,7 +130,7 @@ This document proposes 5 new hero professions for Eagle0. Each profession has ab
|
||||
|
||||
---
|
||||
|
||||
### 2. ALCHEMIST (Fire & Transformation Specialist)
|
||||
### 3. ALCHEMIST (Fire & Transformation Specialist)
|
||||
|
||||
**Fantasy:** The mad scientist who manipulates the elements through science, not magic.
|
||||
|
||||
@@ -47,21 +145,6 @@ This document proposes 5 new hero professions for Eagle0. Each profession has ab
|
||||
|
||||
---
|
||||
|
||||
### 3. WARDEN (Defensive Specialist)
|
||||
|
||||
**Fantasy:** The stalwart defender who holds the line and protects allies.
|
||||
|
||||
**Eagle Ability: "Garrison"**
|
||||
- A province with a Warden-led unit gets +1 to defense when attacked
|
||||
- Encourages strategic placement of defensive heroes
|
||||
|
||||
**Shardok Ability: "Intercept"**
|
||||
- Once per turn, when an adjacent friendly unit is attacked, the Warden's unit can take the hit instead
|
||||
- Creates a "bodyguard" mechanic that protects valuable units
|
||||
- Costs action points to maintain readiness
|
||||
|
||||
---
|
||||
|
||||
### 4. INQUISITOR (Anti-Magic & Intelligence)
|
||||
|
||||
**Fantasy:** The witch-hunter who counters supernatural threats and uncovers secrets.
|
||||
@@ -96,8 +179,174 @@ This document proposes 5 new hero professions for Eagle0. Each profession has ab
|
||||
|
||||
These professions were designed to:
|
||||
|
||||
1. **Fill mechanical gaps**: Warden provides defensive depth, Inquisitor counters magic-heavy strategies
|
||||
2. **Create counterplay**: Inquisitor vs Mage/Necromancer, Beastmaster vs Rangers (nature vs nature)
|
||||
3. **Avoid overlap**: Each has a unique niche not covered by existing professions
|
||||
4. **Support both layers**: Each ability is meaningful in its respective game mode
|
||||
5. **Enable interesting decisions**: Intercept creates bodyguard tactics, Inspire creates action economy choices
|
||||
1. **Fill mechanical gaps**: Warden gives Constitution a profession and adds defensive depth
|
||||
2. **Create counterplay**: Inquisitor vs Mage/Necromancer, Warden vs burst damage, Beastmaster vs beast events
|
||||
3. **Avoid overlap**: Each profession should own a tactical and strategic niche not covered by existing professions
|
||||
4. **Support both layers**: Each ability should be meaningful in Eagle and Shardok
|
||||
5. **Enable interesting decisions**: Guard creates positioning choices, Inspire creates action economy choices, Transmute
|
||||
creates resource tradeoffs
|
||||
|
||||
## Unified Candidate Ranking
|
||||
|
||||
This ranking considers every candidate together, regardless of where the idea came from. The main criteria are mechanical
|
||||
fit, overlap with existing professions, implementation clarity, strategic-layer value, Shardok value, and narrative
|
||||
payoff.
|
||||
|
||||
1. **Warden**: Best immediate fit. It gives Constitution a profession path and creates a distinctive custody/protection
|
||||
identity.
|
||||
2. **Quartermaster**: Best strategic depth after Warden. Logistics touches resources, armies, travel, and attrition
|
||||
without adding another damage specialist.
|
||||
3. **Envoy**: Best diplomacy/prisoner-system extension. Strong narrative payoff and clear Eagle-side value.
|
||||
4. **Surgeon**: Best loss-mitigation role. Makes battle aftermath less binary and gives non-magical healing a place.
|
||||
5. **Herald**: Strong morale and action-economy support. Distinct from Paladin if kept secular and battlefield-command
|
||||
focused.
|
||||
6. **Marshal**: Strong army identity, especially for formation play and troop organization, but needs careful separation
|
||||
from Champion.
|
||||
7. **Inquisitor**: Useful counterplay against Mage and Necromancer, though anti-magic should not become too narrow.
|
||||
8. **Pathfinder**: Good campaign movement role, but should focus on routes and terrain rather than replacing Ranger
|
||||
scouting.
|
||||
9. **Alchemist**: Good resource and terrain manipulation fantasy, but risks crowding Mage and Engineer unless scoped
|
||||
around risky transformations.
|
||||
10. **Spymaster**: Strong flavor and intelligence value, but needs slow strategic effects to avoid replacing Recon.
|
||||
11. **Artificer**: Fun temporary-device fantasy, but overlaps Engineer unless it owns one-shot preparation rather than
|
||||
construction.
|
||||
12. **Beastmaster**: Connects nicely to beast events, but overlaps Ranger unless focused on province events and wild
|
||||
allies.
|
||||
13. **Harbinger**: Excellent flavor, but probably later because fear/morale already touches Necromancer and Paladin
|
||||
space.
|
||||
|
||||
The best near-term sequence is likely **Warden**, then **Quartermaster** or **Envoy**. Avoid adding another pure damage
|
||||
caster until defensive, logistical, diplomatic, morale, and aftermath niches are better represented.
|
||||
|
||||
## Additional Candidate Details
|
||||
|
||||
These ideas come from looking at open mechanical space in Eagle and Shardok: logistics, loyalty, prisoners, province
|
||||
events, diplomacy, battlefield control, and non-damage support.
|
||||
|
||||
### Marshal
|
||||
|
||||
**Fantasy:** The army organizer who turns a pile of units into an actual campaign force.
|
||||
|
||||
**Best stat fit:** Strength or Charisma
|
||||
|
||||
**Eagle Ability: "Muster"**
|
||||
- Reorganize, reinforce, or prepare battalions more efficiently than a normal hero
|
||||
- Could reduce vigor cost for Organize Troops or improve training/armament transfer efficiency
|
||||
|
||||
**Shardok Ability: "Command Formation"**
|
||||
- Adjacent friendly units gain a small defensive or morale bonus while holding formation
|
||||
- Creates a tactical identity around positioning several units together
|
||||
|
||||
**Why it is interesting:** Champion currently owns heroic combat and training, but not army-level coordination. Marshal
|
||||
would be about disciplined formations rather than duels.
|
||||
|
||||
### Quartermaster
|
||||
|
||||
**Fantasy:** The logistics expert who keeps armies fed, paid, armed, and moving.
|
||||
|
||||
**Best stat fit:** Constitution or Wisdom
|
||||
|
||||
**Eagle Ability: "Provision"**
|
||||
- Move food/gold/supplies with less waste or farther than normal
|
||||
- Reduce attrition or readiness loss for armies operating away from strong provinces
|
||||
|
||||
**Shardok Ability: "Resupply"**
|
||||
- Restore limited ammunition, repair light damage, or grant a one-turn readiness buff to a nearby unit
|
||||
|
||||
**Why it is interesting:** The strategic layer already has meaningful resources. A profession that manipulates logistics
|
||||
would create strong choices without simply adding another combat spell.
|
||||
|
||||
### Envoy
|
||||
|
||||
**Fantasy:** The negotiator, hostage-broker, and oath-maker.
|
||||
|
||||
**Best stat fit:** Charisma
|
||||
|
||||
**Eagle Ability: "Parley"**
|
||||
- Improve diplomacy outcomes, ransom terms, prisoner returns, or truce/alliance offer odds
|
||||
- Could reduce the risk of ambassadors being imprisoned or create better return-prisoner rewards
|
||||
|
||||
**Shardok Ability: "Demand Surrender"**
|
||||
- Attempt to force a damaged or isolated enemy unit to flee, with odds based on charisma and battlefield state
|
||||
|
||||
**Why it is interesting:** Eagle has relationships, ransoms, truces, alliances, and prisoner choices. Envoy would make
|
||||
diplomacy feel like a profession rather than only a menu action.
|
||||
|
||||
### Spymaster
|
||||
|
||||
**Fantasy:** The patient handler of informants, rumors, sabotage, and false trails.
|
||||
|
||||
**Best stat fit:** Wisdom or Charisma
|
||||
|
||||
**Eagle Ability: "Infiltrate"**
|
||||
- Plant a delayed intelligence effect in an enemy province, revealing troop movements or weakening support
|
||||
- Could counter or complement Ranger recon without duplicating it
|
||||
|
||||
**Shardok Ability: "Sabotage"**
|
||||
- Before or during battle, reduce one enemy unit's readiness, movement, or first action effectiveness
|
||||
|
||||
**Why it is interesting:** Ranger is field reconnaissance. Spymaster can be slower, political, and province-focused.
|
||||
|
||||
### Surgeon
|
||||
|
||||
**Fantasy:** The healer who saves lives after the dramatic part of the story is over.
|
||||
|
||||
**Best stat fit:** Wisdom or Constitution
|
||||
|
||||
**Eagle Ability: "Triage"**
|
||||
- Reduce hero vigor loss, casualty severity, or prisoner death/execution fallout after battles
|
||||
- Could improve recovery in provinces with many wounded heroes or battered battalions
|
||||
|
||||
**Shardok Ability: "Stabilize"**
|
||||
- Prevent a nearby friendly hero unit from being captured or destroyed once per battle, leaving it routed or exhausted
|
||||
instead
|
||||
|
||||
**Why it is interesting:** Paladin has holy support, but not mundane medical recovery. Surgeon creates a grounded support
|
||||
role that can make losses less binary.
|
||||
|
||||
### Artificer
|
||||
|
||||
**Fantasy:** The maker of rare devices, siege tools, lenses, traps, and battlefield instruments.
|
||||
|
||||
**Best stat fit:** Wisdom or Agility
|
||||
|
||||
**Eagle Ability: "Prototype"**
|
||||
- Invest gold and vigor to create a temporary province or battalion enhancement
|
||||
- Examples: better siege readiness, scouting lenses, defensive traps, or weatherproof stores
|
||||
|
||||
**Shardok Ability: "Deploy Device"**
|
||||
- Place a one-use trap, barricade, signal flare, or field tool on a nearby hex
|
||||
|
||||
**Why it is interesting:** Engineer currently owns building and repair. Artificer can own temporary inventions and
|
||||
one-shot tactical preparation.
|
||||
|
||||
### Harbinger
|
||||
|
||||
**Fantasy:** The terrifying omen-bearer whose arrival changes morale before the first blow lands.
|
||||
|
||||
**Best stat fit:** Charisma or Wisdom
|
||||
|
||||
**Eagle Ability: "Portent"**
|
||||
- Lower enemy support, increase unrest, or amplify the psychological effect of victories and executions
|
||||
- Could be risky: fear-based rule damages diplomacy or loyalty if overused
|
||||
|
||||
**Shardok Ability: "Dread Standard"**
|
||||
- Enemies near the Harbinger suffer morale penalties or worse flee odds
|
||||
|
||||
**Why it is interesting:** Necromancer has supernatural fear, but Harbinger could be political and symbolic rather than
|
||||
undead-focused.
|
||||
|
||||
### Pathfinder
|
||||
|
||||
**Fantasy:** The guide who knows hidden passes, river crossings, and winter roads.
|
||||
|
||||
**Best stat fit:** Agility or Constitution
|
||||
|
||||
**Eagle Ability: "Find Passage"**
|
||||
- Move armies or heroes through difficult terrain, winter, blizzards, or river-heavy borders with lower penalties
|
||||
- Could create one-turn temporary travel links between neighboring provinces under specific conditions
|
||||
|
||||
**Shardok Ability: "Open Route"**
|
||||
- Let a nearby unit ignore one terrain penalty or cross a difficult hex safely this turn
|
||||
|
||||
**Why it is interesting:** Ranger owns stealth and scouting. Pathfinder owns movement and campaign geography.
|
||||
|
||||
@@ -486,7 +486,7 @@ jobs:
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
lfs: false
|
||||
|
||||
@@ -517,7 +517,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
needs: []
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
lfs: false
|
||||
|
||||
@@ -555,7 +555,7 @@ jobs:
|
||||
needs: [build-eagle, build-shardok]
|
||||
environment: production
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Deploy to DigitalOcean
|
||||
uses: appleboy/ssh-action@v1.0.0
|
||||
|
||||
+130
-234
@@ -1,267 +1,163 @@
|
||||
# Tutorial Battle System
|
||||
|
||||
This document describes the tutorial battle system for first-time players. The system provides a scripted introductory battle that teaches combat basics while telling a story.
|
||||
This document describes the current opening tutorial battle. It is based on the live implementation in `TutorialGameCreation.scala`, `TutorialBattleController.cpp`, `ShardokGamesManager.cpp`, `ResolveBattleAction.scala`, and the Unity dialogue scripts.
|
||||
|
||||
For the Unity tutorial UI/dialogue architecture, see `TUTORIAL_SYSTEM.md`. For copy and content notes, see `TUTORIAL_CONTENT.md`.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
## Current Flow
|
||||
|
||||
When a new player starts their first game in tutorial mode, they experience:
|
||||
When a player creates a tutorial game at the default `OpeningBattle` phase:
|
||||
|
||||
1. **Narrative Intro** - Story screens introducing the scenario
|
||||
2. **Immediate Battle** - Skip strategic map, start directly in combat
|
||||
3. **Scripted Flee** - Enemy flees when player is "on the ropes"
|
||||
4. **Reinforcements** - Allied heroes arrive mid-battle
|
||||
5. **Heroes Join** - Reinforcement heroes join player's faction after battle
|
||||
1. `TutorialGameCreation.createTutorialGame()` creates a `GameType.Tutorial(TutorialPhase.OpeningBattle)` game starting in `RoundPhase.BattleRequest`.
|
||||
2. Province 14, **Onmaa**, belongs to Sadar Rakon's faction and already has a defending army.
|
||||
3. Bregos Fyar's faction has a hostile attacking army, led by Ikhaan Tarn, already moving from province 31 to Onmaa.
|
||||
4. `RequestBattlesAction` immediately creates the Shardok battle and tags tutorial reinforcement hero IDs `100` and `101`.
|
||||
5. `GamesManager` stores the generated `TutorialBattleConfig` for this game and passes it to Shardok when the battle starts.
|
||||
6. Shardok creates the battle with the configured timed reinforcement events.
|
||||
7. Unity plays the active dialogue scripts from `tutorial_strategic.json` and `tutorial_battle.json`.
|
||||
|
||||
There is also a tutorial phase-start path. Starting after `OpeningBattle` applies precomputed tutorial setup results from `TutorialPhaseResultsLoader`; this is why the Unity post-battle trigger logic handles a "battle was never observed" auto-resolve path.
|
||||
|
||||
---
|
||||
|
||||
## Battle Configuration
|
||||
## Battle Setup
|
||||
|
||||
### Defender (Player - John Ranil)
|
||||
- **Heroes**: John Ranil + 2 random vassals (3 total)
|
||||
- **Units**:
|
||||
- 2x Light Infantry (300 troops each, 60 training/armament)
|
||||
- 1x Longbowmen (200 troops, 60 training/armament)
|
||||
- **Province**: 14 (Onmaa) with 40 support, 500 gold, 3000 food
|
||||
- **Restriction**: Cannot flee (must defend)
|
||||
### Defender: Sadar Rakon's Faction
|
||||
|
||||
### Attacker (Ikhaan Tarn)
|
||||
- **Heroes**: Ikhaan Tarn + 2 sworn brothers (3 total)
|
||||
- **Units**:
|
||||
- 1x Heavy Cavalry (600 troops, 80 training/armament)
|
||||
- 1x Heavy Infantry (500 troops, 80 training/armament)
|
||||
- 1x Longbowmen (300 troops, 80 training/armament)
|
||||
- **Origin Province**: 31
|
||||
Configured in `src/main/resources/net/eagle0/eagle/tutorial_parameters.json`:
|
||||
|
||||
### Flee Trigger
|
||||
Attacker flees when ANY of these conditions are met:
|
||||
- Player loses 1 unit
|
||||
- 5 rounds have passed
|
||||
- **Faction ID:** 3
|
||||
- **Province:** 14, Onmaa
|
||||
- **Starting support:** 23
|
||||
- **Starting resources:** 50 gold, 4000 food
|
||||
- **Heroes:** Sadar Rakon, Old Marek the Learned, Agamemnon
|
||||
- **Battalions:**
|
||||
- Rakon's Loyalists: Light Infantry, 529 size, 80 training, 75 armament
|
||||
- Onmaa Defenders: Light Infantry, 311 size, 80 training, 60 armament
|
||||
- Hunters of the Steppe: Longbowmen, 478 size, 80 training, 60 armament
|
||||
|
||||
### Reinforcements
|
||||
When Tarn flees, these heroes arrive as player reinforcements:
|
||||
- Elena Fyar
|
||||
- Hedrick
|
||||
- The Boulder
|
||||
### Attacker: Bregos Fyar's Faction
|
||||
|
||||
The opening attacking army is configured under Bregos Fyar's `attackingArmies`:
|
||||
|
||||
- **Faction ID:** 2
|
||||
- **Origin province:** 31
|
||||
- **Destination province:** 14, Onmaa
|
||||
- **Heroes:** Ikhaan Tarn, Tall Edgtheow, Waylaid Julius, Luke the Prank-tricker
|
||||
- **Battalions:**
|
||||
- Doomriders: Heavy Cavalry, 592 size, 80 training, 80 armament
|
||||
- The Shardok's Guard: Heavy Infantry, 507 size, 80 training, 80 armament
|
||||
- Bowmen of Nikemi: Longbowmen, 291 size, 80 training, 80 armament
|
||||
- Swift Sabres: Light Cavalry, 450 size, 80 training, 80 armament
|
||||
|
||||
Tarn's unit is made visible to the defender from the start through `TutorialBattleConfig.initial_visibilities`.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Architecture
|
||||
## Reinforcements
|
||||
|
||||
### Phase 1: Narrative Intro System
|
||||
The current tutorial config has two timed reinforcement events. It does not currently configure a scripted Tarn flee event.
|
||||
|
||||
**Proto Changes:**
|
||||
- `game_state_view.proto`: Added `TutorialNarrativeScreen` message and `pending_narrative_screens` field
|
||||
- `game_parameters.proto`: Added `TutorialNarrativeScreen` and `tutorial_narrative_screens` field
|
||||
| Event ID | Trigger | Hero ID | Hero | Battalion |
|
||||
|----------|---------|---------|------|-----------|
|
||||
| `elena_reinforcement_round_5` | End of round 5 or later | 101 | Elena Fyar | Fyar's Vanguard, Heavy Infantry, 535 size, 75 training, 75 armament |
|
||||
| `ranil_reinforcement_round_7` | End of round 7 or later | 100 | John Ranil | Ranil's Riders, Heavy Cavalry, 458 size, 80 training, 80 armament |
|
||||
|
||||
**Unity Client:**
|
||||
- `NarrativeScreenController.cs`: Modal screen with title, body text, optional image
|
||||
- `EagleGameController.cs`: Checks for pending narrative screens on game start
|
||||
Shardok creates reinforcement units at battle creation time with `PENDING_REINFORCEMENT` status. `TutorialBattleController` activates them when their event triggers and returns a `TUTORIAL_REINFORCEMENTS_ARRIVED` action result. Unity maps that action to profession-specific dialogue triggers:
|
||||
|
||||
**Narrative Content** (defined in `tutorial_game_parameters.json`):
|
||||
```
|
||||
Screen 1: "The Engineer of Onmaa" - Player backstory
|
||||
Screen 2: "The Traitor Arrives" - Tarn's attack
|
||||
Screen 3: "Defend Your Home" - Call to action
|
||||
```
|
||||
- `tutorial_reinforcement_paladin` for Elena Fyar
|
||||
- `tutorial_reinforcement_engineer` for John Ranil
|
||||
|
||||
### Phase 2: Auto-Start Battle
|
||||
|
||||
**Configuration:**
|
||||
- `tutorial_game_parameters.json`: Contains `tutorialBattle` config with attacker, target, flee conditions
|
||||
- `game_parameters.proto`: `TutorialBattleConfig` message
|
||||
|
||||
**Game Creation:**
|
||||
- `NewGameCreation.scala`: `setupTutorialBattle()` method creates:
|
||||
- `HostileArmyGroup` with `Attacking` status (bypasses decision phase)
|
||||
- `Army` for defender
|
||||
- Both added to target province
|
||||
|
||||
**Battle Creation:**
|
||||
- When round advances to `BattleRequest` phase, `RequestBattlesAction` automatically creates the `ShardokBattle`
|
||||
|
||||
### Phase 3: Scripted Flee Mechanism
|
||||
|
||||
**C++ Component:**
|
||||
- `TutorialBattleController.hpp/cpp`: Controller for scripted battle logic
|
||||
|
||||
**Key Methods:**
|
||||
```cpp
|
||||
// Check all events and execute triggered actions (called at end of each round)
|
||||
std::vector<ActionResult> CheckAndExecuteEvents(
|
||||
const GameStateW& state,
|
||||
const SettingsGetter& settings,
|
||||
const std::shared_ptr<RandomGenerator>& randomGenerator);
|
||||
```
|
||||
|
||||
**Integration:**
|
||||
- `ShardokEngine.cpp`: Calls `CheckAndExecuteEvents()` at end of each round in `HandlePlayerTurnEnd()`
|
||||
- Events fire in config order, each event fires at most once
|
||||
|
||||
### Phase 4: Mid-Battle Reinforcements
|
||||
|
||||
**Action Types** (in `action_type.proto`):
|
||||
- `TUTORIAL_ENEMY_FLED = 78`
|
||||
- `TUTORIAL_REINFORCEMENTS_ARRIVED = 79`
|
||||
|
||||
**Implementation (TODO):**
|
||||
- When scripted flee triggers, create reinforcement units in defender's reserves
|
||||
- Units have `UnitStatus_RESERVE_UNIT` status
|
||||
- Reinforcements enter battle on subsequent turns
|
||||
|
||||
### Phase 5: Tutorial Popups
|
||||
|
||||
**Content Definitions** (in `TutorialContentDefinitions.cs`):
|
||||
- `tutorial_battle_started`: "Defend your province!"
|
||||
- `tutorial_enemy_fled`: "The enemy retreats!"
|
||||
- `tutorial_reinforcements_arrived`: "Allies have arrived!"
|
||||
- `tutorial_battle_victory`: Victory celebration
|
||||
|
||||
**Trigger Integration:**
|
||||
- `TutorialTriggerRegistry.cs`: Handles tutorial action types from `ActionResultView`
|
||||
- `ShardokGameController.cs`: Detects tutorial events and triggers popups
|
||||
|
||||
### Phase 6: Post-Battle Hero Joining
|
||||
|
||||
**Implementation (TODO):**
|
||||
- `ResolveBattleAction.scala`: Check if battle was tutorial and defender won
|
||||
- Create `TutorialHeroJoined` action results adding reinforcement heroes to player faction
|
||||
- Set appropriate loyalty, vigor, location for joined heroes
|
||||
|
||||
### Phase 7: Post-Battle Strategic Dialogue
|
||||
|
||||
After the battle ends and the player returns to the strategic map, narrative dialogues guide them through the next steps.
|
||||
|
||||
**Dialogue 1: Battle Aftermath** (trigger: `tutorial_battle_ended`)
|
||||
- Old Marek reflects on the close call
|
||||
- Tarn has vanished — rumors of sorcery or escape, no one knows
|
||||
- Captured lieutenants: they followed orders, not to blame for Tarn's madness
|
||||
- Player should try to recruit them or hold them
|
||||
- No instruction text needed — Handle Captured Heroes UI is self-explanatory
|
||||
|
||||
**Dialogue 2: Rebuild Support** (trigger: `tutorial_rebuild_support`)
|
||||
- Fires when available commands no longer include HandleCapturedHeroCommand
|
||||
- Marek explains the province needs support rebuilt after the battle
|
||||
- Engineers (John Ranil) can **Improve** the province — build infrastructure, develop economy
|
||||
- Paladins (Elena Fyar) can **Give Alms** — distribute food to win hearts
|
||||
- Instructions highlight **Improve** and **Give Alms** buttons
|
||||
- Goal: get Support to **40** before January for tax revenue
|
||||
|
||||
**Implementation:**
|
||||
- Dialogues defined in `tutorial_strategic.json`
|
||||
- `tutorial_battle_ended`: fire when tutorial battle is removed from `RunningShardokGameModels`
|
||||
- `tutorial_rebuild_support`: fire when available commands no longer include `HandleCapturedHeroCommand`
|
||||
- Register `ImproveButton` and `AlmsButton` as highlight targets in `TutorialTargetRegistry`
|
||||
|
||||
See `TUTORIAL_CONTENT.md` for full dialogue text.
|
||||
The reinforcement action includes an `attacker_starting_position_index`; Shardok resolves that to currently open map positions and starts a reinforcement placement phase.
|
||||
|
||||
---
|
||||
|
||||
## File Summary
|
||||
## Scripted Event Controller
|
||||
|
||||
`TutorialBattleController` is a generic event-driven controller, even though the current tutorial battle only uses timed reinforcements.
|
||||
|
||||
Supported trigger types:
|
||||
|
||||
- `after_round`
|
||||
- `units_lost`
|
||||
- `damage_taken`
|
||||
- `unit_killed`
|
||||
|
||||
Supported action types:
|
||||
|
||||
- `flee`
|
||||
- `reinforcements`
|
||||
|
||||
Events are checked at the end of each round, evaluated in config order, and each `event_id` fires at most once.
|
||||
|
||||
---
|
||||
|
||||
## Battle Resolution
|
||||
|
||||
`RequestBattlesAction` marks tutorial opening battles with `reinforcementHeroIds = Set(100, 101)`. `ResolveBattleAction` uses that metadata so reinforcement heroes are accepted when battle results return, even though they were not part of the original defending army.
|
||||
|
||||
After the opening battle resolves, `ResolveBattleAction` applies `TutorialTarnDisappearsAction` for `GameType.Tutorial(TutorialPhase.OpeningBattle)`. Tarn is removed from captured/unaffiliated province state and from his faction's leaders list, matching the post-battle dialogue that he has vanished.
|
||||
|
||||
`TutorialBattleAutoResolve` provides a synthetic defender victory for tutorial setup paths that skip past the opening battle. In that synthetic result:
|
||||
|
||||
- The defender wins.
|
||||
- Reinforcement heroes 100 and 101 survive.
|
||||
- Tarn is marked outlawed/escaped.
|
||||
- Other attacker heroes are captured.
|
||||
- Defender battalions take about 30% casualties.
|
||||
- Attacker battalions are destroyed.
|
||||
|
||||
---
|
||||
|
||||
## Unity Dialogue Hooks
|
||||
|
||||
Strategic tutorial dialogue:
|
||||
|
||||
- `game_started`: opening Onmaa monologue, ending with `FightButton` highlighted.
|
||||
- `tutorial_battle_ended`: post-battle aftermath.
|
||||
- `tutorial_rebuild_support`: support rebuilding guidance after captured-hero handling is done.
|
||||
|
||||
Battle tutorial dialogue:
|
||||
|
||||
- `shardok_placement_started`: placement and unit overview.
|
||||
- `shardok_battle_running`: first player turn guidance.
|
||||
- Ability/terrain triggers such as `archery_available`, `melee_available`, `ability_charge_available`, `start_fire_available`, `thunderstorm`, `duel_available`, `hide_available`, and `engineer_near_enemy`.
|
||||
- Reinforcement triggers `tutorial_reinforcement_paladin` and `tutorial_reinforcement_engineer`.
|
||||
- Capture triggers `enemy_hero_captured` and `friendly_hero_captured`.
|
||||
- `shardok_battle_reset`: replay dialogue after a battle reset.
|
||||
|
||||
---
|
||||
|
||||
## Files To Check First
|
||||
|
||||
### New Files
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `TutorialBattleController.hpp/cpp` | C++ scripted flee and reinforcement logic |
|
||||
| `NarrativeScreenController.cs` | Unity narrative screen display |
|
||||
|
||||
### Modified Files
|
||||
| File | Changes |
|
||||
|------|---------|
|
||||
| `tutorial_game_parameters.json` | Battle configuration and narrative content |
|
||||
| `game_state_view.proto` | Narrative screens field |
|
||||
| `game_parameters.proto` | Tutorial battle config |
|
||||
| `player_info.proto` | Tutorial battle flags for Shardok |
|
||||
| `action_type.proto` | Tutorial action types |
|
||||
| `NewGameCreation.scala` | Auto-start battle setup |
|
||||
| `EagleGameController.cs` | Narrative display integration |
|
||||
| `TutorialContentDefinitions.cs` | Battle tutorial content |
|
||||
| `TutorialTriggerRegistry.cs` | Tutorial event triggers |
|
||||
|
||||
---
|
||||
|
||||
## Remaining Implementation
|
||||
|
||||
### Disable Player Flee
|
||||
In `FleeCommandFactory.cpp`:
|
||||
- Check if tutorial mode is enabled
|
||||
- If defender, don't offer flee commands
|
||||
|
||||
### Reinforcement Units
|
||||
In `TutorialBattleController::ExecuteScriptedFlee()`:
|
||||
- Create `Unit` objects for Elena Fyar, Hedrick, The Boulder
|
||||
- Add to defender's reserve units
|
||||
- Generate `TUTORIAL_REINFORCEMENTS_ARRIVED` action
|
||||
|
||||
### Post-Battle Hero Joining
|
||||
In Eagle's battle resolution:
|
||||
- Detect tutorial battle completion
|
||||
- Create heroes in player's faction with proper stats
|
||||
- Generate notification action results
|
||||
| `src/main/resources/net/eagle0/eagle/tutorial_parameters.json` | Tutorial map, factions, starting provinces, armies |
|
||||
| `src/main/scala/net/eagle0/eagle/service/new_game_creation/TutorialGameCreation.scala` | Opening battle setup and `TutorialBattleConfig` creation |
|
||||
| `src/main/protobuf/net/eagle0/common/tutorial_battle_config.proto` | Scripted event config schema |
|
||||
| `src/main/cpp/net/eagle0/shardok/server/ShardokGamesManager.cpp` | Adds pending reinforcement units and applies initial visibility |
|
||||
| `src/main/cpp/net/eagle0/shardok/library/tutorial/TutorialBattleController.cpp` | Evaluates scripted events and creates Shardok action results |
|
||||
| `src/main/scala/net/eagle0/eagle/library/actions/impl/action/RequestBattlesAction.scala` | Creates the opening battle and tags reinforcement hero IDs |
|
||||
| `src/main/scala/net/eagle0/eagle/library/actions/impl/action/ResolveBattleAction.scala` | Resolves reinforcements and applies Tarn disappearance |
|
||||
| `src/main/scala/net/eagle0/eagle/service/tutorial/TutorialBattleAutoResolve.scala` | Synthetic result for phase-start paths after the opening battle |
|
||||
| `src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Tutorial/Triggers/TutorialTriggerRegistry.cs` | Converts strategic and tactical events into dialogue triggers |
|
||||
| `src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Resources/Dialogues/tutorial_battle.json` | Active battle dialogue content |
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] New user sees narrative screens before battle
|
||||
- [ ] Battle starts immediately after narratives (no strategic map)
|
||||
- [ ] Player has correct units (3 heroes, 3 battalions)
|
||||
- [ ] Attacker has correct units (3 heroes, 3 battalions)
|
||||
- [ ] Player cannot flee
|
||||
- [ ] Tarn flees after player loses 1 unit
|
||||
- [ ] Tarn flees after 5 rounds (if no units lost)
|
||||
- [ ] Reinforcements appear when Tarn flees
|
||||
- [ ] Tutorial popups appear at correct moments
|
||||
- [ ] Reinforcement heroes join faction after battle victory
|
||||
|
||||
---
|
||||
|
||||
## Configuration Reference
|
||||
|
||||
### tutorial_game_parameters.json
|
||||
```json
|
||||
{
|
||||
"tutorialBattle": {
|
||||
"attackerFactionHead": "Ikhaan Tarn",
|
||||
"targetProvinceId": 14,
|
||||
"fleeAfterDefenderUnitsLost": 1,
|
||||
"fleeAfterRounds": 5,
|
||||
"reinforcements": ["Elena Fyar", "Hedrick", "The Boulder"]
|
||||
},
|
||||
"tutorialNarrativeScreens": [
|
||||
{ "title": "...", "bodyText": "...", "imagePath": "" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### TutorialBattleConfig Proto (Shardok)
|
||||
```protobuf
|
||||
message TutorialBattleConfig {
|
||||
bool enabled = 1;
|
||||
repeated TutorialEvent events = 2;
|
||||
repeated UnitInitialSize initial_sizes = 3; // For damage tracking
|
||||
}
|
||||
|
||||
message TutorialEvent {
|
||||
string event_id = 1;
|
||||
TutorialTrigger trigger = 2;
|
||||
TutorialAction action = 3;
|
||||
}
|
||||
|
||||
message TutorialTrigger {
|
||||
oneof trigger_type {
|
||||
RoundTrigger after_round = 1;
|
||||
UnitsLostTrigger units_lost = 2;
|
||||
DamageTakenTrigger damage_taken = 3;
|
||||
UnitKilledTrigger unit_killed = 4;
|
||||
}
|
||||
}
|
||||
|
||||
message TutorialAction {
|
||||
oneof action_type {
|
||||
FleeAction flee = 1;
|
||||
ReinforcementsAction reinforcements = 2; // Uses CommonUnit
|
||||
}
|
||||
}
|
||||
```
|
||||
- [ ] Tutorial game starts in BattleRequest and creates an Onmaa battle immediately.
|
||||
- [ ] Opening strategic dialogue highlights the Fight button.
|
||||
- [ ] Tarn's unit is visible to the defender from battle start.
|
||||
- [ ] Placement dialogue fires during setup.
|
||||
- [ ] First-turn battle-running dialogue fires on the player's first active turn.
|
||||
- [ ] Elena Fyar arrives from the configured round-5 event.
|
||||
- [ ] John Ranil arrives from the configured round-7 event.
|
||||
- [ ] Reinforcement arrival dialogues fire once per hero.
|
||||
- [ ] Battle reset clears battle dialogue completion for replay.
|
||||
- [ ] Post-battle aftermath fires after the battle leaves `RunningShardokGameModels`.
|
||||
- [ ] Rebuild-support dialogue waits until captured-hero handling and visible notifications are done.
|
||||
- [ ] Starting a tutorial at a later phase uses precomputed setup and still reaches coherent strategic dialogue state.
|
||||
|
||||
+13
-13
@@ -1,6 +1,8 @@
|
||||
# Tutorial Content Guide
|
||||
|
||||
This document defines all tutorial content. Edit this to refine the text, then update `TutorialContentDefinitions.cs` to match.
|
||||
This document describes tutorial content. The active tutorial experience is driven by the narrative dialogue JSON files under `src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Resources/Dialogues/`.
|
||||
|
||||
The older step-based modal/overlay content in `TutorialContentDefinitions.cs` is currently dormant: `RegisterAll()` returns before registering any sequences. Keep that in mind when editing this file; changing `TutorialContentDefinitions.cs` will not affect the live tutorial until the early return is removed and the overlap with dialogue triggers is audited.
|
||||
|
||||
---
|
||||
|
||||
@@ -181,17 +183,16 @@ These fire after the first tutorial battle ends and the player returns to the st
|
||||
>
|
||||
> Any of our heroes can develop the land or give alms to the people — nothing wins hearts faster than a full belly. Engineers like John Ranil are especially effective at improving a province, and paladins like Elena Fyar are the best at winning hearts. Either way, we need the people's support before the tax collectors come round in January.
|
||||
|
||||
**Instructions:** Use **Improve** and **Give Alms** to raise Support. You must have at least **40** support in the province by January in order to collect gold and food in taxes.
|
||||
**Instructions:** John Ranil's step highlights `ImproveProvinceButton` and explains **Improve**. Elena Fyar's step highlights `GiveAlmsButton` and explains **Give Alms**. You must have at least **40** support in the province by January in order to collect gold and food in taxes.
|
||||
|
||||
**Highlight targets:** `SupportField`
|
||||
**Highlight targets:** `ImproveProvinceButton`, `GiveAlmsButton`
|
||||
|
||||
### Implementation Notes
|
||||
|
||||
- Both dialogues go in `tutorial_strategic.json`
|
||||
- Need new trigger events: `tutorial_battle_ended` and `tutorial_rebuild_support`
|
||||
- `tutorial_battle_ended` should fire when the tutorial battle is removed from `RunningShardokGameModels` in `EagleGameModel.ApplyGameStateViewDiff`
|
||||
- `tutorial_rebuild_support` should fire when available commands no longer include `HandleCapturedHeroCommand` (i.e. the captured heroes phase is done and the player has regular commands available)
|
||||
- The "rebuild" dialogue should highlight the Improve and Give Alms buttons in the command panel (requires registering them as highlight targets)
|
||||
- Both dialogues are defined in `tutorial_strategic.json`.
|
||||
- `tutorial_battle_ended` fires from `TutorialTriggerRegistry.CheckTutorialBattleEnded()` when the opening battle is gone and strategic commands are available. It also handles the server-side auto-resolve path where the Unity client never observed a running Shardok model.
|
||||
- `tutorial_rebuild_support` fires from `TutorialTriggerRegistry.CheckTutorialRebuildSupport()` when available commands no longer include `HandleCapturedHeroCommand` and the notification panel has no visible info.
|
||||
- `ImproveProvinceButton` and `GiveAlmsButton` are runtime command-button targets registered by `CommandButtonPanelController`.
|
||||
|
||||
---
|
||||
|
||||
@@ -210,11 +211,10 @@ These fire after the first tutorial battle ends and the player returns to the st
|
||||
## Adding New Tutorials
|
||||
|
||||
1. Add entry to this document
|
||||
2. Update `TutorialContentDefinitions.cs`:
|
||||
- For onboarding: add to `CreateOnboardingSequence()`
|
||||
- For contextual: add to `RegisterStrategicTutorials()` or `RegisterTacticalTutorials()`
|
||||
3. Ensure trigger event exists in `TutorialTriggerRegistry.cs`
|
||||
4. Test the flow
|
||||
2. For active tutorial content, add or edit a script in `Assets/Resources/Dialogues/tutorial_strategic.json` or `tutorial_battle.json`.
|
||||
3. Ensure the trigger event exists in `TutorialTriggerRegistry.cs` or is fired directly by the relevant controller.
|
||||
4. If reviving the dormant step-UI system, remove the early return in `TutorialContentDefinitions.RegisterAll()` only after auditing duplicate trigger coverage with the dialogue JSON.
|
||||
5. Test the flow.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+16
-11
@@ -3,7 +3,7 @@
|
||||
This document describes how the Unity client's tutorial system is wired together: the classes involved, the trigger catalog, the step lifecycle, and the expected first-session flow. It complements two existing docs:
|
||||
|
||||
- **`TUTORIAL_CONTENT.md`** — human-facing copy for tutorial steps and dialogues
|
||||
- **`TUTORIAL_BATTLE_SYSTEM.md`** — the scripted first-battle scenario (Tarn vs. John Ranil)
|
||||
- **`TUTORIAL_BATTLE_SYSTEM.md`** — the scripted opening battle at Onmaa
|
||||
|
||||
It also overlaps slightly with the in-tree `Assets/Tutorial/TUTORIAL_PLAN.md`, which is an older implementation plan.
|
||||
|
||||
@@ -20,12 +20,13 @@ There are **two parallel subsystems** that share the same trigger plumbing:
|
||||
| Step-based tutorial UI (modals, overlays, hints) | **Dormant** | `Tutorial/TutorialManager.cs`, `Tutorial/UI/*`, `Tutorial/Content/*` |
|
||||
| Narrative dialogue system | **Live** | `Tutorial/Dialogue/*`, `Resources/Dialogues/*.json` |
|
||||
|
||||
`TutorialContentDefinitions.RegisterAll()` returns early at line 17 with the comment *"Old tutorial content suppressed — replaced by narrative dialogue system."* As a result:
|
||||
`TutorialContentDefinitions.RegisterAll()` returns early with the comment *"Old tutorial content suppressed — replaced by narrative dialogue system."* As a result:
|
||||
|
||||
- `OnboardingSequence` is never assigned, so `StartOnboarding()` no-ops at the "No onboarding sequence assigned" branch.
|
||||
- No contextual tutorial sequences are registered with the trigger registry.
|
||||
- The trigger registry still fires events normally, but nothing on the step-UI side is listening.
|
||||
- `DialogueManager` consumes those same events and matches them against scripts in `Resources/Dialogues/` (`tutorial_strategic.json`, `tutorial_battle.json`).
|
||||
- Dialogue completion is persisted per game through `TutorialDialogueProgressStore`, so completed scripts do not replay after a reconnect or scene reload for the same game.
|
||||
|
||||
**In practice today, the entire active tutorial experience runs through the dialogue system.** The step-UI machinery is preserved for future use — content definitions still exist below the early return in `TutorialContentDefinitions.cs` for reference.
|
||||
|
||||
@@ -37,6 +38,7 @@ There are **two parallel subsystems** that share the same trigger plumbing:
|
||||
- **`Tutorial/TutorialManager.cs`** — Singleton. Owns `OnboardingSequence`, the active step queue, the trigger registry, and the dialogue manager handle. Initialized by `EagleGameController.SetUpGame()` and `ShardokGameController.SetUpGame()`.
|
||||
- **`Tutorial/TutorialState.cs`** — PlayerPrefs persistence. Tracks `OnboardingCompleted`, `OnboardingStepReached`, completed sequence IDs (HashSet for O(1) lookup), dismissed hints, and a global "tutorials disabled" preference.
|
||||
- **`Tutorial/TutorialTargetRegistry.cs`** — Maps string IDs to `RectTransform`s for highlighting. Static targets are Inspector-assigned (`ProvinceInfoPanel`, `SupportField`, `CommitButton`, etc.); dynamic targets register at runtime (e.g., hero rows from `HeroesAndBattalionsPanelController`).
|
||||
- **`Tutorial/TutorialDialogueCoordinator.cs`** — Recomputes durable dialogue triggers from the current strategic model and sends them directly to `DialogueManager`. This covers reconnects, phase-start tutorial games, and strategic beats that should be recoverable from state.
|
||||
|
||||
### Triggers
|
||||
- **`Tutorial/Triggers/TutorialTriggerRegistry.cs`** (~1100 lines) — Routes game events to both the step UI (when sequences are registered) and `DialogueManager`. Maintains one-shot session flags so the same first-encounter trigger doesn't fire twice.
|
||||
@@ -94,14 +96,14 @@ Panels can be anchored via `TutorialPanelAnchor` (`Center` / `Left` / `Right` /
|
||||
|
||||
## Trigger Catalog
|
||||
|
||||
All triggers are raised via `TutorialTriggerRegistry`. Today these flow to `DialogueManager.TriggerDialogue()` and are matched against the dialogue JSON; if step-UI sequences are re-registered, they will also route there. File:line citations are for the registry unless noted; line numbers may drift.
|
||||
Most event-style triggers are raised via `TutorialTriggerRegistry`. Durable strategic dialogue beats are also recomputed by `TutorialDialogueCoordinator` on each model update and sent directly to `DialogueManager.TriggerDialogue()`. Both paths match against the dialogue JSON; if step-UI sequences are re-registered, only the registry path will route to them. File:line citations are for the registry unless noted; line numbers may drift.
|
||||
|
||||
### Bootstrap / first-session
|
||||
| Trigger | Fires from | When |
|
||||
|---------|-----------|------|
|
||||
| `game_started` | `EagleGameController.SetUpGame()` | First time entering a tutorial game |
|
||||
| `game_started` | `TutorialDialogueCoordinator.EligibleTriggers()` | Any tutorial strategic model update; dialogue progress keeps it from replaying |
|
||||
| `first_battle_available` | `OnModelUpdated()` ~L204 | A `RunningShardokGameModel` first appears |
|
||||
| `battle_entered` | `OnBattleEntered()` ~L793 | User clicks **Battle!** |
|
||||
| `battle_entered` | `OnBattleEntered()` ~L793 | User clicks **Fight!** |
|
||||
| `tutorial_battle_ended` | `CheckTutorialBattleEnded()` ~L594 | Tutorial battle removed from running models |
|
||||
| `tutorial_rebuild_support` | `CheckTutorialRebuildSupport()` ~L616 | Captured-heroes phase done |
|
||||
| `tutorial_taxes_collected` | `CheckTaxesCollected()` ~L680 | New Year action with positive tax delta |
|
||||
@@ -135,7 +137,7 @@ All triggers are raised via `TutorialTriggerRegistry`. Today these flow to `Dial
|
||||
| `hero_stat_gained` | ~L281 | `HeroStatGained` action result |
|
||||
| `hero_profession_gained` | ~L284 | `ProfessionGained` action result |
|
||||
| `tutorial_faction_appears` | ~L287 | `TutorialFactionAppears` action — the Fracture Covenant landing |
|
||||
| `tutorial_hero_faction_appears` | ~L290 | `TutorialHeroFactionAppears` action (no dialogue script today) |
|
||||
| `tutorial_hero_faction_appears` | ~L290 | `TutorialHeroFactionAppears` action; has a four-step strategic dialogue today |
|
||||
| `tutorial_hero_departed` | ~L299 | First `HeroesDeparted` action against another player (King's hero abandons service) |
|
||||
| `tutorial_hero_departed_again` | ~L302 | Second `HeroesDeparted` in a *different* month than the first |
|
||||
|
||||
@@ -220,8 +222,8 @@ Script shape (see `tutorial_strategic.json`):
|
||||
"speakerName": "Old Marek the Learned",
|
||||
"speakerImagePath": "fixed/old_marek_the_learned.png",
|
||||
"dialogueText": "...",
|
||||
"instructionText": "Click Battle! to enter tactical combat.",
|
||||
"highlightTarget": "GoToBattleButton",
|
||||
"instructionText": "Click <b>Fight!</b> to enter tactical combat.",
|
||||
"highlightTarget": "FightButton",
|
||||
"persistHighlight": true,
|
||||
"highlightProvince": "Onmaa",
|
||||
"completionEvent": null
|
||||
@@ -247,10 +249,12 @@ Dialogue panel position defaults sensibly per scene (`top` during combat) and ca
|
||||
|
||||
Reset paths:
|
||||
- `TutorialState.Reset()` clears everything.
|
||||
- Entering a tutorial game (`GameType.Tutorial`) resets so it can be replayed.
|
||||
- Entering a tutorial game (`GameType.Tutorial`) resets `TutorialState`; dialogue replay is controlled separately by the per-game dialogue progress key.
|
||||
- Settings → "Reset Tutorials" calls `TutorialManager.ResetAllProgress()`, which also calls `DialogueManager.ResetCompletedScripts()`.
|
||||
|
||||
`DialogueManager` tracks completed scripts in memory only — they reset whenever `TutorialState` does.
|
||||
`TutorialDialogueProgressStore` separately stores completed dialogue script IDs per game under PlayerPrefs keys named `Eagle0_TutorialDialogueProgress_<gameId>`.
|
||||
|
||||
`DialogueManager` keeps the active game's completed script IDs in memory and saves them through `TutorialDialogueProgressStore`. `ResetCompletedScripts()` clears the current game's dialogue progress; `ClearCurrentGameProgress()` deletes the current game's persisted dialogue key.
|
||||
|
||||
---
|
||||
|
||||
@@ -276,7 +280,7 @@ You play **Sadar Rakon**, formerly Ikhaan Tarn's most trusted lieutenant. Tarn's
|
||||
|
||||
| # | Trigger | What happens |
|
||||
|---|---------|--------------|
|
||||
| 1 | `game_started` | **Opening monologue** (3 steps): Sadar's backstory, the Reclamation, last stand at Onmaa. Ends highlighting the **Battle!** button (`persistHighlight`) |
|
||||
| 1 | `game_started` | **Opening monologue** (3 steps): Sadar's backstory, the Reclamation, last stand at Onmaa. Ends highlighting the **Fight!** button (`FightButton`, `persistHighlight`) |
|
||||
| 2 | `tutorial_battle_ended` | **Aftermath**: Tarn has *vanished*; you've captured his lieutenants. Recruit them or hold them |
|
||||
| 3 | `tutorial_rebuild_support` | **Rebuild Onmaa** (3 steps): Marek frames it; **John Ranil** introduces *Improve* (engineer bonus); **Elena Fyar** introduces *Give Alms* (paladin bonus). Goal: 40 support by January |
|
||||
| 4 | `tutorial_loyalty_warning` | November-ish, low-loyalty hero may leave at year end → use *Give Gold* / *Feast* |
|
||||
@@ -288,6 +292,7 @@ You play **Sadar Rakon**, formerly Ikhaan Tarn's most trusted lieutenant. Tarn's
|
||||
| 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) |
|
||||
| 13 | `tutorial_hero_faction_appears` | John Ranil and Elena Fyar leave Sadar's service and reappear as independent tutorial factions |
|
||||
|
||||
### Tactical battle flow (`tutorial_battle.json`)
|
||||
|
||||
|
||||
+137
-153
@@ -1,44 +1,49 @@
|
||||
# URP Migration Plan
|
||||
# URP Migration Completion Notes
|
||||
|
||||
Last refreshed: 2026-06-12
|
||||
Last refreshed: 2026-06-14
|
||||
|
||||
## Why This Matters
|
||||
|
||||
Unity is steering projects away from the Built-in Render Pipeline (BiRP) toward the
|
||||
Universal Render Pipeline (URP). This project is still on BiRP:
|
||||
Universal Render Pipeline (URP). Eagle0 has completed the URP migration and now runs
|
||||
on URP:
|
||||
|
||||
- `ProjectSettings/GraphicsSettings.asset` has no custom render pipeline asset.
|
||||
- Every quality level in `ProjectSettings/QualitySettings.asset` has no custom render
|
||||
pipeline asset.
|
||||
- `ProjectSettings/GraphicsSettings.asset` points at
|
||||
`Assets/Settings/URP/Eagle0URPPipeline.asset`.
|
||||
- Every active quality level in `ProjectSettings/QualitySettings.asset` uses the
|
||||
same URP pipeline asset.
|
||||
- Legacy Post Processing Stack v2 has been removed from `Packages/manifest.json`;
|
||||
keep verifying it stays out with the migration inventory.
|
||||
|
||||
That means we should treat URP as a compatibility project, not a cosmetic cleanup.
|
||||
The goal is to migrate on our schedule, with visual baselines and small de-risking
|
||||
PRs, instead of discovering late that a Unity upgrade turns the game pink.
|
||||
The migration is no longer an active compatibility project. Keep this document as the
|
||||
completion record and maintenance checklist for future Unity, URP, shader, material,
|
||||
camera layering, or render-order changes.
|
||||
|
||||
## Current Recommendation
|
||||
|
||||
Do **not** switch main to URP yet.
|
||||
Keep URP enabled on main. Do not continue speculative URP cleanup. Use the baseline
|
||||
checklist and inventory tooling only when a future change touches shaders, materials,
|
||||
camera layering, render order, pipeline settings, or a visible rendering regression.
|
||||
|
||||
The next useful work is preparation:
|
||||
Maintenance rules:
|
||||
|
||||
1. Build a reliable visual baseline for the current BiRP project.
|
||||
2. Refresh the shader/material inventory with current scene-split assets.
|
||||
1. Keep the visual baseline current for Connection, Eagle, Shardok, and Settings.
|
||||
2. Refresh the shader/material inventory after meaningful asset or scene changes.
|
||||
3. Remove or isolate unused rendering dependencies.
|
||||
4. Create a disposable URP probe branch to measure actual breakage.
|
||||
5. Only then start a long-lived `urp-migration` branch.
|
||||
4. Keep the existing CI guardrails passing.
|
||||
5. Treat future pipeline experiments as separate branches with explicit findings.
|
||||
|
||||
This keeps normal gameplay work moving while making the eventual migration less
|
||||
mysterious.
|
||||
Known URP regressions found during playtesting have been fixed. Normal gameplay work
|
||||
can proceed; future rendering PRs should be driven by observable regressions or
|
||||
intentional rendering changes.
|
||||
|
||||
## What We Can Do Right Now
|
||||
## Maintenance Workflow
|
||||
|
||||
### 1. Create Visual Baselines
|
||||
|
||||
Before changing render pipelines, capture a small set of known-good views. These
|
||||
should be reproducible enough that a human can compare screenshots after each URP
|
||||
conversion step.
|
||||
Capture a small set of known-good views before any future change to URP settings,
|
||||
custom shaders, render ordering, or third-party visual materials. These should be
|
||||
reproducible enough that a human can compare screenshots after each rendering change.
|
||||
|
||||
Minimum baseline scenes and states:
|
||||
|
||||
@@ -52,19 +57,15 @@ Minimum baseline scenes and states:
|
||||
| Shardok | Terrain, grid lines, labels/icons, bridges, fires, overlays, command buttons |
|
||||
| Scene transitions | Connection -> Eagle -> Shardok -> Eagle |
|
||||
|
||||
Immediate deliverable: a short manual test checklist, or an editor-only capture tool
|
||||
that writes screenshots to an ignored directory. This should happen before any URP
|
||||
settings change.
|
||||
Keep the manual checklist and editor-only capture tool working so rendering PRs can
|
||||
leave screenshots in an ignored directory.
|
||||
|
||||
Use `docs/URP_VISUAL_BASELINE_CHECKLIST.md` for the current manual baseline capture
|
||||
set.
|
||||
|
||||
### 2. Refresh The Shader And Material Inventory
|
||||
### 2. Refresh The Shader And Material Inventory When Needed
|
||||
|
||||
The old inventory is useful, but scene split and recent visual fixes changed the
|
||||
project enough that we should regenerate it before migration work.
|
||||
|
||||
Immediate deliverable: a report containing:
|
||||
The inventory is useful before significant rendering work. It reports:
|
||||
|
||||
- all shaders under `Assets/`
|
||||
- all materials and the shader each material uses
|
||||
@@ -76,9 +77,6 @@ Immediate deliverable: a report containing:
|
||||
- `UnityUI.cginc`
|
||||
- tessellation
|
||||
|
||||
This report can be generated by an editor script or a small external scanner. The
|
||||
important bit is making it repeatable so we can measure progress during migration.
|
||||
|
||||
Run `Eagle0 > Rendering > Generate URP Migration Inventory` in the Unity editor to
|
||||
generate the current report. The tool writes markdown and CSV files under Unity
|
||||
`Temp/urp_migration/`, so running it should not dirty project assets.
|
||||
@@ -87,10 +85,12 @@ The generated CSVs include both the full material list and production material
|
||||
references, which helps separate game-path shader risk from unused package/demo
|
||||
assets.
|
||||
|
||||
Current inventory snapshot from June 12, 2026:
|
||||
`docs/URP_SHADER_DEBT_INVENTORY.md` records the current production-vs-package
|
||||
classification for remaining Built-in-style shader patterns.
|
||||
|
||||
- Render pipeline settings are still Built-in for both graphics default and active
|
||||
quality.
|
||||
Current inventory snapshot:
|
||||
|
||||
- Render pipeline settings are URP for both graphics default and active quality.
|
||||
- Post Processing Stack reference hits are still zero.
|
||||
- The all-material inventory is noisy: most material assets are `Standard`, and
|
||||
many Built-in-only shader hits are in third-party or package/demo assets.
|
||||
@@ -98,10 +98,14 @@ Current inventory snapshot from June 12, 2026:
|
||||
probe. Current production shader references are concentrated in TextMeshPro,
|
||||
Eagle custom map/effect shaders, Shardok grid/fire materials, and a small number
|
||||
of standard-material beast assets.
|
||||
- Highest production custom shader priorities remain `Eagle/ProvinceMap`,
|
||||
`Eagle/ProvinceWeatherMap`, `Eagle/ProvinceParticle`,
|
||||
`Eagle/ClipRectParticleUnlit`, `Shardok/Fire Overlay`, and the Shardok
|
||||
hex/overlay materials.
|
||||
- The project-owned Eagle and Shardok production custom shaders have been hardened
|
||||
for URP and passed Windows Unity builds after merge.
|
||||
- Project-owned runtime-created Eagle/Shardok materials now use serialized shader or
|
||||
material references instead of project shader-name fallbacks.
|
||||
- No project-owned production shader under `Assets/Eagle` or `Assets/Shardok`
|
||||
currently contains the scanned Built-in-era shader patterns.
|
||||
- Package/third-party shader debt remains documented but is not on the production
|
||||
path unless a future inventory run finds new references.
|
||||
|
||||
### 3. Verify Legacy Post Processing Usage
|
||||
|
||||
@@ -109,25 +113,22 @@ The project no longer depends on `com.unity.postprocessing`. The old references
|
||||
limited to an unused Orc/Ogre sample scene profile and scene-template type metadata,
|
||||
not production scenes.
|
||||
|
||||
Immediate deliverable: keep the project free of Post Processing Stack v2 references
|
||||
and use the inventory report to catch regressions.
|
||||
Keep the project free of Post Processing Stack v2 references and use the inventory
|
||||
report to catch regressions.
|
||||
|
||||
If references reappear:
|
||||
|
||||
- document the exact scene/camera/profile usage
|
||||
- either remove the stale dependency or plan the equivalent URP Volume settings
|
||||
|
||||
This is a good small PR because it either deletes a risky dependency now or precisely
|
||||
scopes the later migration work.
|
||||
|
||||
### 4. Keep Drought Effects URP-Friendly
|
||||
|
||||
The old `Eagle/HeatShimmer` shader used a Built-in-style `GrabPass`, but shimmer was
|
||||
already disabled in `DroughtEffect` because it did not work well with the UI Canvas.
|
||||
Drought now intentionally uses the animated sun overlay as its current visual signal.
|
||||
|
||||
Immediate deliverable: keep drought visuals free of `GrabPass` unless we intentionally
|
||||
add a new URP-friendly effect.
|
||||
Keep drought visuals free of `GrabPass` unless we intentionally add a new
|
||||
URP-friendly effect.
|
||||
|
||||
If stronger drought visuals are needed later, prefer:
|
||||
|
||||
@@ -135,120 +136,99 @@ If stronger drought visuals are needed later, prefer:
|
||||
- a small particle/sprite effect around the sun icon
|
||||
- a URP renderer feature only if screen-space distortion is truly worth the complexity
|
||||
|
||||
### 5. Check Third-Party URP Paths
|
||||
### 5. Leave Third-Party Shader Conversion Demand-Driven
|
||||
|
||||
The riskiest third-party shader set is Polytope Studio. The plan should not assume
|
||||
hand conversion until we know whether vendor URP variants exist or whether a current
|
||||
package update has already solved part of this.
|
||||
|
||||
Immediate deliverable: for each paid/third-party visual package, record:
|
||||
|
||||
- current installed version
|
||||
- whether a URP-compatible version exists
|
||||
- whether upgrading is license/account-accessible
|
||||
- whether the package is still used in production scenes
|
||||
The riskiest third-party shader set is Polytope Studio. Do not hand-convert package
|
||||
shader sets speculatively.
|
||||
|
||||
`docs/URP_THIRD_PARTY_ASSET_AUDIT.md` records the current production GUID-reference
|
||||
scan. The important result is that production assets use Polytope character prefabs
|
||||
for Eagle beast effects, but the Polytope environment/water assets that still contain
|
||||
`GrabPass` were not found on the production path.
|
||||
|
||||
Prioritize:
|
||||
If a future visible regression or inventory update makes third-party conversion
|
||||
necessary, record:
|
||||
|
||||
- Polytope Studio lowpoly character/environment shaders
|
||||
- GUI Pro Kit Fantasy RPG shaders
|
||||
- Modern UI Pack
|
||||
- RRFreelance Orc/Ogre shaders
|
||||
- DungeonMonsters2D sprite/animation shaders
|
||||
- current installed version
|
||||
- whether a URP-compatible version exists
|
||||
- whether upgrading is license/account-accessible
|
||||
- whether the package is still used in production scenes
|
||||
|
||||
### 6. Run A Disposable URP Probe
|
||||
### 6. Keep Probe Guidance For Future Pipeline Experiments
|
||||
|
||||
After the baseline and inventory exist, create a throwaway probe branch. This is not
|
||||
the migration branch.
|
||||
The original disposable probe has served its main purpose: URP is enabled on main and
|
||||
the biggest visual regressions were fixed in focused PRs. Keep the probe playbook as
|
||||
a template for future risky pipeline experiments, not as current migration guidance.
|
||||
|
||||
Use `docs/URP_PROBE_PLAYBOOK.md` for the branch structure, probe steps, expected
|
||||
risk areas, and findings template.
|
||||
Use `docs/URP_PROBE_PLAYBOOK.md` when we need to measure a future Unity or render
|
||||
pipeline change cheaply before committing to a mergeable branch.
|
||||
|
||||
Probe goals:
|
||||
|
||||
- install URP
|
||||
- create a minimal URP pipeline asset and renderer asset
|
||||
- switch only the probe branch to URP
|
||||
- run Unity's Render Pipeline Converter
|
||||
- record what turns pink, what compiles, and what still renders
|
||||
- throw away or reset the probe branch after documenting findings
|
||||
|
||||
The probe should answer unknowns cheaply. It should not try to produce a mergeable
|
||||
game.
|
||||
|
||||
## Current Known Risks
|
||||
## Known Maintenance Risks
|
||||
|
||||
| Risk | Why It Matters | Right-Now Action |
|
||||
|---|---|---|
|
||||
| `ProvinceMapShader` | Core Eagle map rendering: province IDs, borders, ocean animation, faction highlighting, clipping | Audit inputs/properties and baseline screenshots |
|
||||
| `UnityUI.cginc` usage | URP does not provide the same include path/semantics | Inventory exact shader uses; plan replacement clipping helpers |
|
||||
| `GrabPass` | No direct URP equivalent | Drought shimmer is removed/deferred; Polytope water shaders are present but not currently production-referenced |
|
||||
| Surface shaders | URP does not support Built-in surface shader generation | Inventory and convert or replace |
|
||||
| Polytope Studio shaders | Human-type Eagle beast effects use Polytope character prefabs with Built-in surface shaders | Check vendor URP availability before hand conversion; prioritize character materials over unused environment shaders |
|
||||
| Post Processing Stack v2 | Replaced by URP Volume system | Verify whether production scenes use it |
|
||||
| Scene lighting | URP lighting/shadow settings differ from BiRP | Baseline and rebake only after shader work is stable |
|
||||
| Render ordering | Recent Shardok fixes rely on careful labels/fire/bridge ordering | Include Shardok overlay cases in baseline |
|
||||
| `UnityUI.cginc` usage in package shaders | URP does not provide the same include path/semantics for hand-authored shaders | Leave package-managed TMP shaders alone unless a visible regression appears. |
|
||||
| `GrabPass` in unused package shaders | No direct URP equivalent | Ignore until production starts referencing those assets. |
|
||||
| Surface shaders in third-party packages | URP does not support Built-in surface shader generation | Covered by beast material compatibility on current production paths; convert only for a visible regression. |
|
||||
| Post Processing Stack v2 | Replaced by URP Volume system | Keep package and runtime/project-setting references from returning. |
|
||||
| Scene lighting | URP lighting/shadow settings differ from BiRP | Baseline and rebake only for visible regressions or intentional lighting work. |
|
||||
| Render ordering | Shardok and Eagle overlays depend on careful layering | Include affected overlays in baseline whenever render order changes. |
|
||||
|
||||
## Migration Strategy After Prep
|
||||
## Follow-Up Strategy
|
||||
|
||||
Once the immediate prep work is complete, start a long-lived `urp-migration` branch.
|
||||
Main should remain on BiRP until URP visual QA passes.
|
||||
Main now carries the completed URP switch. Future rendering work should be split by
|
||||
observable behavior:
|
||||
|
||||
- **Main branch**: continues shipping BiRP while gameplay work proceeds.
|
||||
- **URP branch**: accumulates pipeline, shader, material, lighting, and post-processing
|
||||
changes.
|
||||
- **Regular rebasing**: keep the URP branch current with main to avoid a painful final
|
||||
integration.
|
||||
- **Merge to main**: only after screenshots/manual QA show Connection, Eagle, and
|
||||
Shardok are visually acceptable.
|
||||
- **Compatibility fixes**: small PRs for specific pink/missing/dark assets.
|
||||
- **Render ordering fixes**: small PRs for Shardok/Eagle layer order regressions.
|
||||
- **Inventory/docs updates**: docs-only or editor-tool PRs that do not alter runtime
|
||||
visuals.
|
||||
- **Pipeline experiments**: isolated branches using the probe playbook, with findings
|
||||
copied into a mergeable follow-up.
|
||||
|
||||
This avoids the "everything is pink" problem of switching the pipeline before shaders
|
||||
are converted.
|
||||
This keeps the project from accumulating another broad, hard-to-revert rendering PR.
|
||||
|
||||
## Long-Term Phases
|
||||
## Completed Phases
|
||||
|
||||
### Phase 0: Preparation
|
||||
### Phase 0: Preparation And Switch
|
||||
|
||||
- Visual baseline checklist/screenshots
|
||||
- Repeatable shader/material inventory
|
||||
- Post Processing Stack usage decision
|
||||
- Drought visual decision
|
||||
- Third-party URP availability check
|
||||
- Disposable URP probe findings
|
||||
- Visual baseline checklist/screenshots: complete, keep current
|
||||
- Repeatable shader/material inventory: complete, keep current
|
||||
- Post Processing Stack usage decision: complete, keep removed
|
||||
- Drought visual decision: complete, use non-GrabPass visual signal
|
||||
- Third-party URP availability check: documented
|
||||
- Disposable URP probe findings: superseded by the merged URP switch
|
||||
- CI guardrails for pipeline settings, runtime shader lookup, project shader
|
||||
patterns, and beast-material compatibility: complete
|
||||
|
||||
### Phase 1: Pipeline Setup
|
||||
|
||||
- Install/configure URP on the long-lived branch.
|
||||
- Create URP Pipeline Asset and Renderer Asset.
|
||||
- Configure forward rendering, shadows, HDR, and renderer features.
|
||||
- Run Unity's Render Pipeline Converter.
|
||||
- Keep a record of every auto-converted material.
|
||||
- Install/configure URP: complete
|
||||
- Create URP Pipeline Asset and Renderer Asset: complete
|
||||
- Configure forward rendering, shadows, HDR, and renderer features: complete enough
|
||||
for current gameplay paths
|
||||
- Run Unity's Render Pipeline Converter: complete for the merged switch
|
||||
- Keep a record of every auto-converted material: use the inventory/audit tools
|
||||
|
||||
### Phase 2: Core Custom Shaders
|
||||
### Phase 2: Runtime Visual QA And Cleanup
|
||||
|
||||
Highest priority:
|
||||
Project-owned custom shader conversion, shader-name fallback cleanup, and deletion
|
||||
cleanup are complete. Current CI coverage keeps the URP pipeline assigned, required
|
||||
runtime shader references serialized, required player-runtime-only shaders included,
|
||||
project-owned Eagle/Shardok shaders free of scanned Built-in-only patterns, key
|
||||
Addressables labels covered, and major Eagle/Shardok visual wiring guarded.
|
||||
|
||||
- `ProvinceMapShader`
|
||||
- `ProvinceWeatherMapShader`
|
||||
- `ProvinceWeatherShader`
|
||||
- `ProvinceParticleShader`
|
||||
- `ClipRectParticleUnlit`
|
||||
- `maskShader`
|
||||
- `Hex Mesh Shader`
|
||||
Runtime visual QA was completed during playtesting. Known regressions from that pass
|
||||
were fixed before the migration was called done.
|
||||
|
||||
Conversion notes:
|
||||
Follow-up notes:
|
||||
|
||||
- Replace Built-in includes such as `UnityCG.cginc`.
|
||||
- Replace `UnityUI.cginc` clipping with URP-compatible code.
|
||||
- Preserve stencil operations, render queues, texture lookups, and property names.
|
||||
- Test map interactions, not just static rendering.
|
||||
- Use `docs/URP_VISUAL_BASELINE_CHECKLIST.md` for each future rendering follow-up.
|
||||
- Keep screenshots in an ignored local directory, not in git.
|
||||
- Treat package/third-party shader conversions as response work for visible
|
||||
regressions or new production references, not as speculative cleanup.
|
||||
|
||||
### Phase 3: GrabPass And Special Effects
|
||||
### Phase 3: GrabPass And Special Effects Maintenance
|
||||
|
||||
- Keep drought visuals on the current animated sun overlay unless a new URP-friendly
|
||||
effect is intentionally added.
|
||||
@@ -257,22 +237,23 @@ Conversion notes:
|
||||
- Avoid leaving screen-space effects until the end; they are likely to drive renderer
|
||||
feature requirements.
|
||||
|
||||
### Phase 4: Third-Party Shaders
|
||||
### Phase 4: Third-Party Shaders Maintenance
|
||||
|
||||
- Upgrade packages where vendor URP shaders exist.
|
||||
- Convert remaining PBR/toon/vegetation shaders manually.
|
||||
- Upgrade packages where vendor URP shaders exist and a production need appears.
|
||||
- Convert remaining PBR/toon/vegetation shaders manually only for production
|
||||
references or visible regressions.
|
||||
- Prefer replacement or deletion for unused demo-only assets.
|
||||
|
||||
### Phase 5: Materials, Lighting, And Post Processing
|
||||
### Phase 5: Materials, Lighting, And Post Processing Maintenance
|
||||
|
||||
- Batch-update remaining materials.
|
||||
- Replace Post Processing Stack v2 with URP Volumes if still needed.
|
||||
- Rebake lighting for production scenes.
|
||||
- Tune shadow and HDR settings.
|
||||
- Batch-update materials only for production paths or visible regressions.
|
||||
- Replace Post Processing Stack v2 with URP Volumes only if a production need returns.
|
||||
- Rebake lighting for production scenes when scene lighting is intentionally changed.
|
||||
- Tune shadow and HDR settings when visual QA or performance work justifies it.
|
||||
|
||||
### Phase 6: Validation
|
||||
### Phase 6: Validation Maintenance
|
||||
|
||||
Required visual QA:
|
||||
Required visual QA for future rendering changes:
|
||||
|
||||
- Connection lobby
|
||||
- Eagle map and command flows
|
||||
@@ -284,23 +265,26 @@ Required visual QA:
|
||||
|
||||
## Rough Effort
|
||||
|
||||
| Work | Best Case | Realistic | Worst Case |
|
||||
|---|---|---|---|
|
||||
| Preparation/probe | 2-4 days | 1 week | 2 weeks |
|
||||
| Core custom shaders | 1.5 weeks | 2-3 weeks | 4 weeks |
|
||||
| GrabPass/special effects | 2-4 days | 1-2 weeks | 3 weeks |
|
||||
| Third-party shaders/materials | 1.5 weeks | 2-3 weeks | 4 weeks |
|
||||
| Lighting/post-processing | 3-5 days | 1-2 weeks | 2 weeks |
|
||||
| QA/polish | 1 week | 2 weeks | 3 weeks |
|
||||
| **Total** | **5-7 weeks** | **8-12 weeks** | **16+ weeks** |
|
||||
|
||||
The biggest variable is still third-party shader conversion. Vendor URP packages or
|
||||
asset deletion can shrink this significantly.
|
||||
| Work | Status |
|
||||
|---|---|
|
||||
| Preparation/probe | Complete |
|
||||
| Pipeline setup | Complete |
|
||||
| Core custom shaders | Complete |
|
||||
| Runtime shader/build guardrails | Complete; project-owned runtime paths use serialized shader/material references |
|
||||
| GrabPass/special effects | Deferred until a production reference or new effect need appears |
|
||||
| Third-party shaders/materials | Covered by runtime compatibility and visual QA unless a visible regression appears |
|
||||
| Lighting/post-processing | No current Post Processing Stack dependency; rebake/tune only for visible regressions |
|
||||
| QA/polish | Completed for the migration; repeat for future rendering changes |
|
||||
|
||||
## Key Reminders
|
||||
|
||||
- Do not switch main to URP as a first step.
|
||||
- Do not leave `ProvinceMapShader`, `UnityUI.cginc`, or `GrabPass` work until the end.
|
||||
- URP is already enabled on main; keep follow-up work narrowly scoped.
|
||||
- Project-owned shader-name fallbacks should stay out of runtime-created materials;
|
||||
use serialized shader/material references instead.
|
||||
- `Shader.Find` remains acceptable for Unity/URP-provided shader names inside
|
||||
third-party compatibility shims, where there is no project asset to serialize.
|
||||
- Do not hand-convert package shaders unless runtime visual QA exposes a concrete
|
||||
regression or a new production reference appears.
|
||||
- Keep C# changes minimal; current rendering scripts mostly set material properties.
|
||||
- Preserve recent Shardok render-order behavior while changing pipelines.
|
||||
- Treat screenshots as migration tests; compilation alone is not meaningful here.
|
||||
|
||||
+18
-18
@@ -1,11 +1,11 @@
|
||||
# URP Probe Playbook
|
||||
|
||||
Last refreshed: 2026-06-12
|
||||
Last refreshed: 2026-06-14
|
||||
|
||||
This playbook is for the disposable URP probe described in
|
||||
`docs/URP_MIGRATION_PLAN.md`. The probe is not the migration branch and should not
|
||||
be merged as-is. Its job is to answer what breaks when Unity's URP tooling is
|
||||
allowed to touch the project.
|
||||
This playbook is historical reference for the disposable URP probe described in
|
||||
`docs/URP_MIGRATION_PLAN.md`. The URP migration is complete; keep this only as a
|
||||
template for future risky render-pipeline experiments. A probe branch is not a
|
||||
production branch and should not be merged as-is.
|
||||
|
||||
## Goals
|
||||
|
||||
@@ -43,12 +43,13 @@ experimental. Prefer opening the PR from `codex/urp-probe-findings`.
|
||||
1. Create `codex/urp-probe` from `origin/main`.
|
||||
2. Confirm `com.unity.render-pipelines.universal` is installed in
|
||||
`Packages/manifest.json`.
|
||||
3. Create a minimal URP Pipeline Asset and Renderer Asset under a clearly named
|
||||
probe folder, such as `Assets/Settings/URPProbe/`.
|
||||
4. Assign the URP pipeline asset in:
|
||||
3. If testing new pipeline settings, create alternate URP Pipeline/Renderer assets
|
||||
under a clearly named temporary folder.
|
||||
4. Assign the alternate URP pipeline asset in:
|
||||
- `ProjectSettings/GraphicsSettings.asset`
|
||||
- `ProjectSettings/QualitySettings.asset`
|
||||
5. Run Unity's Render Pipeline Converter for materials.
|
||||
5. Run Unity's Render Pipeline Converter only if the experiment intentionally tests
|
||||
material conversion.
|
||||
6. Enter Play Mode from `Assets/Scenes/Main.unity`.
|
||||
7. Exercise the baseline views:
|
||||
- Connection lobby
|
||||
@@ -66,15 +67,14 @@ experimental. Prefer opening the PR from `codex/urp-probe-findings`.
|
||||
|
||||
## Expected High-Risk Areas
|
||||
|
||||
- Eagle province map shaders:
|
||||
- `ProvinceMapShader`
|
||||
- `ProvinceWeatherMapShader`
|
||||
- `ProvinceWeatherShader`
|
||||
- `ProvinceParticleShader`
|
||||
- Shardok map and overlay shaders:
|
||||
- `Hex Mesh Shader`
|
||||
- `maskShader`
|
||||
- `ClipRectParticleUnlit`
|
||||
- Eagle map rendering:
|
||||
- province colors, borders, and ocean animation
|
||||
- weather overlays and province particles
|
||||
- beast effects on the map and in notifications
|
||||
- Shardok map rendering:
|
||||
- terrain, bridges, fire overlays, and grid overlays
|
||||
- foreground UI/effect overlays
|
||||
- unit labels/icons over fires, bridges, and one-shot animations
|
||||
- Third-party 3D materials:
|
||||
- Polytope Studio character prefabs used by Eagle human-type beast effects
|
||||
- RRFreelance Orc/Ogre materials used by `OgreEffect`
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
# URP Shader Debt Inventory
|
||||
|
||||
Last refreshed: 2026-06-14
|
||||
|
||||
This inventory classifies remaining Built-in Render Pipeline shader patterns after
|
||||
the completed URP switch. The project is running URP, but some package shader source
|
||||
files still use Built-in-era helpers or features. That is package/vendor debt, not
|
||||
evidence that the old pipeline is active and not an active migration blocker.
|
||||
|
||||
## Scan Method
|
||||
|
||||
The scan looked for these patterns in Unity shader-like assets:
|
||||
|
||||
- `GrabPass`
|
||||
- `#pragma surface`
|
||||
- `UnityCG.cginc`
|
||||
- `UnityUI.cginc`
|
||||
- `UNITY_MATRIX_`
|
||||
- `sampler2D _GrabTexture`
|
||||
- `tessell`
|
||||
- `Tessellation`
|
||||
|
||||
It then mapped every matching shader asset to its Unity GUID and scanned production
|
||||
serialized assets under:
|
||||
|
||||
- `Assets/Scenes/`
|
||||
- `Assets/Eagle/`
|
||||
- `Assets/Shardok/`
|
||||
- `Assets/ConnectionHandler/`
|
||||
- `Assets/UI/`
|
||||
- `Assets/common/`
|
||||
- `Assets/Tutorial/`
|
||||
- `Assets/Resources/`
|
||||
- `Assets/Materials/`
|
||||
|
||||
This catches serialized production references. It does not prove runtime code never
|
||||
loads an asset by path, but it separates game-path shader debt from package/demo
|
||||
noise well enough to decide whether future rendering work is needed.
|
||||
|
||||
## Summary
|
||||
|
||||
| Bucket | Shader Assets With Built-in Patterns | Serialized Production References |
|
||||
|---|---:|---:|
|
||||
| Project-owned production shaders | 0 | 0 referenced |
|
||||
| Project-owned runtime-loaded shaders | 0 | 0 serialized refs |
|
||||
| Project-owned unreferenced shaders | 0 | 0 referenced |
|
||||
| TextMeshPro package shaders | 17 | 1 referenced |
|
||||
| Third-party/package shaders | 25 | 0 referenced |
|
||||
|
||||
Project-owned runtime-created materials now use serialized shader or material
|
||||
references for Eagle weather, Eagle fireworks, Shardok foreground UI/effects, and
|
||||
Shardok foreground text. Remaining production `Shader.Find` usage is limited to
|
||||
Unity/URP-provided shader names inside `BeastMaterialCompatibility`, plus
|
||||
third-party package code outside our ownership.
|
||||
|
||||
## Completed Project-Owned Shader Hardening
|
||||
|
||||
These production shaders have been converted to URP-compatible HLSL and passed the
|
||||
Windows Unity build after merge.
|
||||
|
||||
| Shader | Production References | Completion Notes |
|
||||
|---|---|---|
|
||||
| `Assets/Eagle/Shaders/ProvinceMapShader.shader` | `Assets/Eagle/Materials/ProvinceMapMaterial.mat` | Converted with province color, border, ocean, and UI clipping behavior preserved. |
|
||||
| `Assets/Eagle/Shaders/ProvinceWeatherMapShader.shader` | `Assets/Eagle/Materials/ProvinceWeatherMapMaterial.mat` | Converted with weather lookup, overlay alignment, clipping, and initialized effect/tint values. |
|
||||
| `Assets/Eagle/Shaders/ProvinceWeatherShader.shader` | `Assets/Eagle/Weather/BlizzardEffect.mat`, `Assets/Eagle/Weather/DroughtEffect.mat`, `Assets/Eagle/Weather/FloodEffect.mat` | Converted for the live drought/flood/blizzard material path. |
|
||||
| `Assets/Eagle/Shaders/ProvinceParticleShader.shader` | `Assets/Eagle/Effects/ProvinceParticleMaterial.mat` | Converted for Eagle map province particles. Weather effect prefabs serialize the shared particle material instead of relying on shader-name fallback. |
|
||||
| `Assets/Eagle/Shaders/ClipRectParticleUnlit.shader` | `Assets/Eagle/Effects/Fireworks.prefab`, `Assets/Eagle/Effects/ParticleAlphaBlendMaterial.mat`, `Assets/Eagle/Effects/ParticleStandardUnlitMaterial.mat` | Converted for Eagle one-shot effects and clipped particle UI paths; Fireworks serializes this shader instead of using shader-name fallback. |
|
||||
| `Assets/Eagle/maskShader.shader` | Formerly `Assets/Eagle/Materials/map_color_nolabels.mat`, `Assets/Eagle/Materials/map_color_whitened.mat` | Deleted after confirming the serialized material path was stale. |
|
||||
| `Assets/Hex Mesh Shader.shader` | None | Deleted after confirming no serialized or code references to the asset, shader name, or GUID. |
|
||||
| `Assets/Shardok/ShardokFireOverlay.shader` | `Assets/Shardok/ShardokFireOverlay.mat` | Converted with the Shardok fire overlay layering path preserved. |
|
||||
| `Assets/Shardok/Shaders/ForegroundUIOverlay.shader` | Serialized Shardok foreground UI/effect shader references | URP pass is serialized on the Shardok scene/container and included in `ProjectSettings/GraphicsSettings.asset`; the stale Built-in fallback pass has been removed. |
|
||||
| `Assets/TextMesh Pro/Resources/Shaders/TMP_SDF Overlay.shader` | Serialized Shardok foreground text shader references | Serialized on the Shardok scene/container and included in `ProjectSettings/GraphicsSettings.asset` for required foreground text overlay material. |
|
||||
|
||||
## CI Guardrails
|
||||
|
||||
- `URPPipelineSettingsTests` keeps Graphics and Quality settings on the same URP
|
||||
pipeline asset.
|
||||
- `RuntimeShaderValidationTests` keeps required runtime shader assets present,
|
||||
serialized Shardok foreground shader references assigned, and player-runtime-only
|
||||
shaders listed in Always Included Shaders.
|
||||
- `WeatherEffectMaterialTests` keeps Eagle flood/blizzard prefabs wired to the
|
||||
shared URP province particle material and prevents shader-name fallbacks from
|
||||
returning to those runtime paths.
|
||||
- `EpidemicEffectMaterialTests` keeps Eagle epidemic particles wired to the shared
|
||||
clipped particle material.
|
||||
- `EagleMapMaterialWiringTests` keeps Eagle map/weather controllers wired to the
|
||||
project map materials and expected URP shaders.
|
||||
- `ProjectShaderPatternTests` fails if project-owned Eagle/Shardok shaders
|
||||
reintroduce the scanned Built-in-only patterns.
|
||||
- `RuntimeShaderFindUsageTests` fails if project-owned Eagle/Shardok runtime
|
||||
scripts reintroduce shader-name lookups outside documented compatibility shims.
|
||||
- `BeastMaterialCompatibilityTests` keeps runtime third-party beast material
|
||||
conversion covered.
|
||||
- `ProvinceBeastsControllerTests` keeps Eagle beast content names routed to
|
||||
specific animated effect prefabs instead of silently falling back to the generic
|
||||
circling-bird effect.
|
||||
- `FireworksEffectTests` keeps the fireworks prefab wired to the project-owned
|
||||
clipped particle shader and prevents shader-name fallback from returning.
|
||||
- `TutorialSpriteAssetTests` keeps tutorial TMP sprite assets and the shared dialogue
|
||||
panel instruction sprite asset renderable.
|
||||
- `ProvinceActionAnimatorSpriteTests` keeps Eagle action animation sprite fields
|
||||
assigned in the Eagle scene.
|
||||
- `PostProcessingStackRemovalTests` keeps legacy Post Processing Stack v2 package
|
||||
and runtime/project-setting references from returning.
|
||||
- `ProductionShaderReferenceTests` keeps shaders with Built-in-only patterns, and
|
||||
materials that use them, off the production serialized asset path except for the
|
||||
package-managed TextMeshPro shader family.
|
||||
|
||||
## Highest Priority: Referenced Project-Owned Shader Debt
|
||||
|
||||
No project-owned production shader under `Assets/Eagle` or `Assets/Shardok`
|
||||
currently contains the scanned Built-in-era shader patterns.
|
||||
|
||||
| Shader | Patterns | Production References | Recommended Action |
|
||||
|---|---|---|---|
|
||||
| None | None | None | No active migration work. Re-run inventory after future rendering changes. |
|
||||
|
||||
## Referenced Package Shader
|
||||
|
||||
| Shader | Patterns | Production References | Recommended Action |
|
||||
|---|---|---|---|
|
||||
| `Assets/TextMesh Pro/Resources/Shaders/TMP_Sprite.shader` | `UnityCG.cginc`, `UnityUI.cginc`, `UNITY_MATRIX_` | 16 tutorial sprite assets under `Assets/Tutorial/Sprite Assets/` | Leave alone unless Unity/URP surfaces a visible TextMeshPro sprite regression. Prefer package/vendor updates over hand-editing TMP package shaders. |
|
||||
|
||||
## Third-Party And Package Hits Without Serialized Production References
|
||||
|
||||
These files still contain Built-in-only shader patterns, but the current GUID scan
|
||||
did not find serialized production references. Treat them as package/demo cleanup,
|
||||
not as urgent game-path blockers.
|
||||
|
||||
| Package Area | Examples | Patterns | Recommended Action |
|
||||
|---|---|---|---|
|
||||
| Polytope Studio characters/weapons/props | Modular NPC, armor, weapons, props shaders | `#pragma surface`, `UnityCG.cginc` | Keep runtime beast material compatibility in place; only import/replace vendor URP materials if we intentionally remove the compatibility shim. |
|
||||
| Polytope Studio environment/water | Water, vegetation, rock shaders | `GrabPass`, `#pragma surface`, `UnityCG.cginc`, tessellation | Ignore unless production starts referencing these assets. Water shaders are the only remaining `GrabPass` hits. |
|
||||
| RRFreelance Orc/Ogre | Ogre, armor, weapon shaders | `#pragma surface`, `UnityCG.cginc` | Current production references are handled by compatibility/material fixes; do not hand-convert unless a visible ogre regression returns. |
|
||||
| TextMeshPro package shaders | TMP SDF/bitmap/surface shaders | `UnityCG.cginc`, `UnityUI.cginc`, `UNITY_MATRIX_`, surface shader variants | Prefer package updates. Avoid editing package shaders unless a concrete TMP visual regression exists. |
|
||||
| Clown.fat and TileableBridgePack | Toony shaders, bridge cutout shader | `#pragma surface`, `UnityCG.cginc`, `UNITY_MATRIX_` | Leave as low priority unless a production reference appears. |
|
||||
|
||||
## Maintenance Guidance
|
||||
|
||||
Runtime visual QA for the URP migration was completed during playtesting and known
|
||||
regressions from that pass were fixed. Leave unreferenced third-party/package shaders
|
||||
alone until they have a visible regression or production reference.
|
||||
|
||||
Future rendering PRs should use `docs/URP_VISUAL_BASELINE_CHECKLIST.md` and include
|
||||
affected screenshots in an ignored local directory, not in git.
|
||||
@@ -1,9 +1,9 @@
|
||||
# URP Third-Party Asset Audit
|
||||
|
||||
Last refreshed: 2026-06-11
|
||||
Last refreshed: 2026-06-14
|
||||
|
||||
This audit scopes third-party visual packages for the URP migration. It is based on
|
||||
a GUID reference scan from production Unity assets under:
|
||||
This audit scopes third-party visual packages after the completed URP migration. It
|
||||
is based on a GUID reference scan from production Unity assets under:
|
||||
|
||||
- `Assets/Scenes`
|
||||
- `Assets/Eagle`
|
||||
@@ -18,17 +18,17 @@ a GUID reference scan from production Unity assets under:
|
||||
The scan maps GUIDs from each package folder, then finds production scenes,
|
||||
prefabs, materials, controllers, assets, and scripts that reference those GUIDs.
|
||||
It does not prove runtime code never loads assets by path, but it catches the
|
||||
serialized references that matter most for a render-pipeline migration.
|
||||
serialized references that matter most for future render-pipeline maintenance.
|
||||
|
||||
## Summary
|
||||
|
||||
| Package | Production Use | URP Risk | Notes |
|
||||
|---|---:|---|---|
|
||||
| Polytope Studio | 20 refs / 18 assets | High | Used by Eagle human-type beast effects. Production refs are character prefabs, not environment/water assets. |
|
||||
| Polytope Studio | 20 refs / 18 assets | Medium | Used by Eagle human-type beast effects. Runtime beast-material compatibility converts active renderers to URP materials; production refs are character prefabs, not environment/water assets. |
|
||||
| GUI Pro Kit Fantasy RPG | 138 refs / 58 assets | Low | Production use is UI sprites/icons. No package shader dependency found in production refs. |
|
||||
| Modern UI Pack v4.2.0 | 23 refs / 6 assets | Low | Production use is UI textures/icons. |
|
||||
| RRFreelance Orc/Ogre | 2 refs / 2 assets | High | Ogre effect uses custom Built-in surface shaders. |
|
||||
| DungeonMonsters2D | 12 refs / 12 assets | Medium | 2D monster prefabs for Eagle beast effects. Expected to be mostly SpriteRenderer/Animator, but verify after URP probe. |
|
||||
| RRFreelance Orc/Ogre | 2 refs / 2 assets | Medium | Ogre effect uses custom Built-in surface shaders in source assets, but active renderers are covered by runtime beast-material compatibility under URP. |
|
||||
| DungeonMonsters2D | 12 refs / 12 assets | Medium | 2D monster prefabs for Eagle beast effects. Expected to be mostly SpriteRenderer/Animator; keep covered in beast-effect visual checks. |
|
||||
| Animal pack deluxe v2 | 7 refs / 7 assets | Medium | Elephant effect and controller refs. Verify lit-material conversion. |
|
||||
| Animal pack deluxe | 24 refs / 24 assets | Medium | Multiple Eagle animal beast effects. Verify lit-material conversion. |
|
||||
| AfricaAnimalsPackLowPoly V2 | 10 refs / 10 assets | Medium | Eagle beast effects and controllers. Verify lit-material conversion. |
|
||||
@@ -47,7 +47,7 @@ serialized references that matter most for a render-pipeline migration.
|
||||
| HoneyBadger | 2 refs / 1 asset | Medium | Honey badger effect mesh/animation. Verify material conversion. |
|
||||
| Raccoon | 2 refs / 1 asset | Medium | Raccoon effect mesh/animation. Verify material conversion. |
|
||||
|
||||
## Highest-Risk Findings
|
||||
## Runtime Beast Findings
|
||||
|
||||
### Polytope Studio
|
||||
|
||||
@@ -63,6 +63,10 @@ Production assets reference Polytope character prefabs for Eagle map effects:
|
||||
These prefabs ultimately use `PT_NPC_Mat`, which references
|
||||
`Polytope Studio/Lowpoly_Characters/Sources/Modular_NPC/Shaders/PT_Modular_NPC_Shader_PBR.shader`.
|
||||
That shader uses Built-in surface shader generation and is not URP-ready as-is.
|
||||
At runtime, Eagle beast effects pass their renderers through
|
||||
`BeastMaterialCompatibility.ConfigureRenderer`, which replaces non-URP materials
|
||||
with URP/Lit materials and bakes Polytope palette textures when needed. Treat this
|
||||
as covered by the compatibility path unless a future visual regression appears.
|
||||
|
||||
Polytope environment and water shaders still exist in the project and include
|
||||
Built-in-only features such as `GrabPass`, `UnityCG.cginc`, surface shaders, and
|
||||
@@ -78,26 +82,30 @@ Production assets reference the Orc/Ogre package from:
|
||||
- `Assets/Eagle/Effects/OgreMapAnims.controller`
|
||||
|
||||
The custom materials reference package shaders that use Built-in surface shader
|
||||
generation:
|
||||
generation in the source assets:
|
||||
|
||||
- `RRFreelance-Characters/Orc-Ogre/Shaders/OgreShader.shader`
|
||||
- `RRFreelance-Characters/Orc-Ogre/Shaders/ArmorShader2sided.shader`
|
||||
- `RRFreelance-Characters/Orc-Ogre/Shaders/WeaponShader.shader`
|
||||
|
||||
This should be converted, replaced with URP/Lit-compatible materials, or swapped
|
||||
for standard materials during the URP migration.
|
||||
At runtime, the Eagle monster-effect path also uses
|
||||
`BeastMaterialCompatibility.ConfigureRenderer`, so these renderers should be
|
||||
left on the compatibility path unless the Ogre effect becomes pink, dark, or
|
||||
otherwise visibly broken.
|
||||
|
||||
## Recommended URP Order
|
||||
## Maintenance Guidance
|
||||
|
||||
1. During the disposable URP probe, check whether Unity's converter can upgrade the
|
||||
Polytope character materials and RRFreelance Ogre materials cleanly.
|
||||
2. If the converter leaves them pink or visually broken, replace the affected
|
||||
The URP migration is complete; do not convert third-party packages speculatively.
|
||||
|
||||
1. Keep the runtime beast-effect compatibility path covered by existing tests and
|
||||
future visual checks when beast rendering changes.
|
||||
2. If compatibility leaves a beast pink, dark, or visibly broken, replace the affected
|
||||
materials with URP/Lit materials before touching unused demo-only shader packs.
|
||||
3. Leave Polytope water/vegetation/environment conversion until a production
|
||||
reference appears. They are noisy in the shader inventory but not currently on
|
||||
the game path.
|
||||
4. Include Eagle beast effects in the visual baseline and URP probe. They cover
|
||||
most third-party 3D package risk in one place.
|
||||
4. Include Eagle beast effects in the visual baseline. They cover most third-party
|
||||
3D package risk in one place.
|
||||
5. Treat UI sprite packs as low pipeline risk. Their risk is texture import/render
|
||||
ordering, not shader conversion.
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
# URP Visual Baseline Checklist
|
||||
|
||||
Use this checklist before any URP probe or migration branch changes render-pipeline
|
||||
settings. The goal is not exhaustive gameplay QA; it is a stable set of visual
|
||||
states that can be compared by eye after shader, material, lighting, or render-order
|
||||
changes.
|
||||
Use this checklist before any future branch changes render-pipeline settings, custom
|
||||
shaders, materials, lighting, camera layering, or render order. The URP migration is
|
||||
complete; this checklist is now a maintenance tool. The goal is not exhaustive
|
||||
gameplay QA, but a stable set of visual states that can be compared by eye after
|
||||
rendering changes.
|
||||
|
||||
Save screenshots under:
|
||||
|
||||
@@ -21,7 +22,7 @@ write a timestamped Game view capture to that directory.
|
||||
- Use the same Game view resolution for every capture in a comparison set.
|
||||
- Capture the full Game view, not cropped panels.
|
||||
- Name files with a stable prefix and short state name, for example
|
||||
`birp_01_connection_accounts.png` and `urp_01_connection_accounts.png`.
|
||||
`main_01_connection_accounts.png` and `branch_01_connection_accounts.png`.
|
||||
- If a view depends on server data or an account, write a short note next to the
|
||||
screenshot naming the game/account/state used.
|
||||
|
||||
@@ -47,7 +48,7 @@ write a timestamped Game view capture to that directory.
|
||||
|
||||
## Acceptance Notes
|
||||
|
||||
For each URP probe, record:
|
||||
For each rendering follow-up, record:
|
||||
|
||||
- Unity version and branch name.
|
||||
- Screenshot directory.
|
||||
@@ -56,5 +57,5 @@ For each URP probe, record:
|
||||
- Any render-order differences, especially Shardok overlays and Eagle beast effects.
|
||||
- Whether the difference is acceptable, needs shader work, or needs asset/material work.
|
||||
|
||||
Do not merge a URP branch to main until the Connection, Eagle, and Shardok baseline
|
||||
captures are visually acceptable.
|
||||
Do not merge a rendering branch until the affected Connection, Eagle, Shardok, or
|
||||
Settings baseline captures are visually acceptable.
|
||||
|
||||
@@ -2,7 +2,7 @@ module github.com/nolen777/eagle0
|
||||
|
||||
go 1.25.0
|
||||
|
||||
toolchain go1.25.11
|
||||
toolchain go1.26.4
|
||||
|
||||
require (
|
||||
github.com/aws/aws-sdk-go-v2 v1.42.0
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
|
||||
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
|
||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||
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/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=
|
||||
@@ -74,32 +80,86 @@ 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/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=
|
||||
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4=
|
||||
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
|
||||
github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A=
|
||||
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
|
||||
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang/glog v1.2.5 h1:DrW6hGnjIhtvhOIiAKT6Psh/Kd/ldepEa81DKeiRJ5I=
|
||||
github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
||||
github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=
|
||||
github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=
|
||||
github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6 h1:VQpB2SpK88C6B5lPHTuSZKb2Qee1QWwiFlC5CKY4AW0=
|
||||
github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6/go.mod h1:yE65LFCeWf4kyWD5re+h4XNvOHJEXOCOuJZ4v8l5sgk=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
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/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=
|
||||
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
||||
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
||||
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
||||
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
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/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=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
||||
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
||||
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260610212136-7ab31c22f7ad h1:3iLyITS/sySRwbUKoC7ogfj2Yr1Cjs0pfaRKj5U5HEw=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260610212136-7ab31c22f7ad/go.mod h1:KdNqO+rCIWgFumrNBSEDlDNrkrQnpkax7Tv1WxNY8V4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
|
||||
|
||||
+18
-18
@@ -13,7 +13,7 @@
|
||||
"com.google.code.gson:gson": 579810017,
|
||||
"com.google.errorprone:error_prone_annotations": 950460708,
|
||||
"com.google.guava:failureaccess": 1582410452,
|
||||
"com.google.guava:guava": 582798237,
|
||||
"com.google.guava:guava": 1001533771,
|
||||
"com.google.j2objc:j2objc-annotations": 2003271689,
|
||||
"com.google.re2j:re2j": 1525409503,
|
||||
"com.google.s2a.proto.v2:s2a-proto": 898932763,
|
||||
@@ -85,22 +85,22 @@
|
||||
"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": -1424193143,
|
||||
"com.google.auth:google-auth-library-oauth2-http": -1252786723,
|
||||
"com.google.auto.value:auto-value": -1399392164,
|
||||
"com.google.auto.value:auto-value-annotations": 1092025564,
|
||||
"com.google.code.findbugs:jsr305": -1038659426,
|
||||
"com.google.code.gson:gson": 592204959,
|
||||
"com.google.errorprone:error_prone_annotations": 1213248887,
|
||||
"com.google.guava:failureaccess": -56291903,
|
||||
"com.google.guava:guava": 1970967483,
|
||||
"com.google.guava:guava": 2119270017,
|
||||
"com.google.guava:listenablefuture": 1588902908,
|
||||
"com.google.http-client:google-http-client": -136457690,
|
||||
"com.google.http-client:google-http-client-gson": -1055701869,
|
||||
"com.google.http-client:google-http-client": 1756875461,
|
||||
"com.google.http-client:google-http-client-gson": 1267916085,
|
||||
"com.google.j2objc:j2objc-annotations": -2074422376,
|
||||
"com.google.protobuf:protobuf-java": -1065767575,
|
||||
"com.google.re2j:re2j": -461460138,
|
||||
"com.google.s2a.proto.v2:s2a-proto": 1108303781,
|
||||
"com.google.truth:truth": 1427899897,
|
||||
"com.google.s2a.proto.v2:s2a-proto": 833783597,
|
||||
"com.google.truth:truth": 1846223773,
|
||||
"com.lihaoyi:fansi_2.13": -224971606,
|
||||
"com.lihaoyi:sourcecode_2.13": 2048976,
|
||||
"com.nimbusds:nimbus-jose-jwt": -1763138105,
|
||||
@@ -114,7 +114,7 @@
|
||||
"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": 580120277,
|
||||
"com.thesamet.scalapb:scalapb-runtime-grpc_3": 97373115,
|
||||
"com.thesamet.scalapb:scalapb-runtime_3": 818778125,
|
||||
"com.typesafe:config": -1791448454,
|
||||
"commons-codec:commons-codec": 1377992676,
|
||||
@@ -133,11 +133,11 @@
|
||||
"io.github.alexarchambault:concurrent-reference-hash-map": -1702289188,
|
||||
"io.github.alexarchambault:is-terminal": 357931298,
|
||||
"io.github.java-diff-utils:java-diff-utils": 1636095913,
|
||||
"io.grpc:grpc-api": -1732683013,
|
||||
"io.grpc:grpc-context": -862685412,
|
||||
"io.grpc:grpc-protobuf": -793298635,
|
||||
"io.grpc:grpc-protobuf-lite": -766127633,
|
||||
"io.grpc:grpc-stub": -1619717353,
|
||||
"io.grpc:grpc-api": -947568169,
|
||||
"io.grpc:grpc-context": -22548221,
|
||||
"io.grpc:grpc-protobuf": -1259537716,
|
||||
"io.grpc:grpc-protobuf-lite": -1244666196,
|
||||
"io.grpc:grpc-stub": -1164705340,
|
||||
"io.netty:netty-buffer": -870582473,
|
||||
"io.netty:netty-codec": -1546709832,
|
||||
"io.netty:netty-codec-http": 1825361781,
|
||||
@@ -158,9 +158,9 @@
|
||||
"io.netty:netty-transport-classes-epoll": 704589083,
|
||||
"io.netty:netty-transport-native-epoll:jar:linux-x86_64": -2050990961,
|
||||
"io.netty:netty-transport-native-unix-common": -2090924105,
|
||||
"io.opencensus:opencensus-api": 709780868,
|
||||
"io.opencensus:opencensus-contrib-grpc-metrics": 576725745,
|
||||
"io.opencensus:opencensus-contrib-http-util": -1406323089,
|
||||
"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,
|
||||
"javax.activation:javax.activation-api": -217704356,
|
||||
@@ -402,9 +402,9 @@
|
||||
},
|
||||
"com.google.guava:guava": {
|
||||
"shasums": {
|
||||
"jar": "9532ca9ba8aceb4cd2067171f981c799988b7f5844e3e274811a3bb757034bc4"
|
||||
"jar": "dc573e1fca4fd5454f4a5fd3d7da2df03002876a4175bafc14a95980dd7713b3"
|
||||
},
|
||||
"version": "33.6.0-android"
|
||||
"version": "33.6.0-jre"
|
||||
},
|
||||
"com.google.guava:listenablefuture": {
|
||||
"shasums": {
|
||||
|
||||
@@ -14,6 +14,22 @@
|
||||
"labels": [
|
||||
"dependencies"
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
"description": "Bazel module artifact updates run in GitHub Actions so hosted Renovate does not need unsafe bazel mod deps execution",
|
||||
"matchManagers": [
|
||||
"bazel-module"
|
||||
],
|
||||
"skipArtifactsUpdate": true
|
||||
},
|
||||
{
|
||||
"description": "Go packages import Bazel-generated protobuf packages, so Renovate's generic Go artifact updater cannot run ./...",
|
||||
"matchManagers": [
|
||||
"gomod"
|
||||
],
|
||||
"skipArtifactsUpdate": true
|
||||
}
|
||||
],
|
||||
"lockFileMaintenance": {
|
||||
"enabled": true,
|
||||
"schedule": [
|
||||
|
||||
+15
-53
@@ -1,68 +1,30 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euxo pipefail
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
UNITY_ROOT="$REPO_ROOT/src/main/csharp/net/eagle0/clients/unity/eagle0"
|
||||
OUTPUT_DIR="$UNITY_ROOT/Assets/GeneratedProtos"
|
||||
PLUGIN_DIR="$UNITY_ROOT/Assets/Plugins/Eagle0Protos"
|
||||
GENERATED_SOURCES_DIR="$UNITY_ROOT/Assets/GeneratedProtos"
|
||||
PROTO_DLL_TARGET="//src/main/csharp/net/eagle0/clients/unity/eagle0:eagle0_protos"
|
||||
PROTO_DLL_OUTPUT="$REPO_ROOT/bazel-bin/src/main/csharp/net/eagle0/clients/unity/eagle0/eagle0_protos/netstandard2.1"
|
||||
|
||||
# All C# protobuf generation targets
|
||||
TARGETS=(
|
||||
"//src/main/protobuf/net/eagle0/common:common_csharp_proto_srcs"
|
||||
"//src/main/protobuf/net/eagle0/shardok/storage:storage_csharp_proto_srcs"
|
||||
"//src/main/protobuf/net/eagle0/shardok/common:common_csharp_proto_srcs"
|
||||
"//src/main/protobuf/net/eagle0/shardok/api:api_csharp_proto_srcs"
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:common_csharp_proto_srcs"
|
||||
"//src/main/protobuf/net/eagle0/eagle/views:views_csharp_proto_srcs"
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:api_csharp_proto_srcs"
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:api_csharp_grpc_srcs"
|
||||
"//src/main/protobuf/net/eagle0/eagle/api/command/util:util_csharp_proto_srcs"
|
||||
)
|
||||
/bin/echo "Building C# protobuf DLL..."
|
||||
bazel build "$PROTO_DLL_TARGET"
|
||||
|
||||
# Subdirectory for each target (avoids filename collisions across packages)
|
||||
SUBDIRS=(
|
||||
"net/eagle0/common"
|
||||
"net/eagle0/shardok/storage"
|
||||
"net/eagle0/shardok/common"
|
||||
"net/eagle0/shardok/api"
|
||||
"net/eagle0/eagle/common"
|
||||
"net/eagle0/eagle/views"
|
||||
"net/eagle0/eagle/api"
|
||||
"net/eagle0/eagle/api"
|
||||
"net/eagle0/eagle/api/command/util"
|
||||
)
|
||||
/bin/echo "Syncing generated protobuf DLL to $PLUGIN_DIR..."
|
||||
|
||||
/bin/echo "Building C# protobuf sources..."
|
||||
bazel build "${TARGETS[@]}"
|
||||
|
||||
/bin/echo "Syncing generated .cs files to $OUTPUT_DIR..."
|
||||
|
||||
# Stage new files in a temp directory, then rsync to preserve timestamps
|
||||
# on unchanged files. This avoids triggering Unity reimport when protos
|
||||
# haven't changed, saving ~7 minutes of script recompilation.
|
||||
STAGING_DIR=$(mktemp -d)
|
||||
trap "rm -rf '$STAGING_DIR'" EXIT
|
||||
|
||||
for i in "${!TARGETS[@]}"; do
|
||||
target="${TARGETS[$i]}"
|
||||
subdir="${SUBDIRS[$i]}"
|
||||
|
||||
# Convert target label to bazel-bin path
|
||||
# //src/main/protobuf/net/eagle0/common:common_csharp_proto_srcs
|
||||
# -> bazel-bin/src/main/protobuf/net/eagle0/common/common_csharp_proto_srcs
|
||||
target_path="${target#//}"
|
||||
pkg="${target_path%%:*}"
|
||||
name="${target_path##*:}"
|
||||
bin_dir="$REPO_ROOT/bazel-bin/$pkg/$name"
|
||||
|
||||
dest_dir="$STAGING_DIR/$subdir"
|
||||
mkdir -p "$dest_dir"
|
||||
find "$bin_dir" -name "*.cs" -exec cp {} "$dest_dir/" \;
|
||||
done
|
||||
cp "$PROTO_DLL_OUTPUT/Eagle0Protos.dll" "$STAGING_DIR/"
|
||||
cp "$PROTO_DLL_OUTPUT/Eagle0Protos.xml" "$STAGING_DIR/"
|
||||
|
||||
# Sync only changed files and delete removed ones; --checksum compares
|
||||
# content not timestamps so unchanged files keep their original mtime.
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
rsync -rc --delete "$STAGING_DIR/" "$OUTPUT_DIR/"
|
||||
mkdir -p "$PLUGIN_DIR"
|
||||
rsync -rc --delete --exclude "*.meta" "$STAGING_DIR/" "$PLUGIN_DIR/"
|
||||
|
||||
/bin/echo "Done. Generated C# proto sources in $OUTPUT_DIR"
|
||||
rm -rf "$GENERATED_SOURCES_DIR"
|
||||
|
||||
/bin/echo "Done. Generated C# proto DLL in $PLUGIN_DIR"
|
||||
|
||||
Executable
+17
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
bazel_cmd=(bazel)
|
||||
if [ -n "${BAZEL_OUTPUT_USER_ROOT:-}" ]; then
|
||||
bazel_cmd+=("--output_user_root=${BAZEL_OUTPUT_USER_ROOT}")
|
||||
fi
|
||||
if [ -n "${BAZEL_OUTPUT_BASE:-}" ]; then
|
||||
bazel_cmd+=("--output_base=${BAZEL_OUTPUT_BASE}")
|
||||
fi
|
||||
|
||||
"${bazel_cmd[@]}" build \
|
||||
--platforms=//:linux_arm64 \
|
||||
--extra_toolchains=@llvm_toolchain_linux_arm64//:cc-toolchain-aarch64-linux \
|
||||
"$@" \
|
||||
//src/main/cpp/net/eagle0/shardok:shardok-server
|
||||
@@ -0,0 +1,352 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import random
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
|
||||
API_KEY_FILE = Path("src/main/scala/net/eagle0/common/llm_integration/api_keys.txt")
|
||||
IMAGE_ENDPOINT = "https://api.openai.com/v1/images/generations"
|
||||
|
||||
APPEARANCE_PROFILES = [
|
||||
"West African features, very dark brown skin",
|
||||
"East African features, deep brown skin",
|
||||
"North African features, olive-brown skin",
|
||||
"Arabian Peninsula features, warm medium-brown skin",
|
||||
"Persian features, olive skin",
|
||||
"Turkic Central Asian features, tan skin",
|
||||
"Mongolian features, golden-tan skin",
|
||||
"North Indian features, medium-brown skin",
|
||||
"South Indian features, dark brown skin",
|
||||
"Bengali features, warm brown skin",
|
||||
"Southeast Asian features, medium tan skin",
|
||||
"Vietnamese features, light golden skin",
|
||||
"Chinese features, light olive skin",
|
||||
"Korean features, fair skin",
|
||||
"Japanese features, fair-to-light olive skin",
|
||||
"Indigenous Andean features, copper-brown skin",
|
||||
"Mesoamerican features, medium brown skin",
|
||||
"Native North American features, warm brown skin",
|
||||
"Maori or Polynesian features, medium brown skin",
|
||||
"Aboriginal Australian features, dark brown skin",
|
||||
"Afro-Caribbean features, dark brown skin",
|
||||
"Afro-Latin features, medium-dark brown skin",
|
||||
"Brazilian mixed-heritage features, medium brown skin",
|
||||
"Mexican mestizo features, tan skin",
|
||||
"Mediterranean European features, olive skin",
|
||||
"Iberian features, light olive skin",
|
||||
"Italian features, olive skin",
|
||||
"Greek features, olive skin",
|
||||
"French features, light skin",
|
||||
"Irish features, fair skin with freckles",
|
||||
"Scottish features, ruddy fair skin",
|
||||
"Nordic features, very fair skin",
|
||||
"Slavic features, fair-to-light olive skin",
|
||||
"Ashkenazi Jewish features, light olive skin",
|
||||
"Armenian features, olive skin",
|
||||
"Ethiopian highland features, medium-dark brown skin",
|
||||
"Moroccan Amazigh features, tan olive skin",
|
||||
"Egyptian features, warm tan skin",
|
||||
"Filipino features, medium golden-brown skin",
|
||||
"Pacific Islander features, warm brown skin",
|
||||
]
|
||||
|
||||
AGE_PROFILES = [
|
||||
"young adult",
|
||||
"adult",
|
||||
"seasoned middle-aged",
|
||||
"older veteran",
|
||||
]
|
||||
|
||||
FACE_PROFILES = [
|
||||
"broad, sturdy face",
|
||||
"narrow, severe face",
|
||||
"round, watchful face",
|
||||
"angular, weathered face",
|
||||
"scarred but composed face",
|
||||
"plain, practical face",
|
||||
"commanding, heavy-browed face",
|
||||
"tired but alert face",
|
||||
]
|
||||
|
||||
HAIR_PROFILES = [
|
||||
"cropped dark hair",
|
||||
"close-cropped gray hair",
|
||||
"braided black hair",
|
||||
"shaved head",
|
||||
"curly dark hair pulled back",
|
||||
"straight black hair tied back",
|
||||
"auburn hair streaked with gray",
|
||||
"white hair cut short",
|
||||
"dark beard and close hair",
|
||||
"neat graying beard",
|
||||
"coiled natural hair",
|
||||
"long dark hair bound under a cap",
|
||||
]
|
||||
|
||||
|
||||
def api_key():
|
||||
env_value = os.environ.get("OPENAI_API_KEY")
|
||||
if env_value:
|
||||
return env_value
|
||||
|
||||
if API_KEY_FILE.exists():
|
||||
for line in API_KEY_FILE.read_text().splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
key, value = line.split("=", 1)
|
||||
if key.strip() == "openai_api_key":
|
||||
value = value.strip()
|
||||
if value and not value.startswith("your_"):
|
||||
return value
|
||||
|
||||
raise RuntimeError(
|
||||
"OpenAI API key not found. Set OPENAI_API_KEY or add openai_api_key to "
|
||||
f"{API_KEY_FILE}."
|
||||
)
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate Warden hero headshots and update generated_heroes.tsv image paths."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--hero-tsv",
|
||||
default="src/main/resources/net/eagle0/eagle/generated_heroes.tsv",
|
||||
help="Generated heroes TSV to read and update.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--base-image-dir",
|
||||
default=str(Path.home() / "Documents/headshots"),
|
||||
help="Local headshot root that sync_headshots.sh uploads to S3.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--model",
|
||||
default="gpt-image-2",
|
||||
help="OpenAI image generation model.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--quality",
|
||||
default="low",
|
||||
choices=("low", "medium", "high", "auto"),
|
||||
help="Image generation quality.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--size",
|
||||
default="1024x1024",
|
||||
help="Generated image size.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--limit",
|
||||
type=int,
|
||||
default=0,
|
||||
help="Generate at most this many missing images; 0 means all missing images.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="Print planned paths without calling the API or writing files.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--overwrite",
|
||||
action="store_true",
|
||||
help="Regenerate images even if the target PNG already exists.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--concurrency",
|
||||
type=int,
|
||||
default=4,
|
||||
help="Number of parallel image generation requests.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def read_rows(path):
|
||||
lines = path.read_text().splitlines()
|
||||
fieldnames = lines[0].split("\t")
|
||||
rows = []
|
||||
for line in lines[1:]:
|
||||
fields = line.split("\t")
|
||||
row = dict(zip(fieldnames, fields))
|
||||
row["_fields"] = fields
|
||||
rows.append(row)
|
||||
return fieldnames, rows
|
||||
|
||||
|
||||
def write_rows(path, fieldnames, rows):
|
||||
image_path_index = fieldnames.index("image_path")
|
||||
lines = ["\t".join(fieldnames)]
|
||||
for row in rows:
|
||||
fields = list(row["_fields"])
|
||||
fields[image_path_index] = row["image_path"]
|
||||
lines.append("\t".join(fields))
|
||||
path.write_text("\n".join(lines) + "\n")
|
||||
|
||||
|
||||
def image_gender(row, warden_index):
|
||||
gender = row["gender"]
|
||||
if gender in ("male", "female"):
|
||||
return gender
|
||||
|
||||
digest = hashlib.sha256(row["name"].encode("utf-8")).digest()
|
||||
return "male" if (digest[0] + warden_index) % 2 == 0 else "female"
|
||||
|
||||
|
||||
def image_filename(row, warden_index):
|
||||
digest = hashlib.sha256(row["name"].encode("utf-8")).hexdigest()[:16]
|
||||
return f"{warden_index:04d}_{digest}.png"
|
||||
|
||||
|
||||
def choose_profile(items, row, warden_index, salt):
|
||||
digest = hashlib.sha256(f"{salt}:{warden_index}:{row['name']}".encode("utf-8")).digest()
|
||||
return items[int.from_bytes(digest[:4], "big") % len(items)]
|
||||
|
||||
|
||||
def prompt_for(row, warden_index):
|
||||
prime_notes = [
|
||||
f"strength {row['strength']}",
|
||||
f"agility {row['agility']}",
|
||||
f"constitution {row['constitution']}",
|
||||
f"wisdom {row['wisdom']}",
|
||||
f"charisma {row['charisma']}",
|
||||
]
|
||||
appearance = choose_profile(APPEARANCE_PROFILES, row, warden_index, "appearance")
|
||||
age = choose_profile(AGE_PROFILES, row, warden_index, "age")
|
||||
face = choose_profile(FACE_PROFILES, row, warden_index, "face")
|
||||
hair = choose_profile(HAIR_PROFILES, row, warden_index, "hair")
|
||||
return (
|
||||
"Fantasy strategy game hero portrait, square headshot composition. "
|
||||
f"Create a distinctive medieval Warden named {row['name']}. "
|
||||
f"Gender presentation: {row['gender']}. "
|
||||
f"Appearance: {age}, {appearance}, {face}, {hair}. "
|
||||
f"Personality traits: {row['personality']}. "
|
||||
"The character is a jailer, keeper of prisoners, and battlefield custodian: "
|
||||
"stern keys, austere authority, prison-watch uniform, no modern clothing. "
|
||||
"Show a resilient, high-constitution figure with weathered endurance and moral judgment. "
|
||||
f"Stat cues: {', '.join(prime_notes)}. "
|
||||
"Painted realistic fantasy style, expressive face, shoulders visible, neutral background, "
|
||||
"soft dramatic lighting, no text, no logo, no frame, no watermark."
|
||||
)
|
||||
|
||||
|
||||
def request_image(key, model, quality, size, prompt):
|
||||
body = {
|
||||
"model": model,
|
||||
"prompt": prompt,
|
||||
"size": size,
|
||||
"quality": quality,
|
||||
"n": 1,
|
||||
"output_format": "png",
|
||||
}
|
||||
request = urllib.request.Request(
|
||||
IMAGE_ENDPOINT,
|
||||
data=json.dumps(body).encode("utf-8"),
|
||||
headers={
|
||||
"Authorization": f"Bearer {key}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method="POST",
|
||||
)
|
||||
with urllib.request.urlopen(request, timeout=300) as response:
|
||||
response_body = json.loads(response.read().decode("utf-8"))
|
||||
|
||||
data = response_body["data"][0]
|
||||
if "b64_json" in data:
|
||||
return base64.b64decode(data["b64_json"])
|
||||
|
||||
if "url" in data:
|
||||
with urllib.request.urlopen(data["url"], timeout=300) as response:
|
||||
return response.read()
|
||||
|
||||
raise RuntimeError("Image response did not include b64_json or url")
|
||||
|
||||
|
||||
def generate_with_retries(key, args, prompt, attempts=5):
|
||||
for attempt in range(1, attempts + 1):
|
||||
try:
|
||||
return request_image(key, args.model, args.quality, args.size, prompt)
|
||||
except urllib.error.HTTPError as error:
|
||||
body = error.read().decode("utf-8", errors="replace")
|
||||
retryable = error.code in (408, 409, 429, 500, 502, 503, 504)
|
||||
if not retryable or attempt == attempts:
|
||||
raise RuntimeError(f"OpenAI image request failed: {error.code} {body}") from error
|
||||
except (TimeoutError, urllib.error.URLError):
|
||||
if attempt == attempts:
|
||||
raise
|
||||
|
||||
time.sleep(min(60, 2**attempt) + random.random())
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
hero_tsv = Path(args.hero_tsv)
|
||||
base_image_dir = Path(args.base_image_dir).expanduser()
|
||||
fieldnames, rows = read_rows(hero_tsv)
|
||||
|
||||
key = None if args.dry_run else api_key()
|
||||
warden_index = 0
|
||||
generation_tasks = []
|
||||
|
||||
for row in rows:
|
||||
if row["profession"] != "warden":
|
||||
continue
|
||||
|
||||
warden_index += 1
|
||||
gender = image_gender(row, warden_index)
|
||||
rel_path = f"warden/{gender}/{image_filename(row, warden_index)}"
|
||||
target_path = base_image_dir / rel_path
|
||||
row["image_path"] = rel_path
|
||||
|
||||
if target_path.exists() and not args.overwrite:
|
||||
print(f"exists {rel_path}", flush=True)
|
||||
continue
|
||||
|
||||
generation_tasks.append(
|
||||
(row["name"], rel_path, target_path, prompt_for(row, warden_index))
|
||||
)
|
||||
|
||||
if args.dry_run:
|
||||
print(f"Dry run planned {warden_index} Warden image paths", flush=True)
|
||||
else:
|
||||
if args.limit:
|
||||
generation_tasks = generation_tasks[: args.limit]
|
||||
|
||||
for _, _, target_path, _ in generation_tasks:
|
||||
target_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def generate_task(task):
|
||||
name, rel_path, target_path, prompt = task
|
||||
print(f"generate {rel_path} for {name}", flush=True)
|
||||
image_bytes = generate_with_retries(key, args, prompt)
|
||||
if not image_bytes.startswith(b"\x89PNG\r\n\x1a\n"):
|
||||
raise RuntimeError(f"Generated image for {name} is not a PNG")
|
||||
target_path.write_bytes(image_bytes)
|
||||
return rel_path
|
||||
|
||||
generated_count = 0
|
||||
if generation_tasks:
|
||||
with ThreadPoolExecutor(max_workers=max(1, args.concurrency)) as executor:
|
||||
futures = [executor.submit(generate_task, task) for task in generation_tasks]
|
||||
for future in as_completed(futures):
|
||||
rel_path = future.result()
|
||||
generated_count += 1
|
||||
print(
|
||||
f"wrote {rel_path} ({generated_count}/{len(generation_tasks)})",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
write_rows(hero_tsv, fieldnames, rows)
|
||||
print(f"Updated {hero_tsv}; generated {generated_count} missing images", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+33
@@ -0,0 +1,33 @@
|
||||
#!/bin/bash
|
||||
# Runs the repository-pinned clang-tidy binary from the workspace root.
|
||||
#
|
||||
# Bazel's `run` command executes clang-tidy from its runfiles tree, which makes
|
||||
# workspace-relative source paths and .clang-tidy discovery fail. This wrapper
|
||||
# builds the tool and then invokes the produced binary directly.
|
||||
|
||||
set -e
|
||||
|
||||
export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
|
||||
|
||||
repo_root="$(git rev-parse --show-toplevel)"
|
||||
|
||||
if [ "$#" -eq 0 ]; then
|
||||
echo "Usage: scripts/run-clang-tidy.sh [clang-tidy args] <files> [-- <compiler args>]"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " scripts/run-clang-tidy.sh --verify-config"
|
||||
echo " scripts/run-clang-tidy.sh src/main/cpp/net/eagle0/shardok/library/CombatDamage.hpp -- -I$repo_root -std=c++23"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
bazel build @llvm_toolchain//:clang-tidy
|
||||
|
||||
bazel_bin="$(bazel info bazel-bin)"
|
||||
clang_tidy="$bazel_bin/external/toolchains_llvm++llvm+llvm_toolchain/clang-tidy"
|
||||
|
||||
if [ ! -x "$clang_tidy" ]; then
|
||||
echo "Unable to find clang-tidy binary at $clang_tidy"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
"$clang_tidy" --config-file="$repo_root/.clang-tidy" "$@"
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/bin/zsh
|
||||
|
||||
/opt/homebrew/bin/s3cmd sync \
|
||||
--acl-public \
|
||||
--exclude="*dream_log.txt" \
|
||||
--exclude="*.DS_Store" \
|
||||
--delete-removed \
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <mutex>
|
||||
|
||||
#include "tools/cpp/runfiles/runfiles.h"
|
||||
@@ -37,7 +38,7 @@ auto rloc(const string& execPath) -> string {
|
||||
const std::unique_ptr<Runfiles> runfiles(Runfiles::Create(execPath, &error));
|
||||
|
||||
if (runfiles == nullptr) {
|
||||
fprintf(stderr, "Error! %s\n", error.c_str());
|
||||
std::cerr << "Error! " << error << '\n';
|
||||
abort();
|
||||
// error handling
|
||||
}
|
||||
@@ -78,9 +79,9 @@ auto FilesystemUtils::MapFilesDirectory() -> string {
|
||||
|
||||
void FilesystemUtils::MakeDirectoryIfNecessary(const string& directoryPath) {
|
||||
if (fs::create_directories(directoryPath))
|
||||
fprintf(stderr, "Directory %s created\n", directoryPath.c_str());
|
||||
std::cerr << "Directory " << directoryPath << " created\n";
|
||||
else
|
||||
fprintf(stderr, "No new directory created for %s\n", directoryPath.c_str());
|
||||
std::cerr << "No new directory created for " << directoryPath << '\n';
|
||||
}
|
||||
|
||||
auto FilesystemUtils::SaveFilesDirectory() -> string {
|
||||
@@ -140,11 +141,11 @@ auto FilesystemUtils::AtomicallySaveToPath(const string& path, const byte_vector
|
||||
if (ostr.good()) {
|
||||
const int err = rename(tempPath.c_str(), path.c_str());
|
||||
if (err == -1) {
|
||||
fprintf(stderr, "Failed to move file to %s! Errno %d\n", path.c_str(), errno);
|
||||
std::cerr << "Failed to move file to " << path << "! Errno " << errno << '\n';
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
fprintf(stderr, "Failed writing to %s!\n", tempPath.c_str());
|
||||
std::cerr << "Failed writing to " << tempPath << "!\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
// Copyright © 2017 none. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef FilesystemUtils_hpp
|
||||
#define FilesystemUtils_hpp
|
||||
#ifndef EAGLE0_COMMON_FILESYSTEM_UTILS_HPP
|
||||
#define EAGLE0_COMMON_FILESYSTEM_UTILS_HPP
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -36,4 +36,4 @@ public:
|
||||
static auto LoadFromPath(const string& path) -> byte_vector;
|
||||
};
|
||||
|
||||
#endif /* FilesystemUtils_hpp */
|
||||
#endif // EAGLE0_COMMON_FILESYSTEM_UTILS_HPP
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
// Copyright © 2018 none. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef MapUtils_hpp
|
||||
#define MapUtils_hpp
|
||||
#ifndef EAGLE0_COMMON_MAP_UTILS_HPP
|
||||
#define EAGLE0_COMMON_MAP_UTILS_HPP
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
@@ -19,4 +19,4 @@ auto IntForKey(const std::unordered_map<std::string, std::string>& map, const st
|
||||
auto BoolForKey(const std::unordered_map<std::string, std::string>& map, const std::string& key)
|
||||
-> bool;
|
||||
|
||||
#endif /* MapUtils_hpp */
|
||||
#endif // EAGLE0_COMMON_MAP_UTILS_HPP
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
// Copyright © 2019 none. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef ProtobufWarningSuppression_h
|
||||
#define ProtobufWarningSuppression_h
|
||||
#ifndef EAGLE0_COMMON_PROTOBUF_WARNING_SUPPRESSION_HPP
|
||||
#define EAGLE0_COMMON_PROTOBUF_WARNING_SUPPRESSION_HPP
|
||||
|
||||
#define SUPPRESS_PROTOBUF_WARNINGS \
|
||||
_Pragma("GCC diagnostic ignored \"-Wpragmas\"") \
|
||||
@@ -19,4 +19,4 @@
|
||||
"GCC diagnostic ignored \"-Wshorten-64-to-32\"") \
|
||||
_Pragma("GCC diagnostic ignored \"-Wnested-anon-types\"")
|
||||
|
||||
#endif /* ProtobufWarningSuppression_h */
|
||||
#endif // EAGLE0_COMMON_PROTOBUF_WARNING_SUPPRESSION_HPP
|
||||
|
||||
@@ -6,11 +6,13 @@
|
||||
// Copyright (c) 2015 none. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef __eagle0__ShardokRandomGenerator__
|
||||
#define __eagle0__ShardokRandomGenerator__
|
||||
#ifndef EAGLE0_COMMON_RANDOM_GENERATOR_HPP
|
||||
#define EAGLE0_COMMON_RANDOM_GENERATOR_HPP
|
||||
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <random>
|
||||
#include <vector>
|
||||
|
||||
class RandomGenerator;
|
||||
|
||||
@@ -22,6 +24,10 @@ private:
|
||||
|
||||
public:
|
||||
RandomGenerator() = default;
|
||||
RandomGenerator(const RandomGenerator&) = default;
|
||||
auto operator=(const RandomGenerator&) -> RandomGenerator& = default;
|
||||
RandomGenerator(RandomGenerator&&) = default;
|
||||
auto operator=(RandomGenerator&&) -> RandomGenerator& = default;
|
||||
|
||||
virtual ~RandomGenerator() = default;
|
||||
|
||||
@@ -35,7 +41,7 @@ public:
|
||||
|
||||
template<class T>
|
||||
auto RandomElement(const std::vector<T>& vec) -> T {
|
||||
return vec[IntBelow((int)vec.size())];
|
||||
return vec.at(static_cast<std::size_t>(IntBelow(static_cast<int>(vec.size()))));
|
||||
}
|
||||
|
||||
static auto ChanceOpenEndedPercentileAtOrAbove(double value) -> double;
|
||||
@@ -56,4 +62,4 @@ public:
|
||||
auto IntBetween(int min, int max) -> int override; // inclusive, exclusive (min<=return<max)
|
||||
};
|
||||
|
||||
#endif /* defined(__eagle0__ShardokRandomGenerator__) */
|
||||
#endif // EAGLE0_COMMON_RANDOM_GENERATOR_HPP
|
||||
|
||||
@@ -6,10 +6,11 @@
|
||||
// Copyright © 2018 none. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef SequenceRandomGenerator_h
|
||||
#define SequenceRandomGenerator_h
|
||||
#ifndef EAGLE0_COMMON_SEQUENCE_RANDOM_GENERATOR_HPP
|
||||
#define EAGLE0_COMMON_SEQUENCE_RANDOM_GENERATOR_HPP
|
||||
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/common/RandomGenerator.hpp"
|
||||
@@ -24,22 +25,22 @@
|
||||
// which produces: initial=2 (triggers open-ended), accumulated=52, final=2-52=-50
|
||||
class SequenceRandomGenerator : public ::RandomGenerator {
|
||||
private:
|
||||
const std::vector<double> sequence;
|
||||
std::vector<double> sequence;
|
||||
size_t position = 0;
|
||||
|
||||
auto DoubleZeroToOne() -> double override {
|
||||
const double nextVal = sequence[position];
|
||||
const double nextVal = sequence.at(position);
|
||||
position++;
|
||||
if (position >= sequence.size()) { position = 0; }
|
||||
return nextVal;
|
||||
}
|
||||
|
||||
public:
|
||||
explicit SequenceRandomGenerator(std::vector<double> s) : sequence(std::move(s)), position(0) {}
|
||||
explicit SequenceRandomGenerator(std::vector<double> s) : sequence(std::move(s)) {}
|
||||
static auto WithSequence(const std::vector<double>& s)
|
||||
-> std::shared_ptr<SequenceRandomGenerator> {
|
||||
return std::make_shared<SequenceRandomGenerator>(s);
|
||||
}
|
||||
};
|
||||
|
||||
#endif /* SequenceRandomGenerator_h */
|
||||
#endif // EAGLE0_COMMON_SEQUENCE_RANDOM_GENERATOR_HPP
|
||||
|
||||
@@ -80,7 +80,7 @@ void EmitShardokLatencyTrace(
|
||||
out << "}";
|
||||
|
||||
std::scoped_lock lock(LogMutex());
|
||||
std::cerr << out.str() << std::endl;
|
||||
std::cerr << out.str() << '\n';
|
||||
}
|
||||
|
||||
ScopedShardokLatencyTrace::ScopedShardokLatencyTrace(
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <condition_variable>
|
||||
#include <functional>
|
||||
#include <future>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <queue>
|
||||
@@ -171,7 +172,7 @@ public:
|
||||
// Get detailed queue information for debugging
|
||||
void debug_queue_state() const {
|
||||
std::unique_lock<std::mutex> lock(const_cast<std::mutex&>(queue_mutex));
|
||||
printf("ThreadPool: Queue size: %zu\n", tasks.size());
|
||||
std::cout << "ThreadPool: Queue size: " << tasks.size() << '\n';
|
||||
if (!tasks.empty()) {
|
||||
// Create a copy to inspect priorities without modifying queue
|
||||
auto queue_copy = tasks;
|
||||
@@ -180,9 +181,9 @@ public:
|
||||
priorities.push_back(queue_copy.top().priority);
|
||||
queue_copy.pop();
|
||||
}
|
||||
printf("ThreadPool: Priorities in queue: ");
|
||||
for (int p : priorities) { printf("%d ", p); }
|
||||
printf("\n");
|
||||
std::cout << "ThreadPool: Priorities in queue: ";
|
||||
for (int p : priorities) { std::cout << p << ' '; }
|
||||
std::cout << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,4 +198,4 @@ public:
|
||||
|
||||
} // namespace eagle0::common
|
||||
|
||||
#endif // EAGLE0_THREADPOOL_HPP
|
||||
#endif // EAGLE0_THREADPOOL_HPP
|
||||
|
||||
@@ -6,14 +6,14 @@
|
||||
// Copyright © 2018 none. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef TsvParser_hpp
|
||||
#define TsvParser_hpp
|
||||
#ifndef EAGLE0_COMMON_TSV_PARSER_HPP
|
||||
#define EAGLE0_COMMON_TSV_PARSER_HPP
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
typedef std::unordered_map<std::string, std::string> StringMap;
|
||||
using StringMap = std::unordered_map<std::string, std::string>;
|
||||
|
||||
// Parses a TSV, with the first column becoming map keys and subsequent columns becoming values
|
||||
// for different maps in the returned vector.
|
||||
@@ -23,4 +23,4 @@ public:
|
||||
std::vector<StringMap> ParseColumnEntryTsv(std::string tsv);
|
||||
};
|
||||
|
||||
#endif /* TsvParser_hpp */
|
||||
#endif // EAGLE0_COMMON_TSV_PARSER_HPP
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
// Copyright © 2017 none. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef byte_vector_h
|
||||
#define byte_vector_h
|
||||
#ifndef EAGLE0_COMMON_BYTE_VECTOR_HPP
|
||||
#define EAGLE0_COMMON_BYTE_VECTOR_HPP
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
@@ -184,4 +184,4 @@ inline auto operator<<(std::ostream& ostr, byte_vector vec) -> std::ostream& {
|
||||
return ostr;
|
||||
}
|
||||
|
||||
#endif /* byte_vector_h */
|
||||
#endif // EAGLE0_COMMON_BYTE_VECTOR_HPP
|
||||
|
||||
@@ -9,9 +9,11 @@
|
||||
#include <fstream>
|
||||
#include <future>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <mutex>
|
||||
#include <random>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <thread>
|
||||
|
||||
@@ -862,12 +864,12 @@ auto AbstractMCTSAI::LogSearchResults(
|
||||
// Log selected command
|
||||
const std::string selectedDesc =
|
||||
bestChild->action ? bestChild->action->getDescription() : "Unknown";
|
||||
printf("MCTS: Selected %s (visit:%d, reward:%.2f, lookahead:%.2f) from %zu options\n",
|
||||
selectedDesc.c_str(),
|
||||
bestChild->visitCount,
|
||||
bestChild->averageReward,
|
||||
bestChild->lookaheadScore,
|
||||
rootNode->children.size());
|
||||
std::ostringstream selectedLine;
|
||||
selectedLine << std::fixed << std::setprecision(2) << "MCTS: Selected " << selectedDesc
|
||||
<< " (visit:" << bestChild->visitCount << ", reward:" << bestChild->averageReward
|
||||
<< ", lookahead:" << bestChild->lookaheadScore << ") from "
|
||||
<< rootNode->children.size() << " options";
|
||||
std::cout << selectedLine.str() << '\n';
|
||||
|
||||
// Log top 3 actions by visits
|
||||
std::vector<const MCTSNode*> sortedChildren;
|
||||
@@ -879,20 +881,17 @@ auto AbstractMCTSAI::LogSearchResults(
|
||||
|
||||
// Show all actions if there are <= 10, otherwise top 5
|
||||
const size_t numToShow = sortedChildren.size() <= 10 ? sortedChildren.size() : 5;
|
||||
printf("MCTS: Top %zu actions by visits (out of %zu total):\n",
|
||||
numToShow,
|
||||
sortedChildren.size());
|
||||
std::cout << "MCTS: Top " << numToShow << " actions by visits (out of " << sortedChildren.size()
|
||||
<< " total):\n";
|
||||
for (size_t i = 0; i < numToShow; ++i) {
|
||||
const auto* child = sortedChildren[i];
|
||||
printf(" [%zu] visits:%d avgReward:%.2f immediate:%.2f lookahead:%.2f",
|
||||
i,
|
||||
child->visitCount,
|
||||
child->averageReward,
|
||||
child->immediateScore,
|
||||
child->lookaheadScore);
|
||||
std::ostringstream line;
|
||||
line << std::fixed << std::setprecision(2) << " [" << i << "] visits:" << child->visitCount
|
||||
<< " avgReward:" << child->averageReward << " immediate:" << child->immediateScore
|
||||
<< " lookahead:" << child->lookaheadScore;
|
||||
|
||||
// Show the action's own description
|
||||
if (child->action) { printf(" %s", child->action->getDescription().c_str()); }
|
||||
if (child->action) { line << ' ' << child->action->getDescription(); }
|
||||
|
||||
// Show sequence preview
|
||||
if (!child->children.empty()) {
|
||||
@@ -905,16 +904,15 @@ auto AbstractMCTSAI::LogSearchResults(
|
||||
}
|
||||
}
|
||||
if (bestNext && bestNext->action) {
|
||||
printf(" -> %s", bestNext->action->getDescription().c_str());
|
||||
line << " -> " << bestNext->action->getDescription();
|
||||
}
|
||||
}
|
||||
printf("\n");
|
||||
std::cout << line.str() << '\n';
|
||||
}
|
||||
|
||||
printf("MCTS: Tree stats - max depth:%d, total nodes:%d, root visits:%d\n",
|
||||
result.searchDepth,
|
||||
result.nodesEvaluated,
|
||||
rootNode->visitCount);
|
||||
std::cout << "MCTS: Tree stats - max depth:" << result.searchDepth
|
||||
<< ", total nodes:" << result.nodesEvaluated
|
||||
<< ", root visits:" << rootNode->visitCount << '\n';
|
||||
|
||||
// Log best sequence from chosen action (following most-visited path)
|
||||
std::vector<const MCTSNode*> bestSequence;
|
||||
@@ -940,7 +938,10 @@ auto AbstractMCTSAI::LogSearchResults(
|
||||
}
|
||||
|
||||
if (!bestSequence.empty()) {
|
||||
printf("MCTS: Best sequence from chosen action (final: %.2f):\n", sequenceScore);
|
||||
std::ostringstream sequenceLine;
|
||||
sequenceLine << std::fixed << std::setprecision(2)
|
||||
<< "MCTS: Best sequence from chosen action (final: " << sequenceScore << "):";
|
||||
std::cout << sequenceLine.str() << '\n';
|
||||
int displayedStep = 0;
|
||||
for (size_t i = 0; i < bestSequence.size(); ++i) {
|
||||
const auto* node = bestSequence[i];
|
||||
@@ -949,29 +950,31 @@ auto AbstractMCTSAI::LogSearchResults(
|
||||
if (i > 0 && node->parent && node->parent->IsChanceNode()) { continue; }
|
||||
|
||||
displayedStep++;
|
||||
printf(" %d.", displayedStep);
|
||||
if (node->action) { printf(" %s", node->action->getDescription().c_str()); }
|
||||
std::ostringstream line;
|
||||
line << " " << displayedStep << ".";
|
||||
if (node->action) { line << ' ' << node->action->getDescription(); }
|
||||
|
||||
// If this is a chance node, display outcome probabilities and scores
|
||||
if (node->IsChanceNode() && !node->outcomeProbabilities.empty()) {
|
||||
printf(" [");
|
||||
line << " [";
|
||||
for (size_t j = 0; j < node->outcomeProbabilities.size(); ++j) {
|
||||
if (j > 0) printf(", ");
|
||||
if (j > 0) { line << ", "; }
|
||||
const double prob = node->outcomeProbabilities[j] * 100;
|
||||
// Show lookahead score for each outcome if child exists
|
||||
if (j < node->children.size() && node->children[j]->visitCount > 0) {
|
||||
printf("%.0f%%->%.1f", prob, node->children[j]->lookaheadScore);
|
||||
line << std::fixed << std::setprecision(0) << prob << "%->"
|
||||
<< std::setprecision(1) << node->children[j]->lookaheadScore;
|
||||
} else {
|
||||
printf("%.0f%%->?", prob);
|
||||
line << std::fixed << std::setprecision(0) << prob << "%->?";
|
||||
}
|
||||
}
|
||||
printf("]");
|
||||
line << "]";
|
||||
}
|
||||
|
||||
printf(" (visits:%d, immediate:%.2f, lookahead:%.2f)\n",
|
||||
node->visitCount,
|
||||
node->immediateScore,
|
||||
node->lookaheadScore);
|
||||
line << std::fixed << std::setprecision(2) << " (visits:" << node->visitCount
|
||||
<< ", immediate:" << node->immediateScore << ", lookahead:" << node->lookaheadScore
|
||||
<< ")";
|
||||
std::cout << line.str() << '\n';
|
||||
|
||||
// For non-root nodes in the sequence, show what the top alternatives were
|
||||
// Skip showing alternatives for chance nodes (they have outcome children, not action
|
||||
@@ -991,17 +994,17 @@ auto AbstractMCTSAI::LogSearchResults(
|
||||
});
|
||||
|
||||
// Show top 3 alternatives at this decision point
|
||||
printf(" Alternatives at this node (%zu total):\n", siblings.size());
|
||||
std::cout << " Alternatives at this node (" << siblings.size() << " total):\n";
|
||||
const size_t topN = std::min(siblings.size(), size_t(3));
|
||||
for (size_t j = 0; j < topN; ++j) {
|
||||
const auto* alt = siblings[j];
|
||||
printf(" [%zu] visits:%d immediate:%.2f lookahead:%.2f",
|
||||
j,
|
||||
alt->visitCount,
|
||||
alt->immediateScore,
|
||||
alt->lookaheadScore);
|
||||
if (alt->action) { printf(" %s", alt->action->getDescription().c_str()); }
|
||||
printf("\n");
|
||||
std::ostringstream altLine;
|
||||
altLine << std::fixed << std::setprecision(2) << " [" << j
|
||||
<< "] visits:" << alt->visitCount
|
||||
<< " immediate:" << alt->immediateScore
|
||||
<< " lookahead:" << alt->lookaheadScore;
|
||||
if (alt->action) { altLine << ' ' << alt->action->getDescription(); }
|
||||
std::cout << altLine.str() << '\n';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1013,7 +1016,7 @@ auto AbstractMCTSAI::DumpTreeToFile(const MCTSNode* root, const std::string& fil
|
||||
|
||||
std::ofstream out(filepath);
|
||||
if (!out) {
|
||||
fprintf(stderr, "Failed to open dump file: %s\n", filepath.c_str());
|
||||
std::cerr << "Failed to open dump file: " << filepath << '\n';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1042,7 +1045,7 @@ auto AbstractMCTSAI::DumpTreeToFile(const MCTSNode* root, const std::string& fil
|
||||
out << "\n=== End of Tree Dump ===\n";
|
||||
out.close();
|
||||
|
||||
printf("MCTS: Tree dumped to %s\n", filepath.c_str());
|
||||
std::cout << "MCTS: Tree dumped to " << filepath << '\n';
|
||||
}
|
||||
|
||||
auto AbstractMCTSAI::DumpNodeRecursive(
|
||||
|
||||
@@ -141,9 +141,8 @@ public:
|
||||
// Default implementation: no filtering, so filtered index = original index
|
||||
[[nodiscard]] virtual size_t mapFilteredIndexToOriginal(
|
||||
size_t filteredIndex,
|
||||
const MCTSGameState& state) const {
|
||||
const MCTSGameState& /*state*/) const {
|
||||
// Default: no filtering, index stays the same
|
||||
(void)state; // Suppress unused parameter warning
|
||||
return filteredIndex;
|
||||
}
|
||||
|
||||
@@ -158,4 +157,4 @@ public:
|
||||
} // namespace mcts
|
||||
} // namespace shardok
|
||||
|
||||
#endif // EAGLE0_MCTS_GAME_ENGINE_HPP
|
||||
#endif // EAGLE0_MCTS_GAME_ENGINE_HPP
|
||||
|
||||
@@ -6,8 +6,11 @@
|
||||
#define EAGLE0_ABSTRACT_MCTSNODE_HPP
|
||||
|
||||
#include <cmath>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
|
||||
#include "MCTSAction.hpp"
|
||||
@@ -205,13 +208,13 @@ struct MCTSNode {
|
||||
isMaximizingPlayer ? child->lookaheadScore : -child->lookaheadScore;
|
||||
const double explorationValue =
|
||||
explorationConstant * std::sqrt(std::log(visitCount) / child->visitCount);
|
||||
printf(" UCB: %s lookahead=%.2f expl=%.2f (+%.2f) = %.2f [%s]\n",
|
||||
isMaximizingPlayer ? "MAX" : "MIN",
|
||||
child->lookaheadScore,
|
||||
exploitationValue,
|
||||
explorationValue,
|
||||
value,
|
||||
child->action ? child->action->getDescription().c_str() : "root");
|
||||
std::ostringstream line;
|
||||
line << std::fixed << std::setprecision(2)
|
||||
<< " UCB: " << (isMaximizingPlayer ? "MAX" : "MIN")
|
||||
<< " lookahead=" << child->lookaheadScore << " expl=" << exploitationValue
|
||||
<< " (+" << explorationValue << ") = " << value << " ["
|
||||
<< (child->action ? child->action->getDescription() : "root") << "]";
|
||||
std::cout << line.str() << '\n';
|
||||
}
|
||||
|
||||
if (value > bestValue) {
|
||||
|
||||
@@ -28,36 +28,6 @@ cc_binary(
|
||||
deps = [":shardok-server-lib"],
|
||||
)
|
||||
|
||||
cc_binary(
|
||||
name = "shardok-inspector",
|
||||
srcs = ["shardok_inspector.cpp"],
|
||||
copts = COPTS,
|
||||
data = [
|
||||
"//src/main/resources/net/eagle0/shardok:battalion_types",
|
||||
"//src/main/resources/net/eagle0/shardok:settings",
|
||||
"//src/main/resources/net/eagle0/shardok/maps",
|
||||
],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//src/main/cpp/net/eagle0/common:byte_vector",
|
||||
"//src/main/cpp/net/eagle0/common:filesystem_utils",
|
||||
"//src/main/cpp/net/eagle0/common:tsv_parser",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:engine",
|
||||
],
|
||||
)
|
||||
|
||||
cc_binary(
|
||||
name = "corrupt-game-checker",
|
||||
srcs = ["CorruptGameChecker.cpp"],
|
||||
copts = COPTS,
|
||||
deps = [
|
||||
"//src/main/cpp/net/eagle0/common:byte_vector",
|
||||
"//src/main/cpp/net/eagle0/common:filesystem_utils",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:game_state_w",
|
||||
"//src/main/protobuf/net/eagle0/common:shardok_internal_interface_cc_grpc",
|
||||
],
|
||||
)
|
||||
|
||||
cc_binary(
|
||||
name = "hex-map-migrator",
|
||||
srcs = ["HexMapMigrator.cpp"],
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
//
|
||||
// Created by Dan Crosby on 1/9/22.
|
||||
//
|
||||
|
||||
#include "src/main/cpp/net/eagle0/common/byte_vector.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
|
||||
#include "src/main/protobuf/net/eagle0/common/shardok_internal_interface.pb.h"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/storage/game.pb.h"
|
||||
|
||||
auto main(int argc, char** argv) -> int {
|
||||
char* path = argv[1];
|
||||
|
||||
printf("Checking file %s\n", path);
|
||||
|
||||
auto bytes = byte_vector::FromPath(path);
|
||||
|
||||
net::eagle0::shardok::storage::Game game;
|
||||
game.ParseFromArray(bytes.data(), bytes.size());
|
||||
|
||||
printf("The game ID is %s\n", game.game_id().c_str());
|
||||
printf("The size is %lu\n", bytes.size());
|
||||
|
||||
const int arCount = game.action_result_size();
|
||||
printf("There are %d results\n", arCount);
|
||||
|
||||
for (int arIndex = 0; arIndex < arCount; arIndex++) {
|
||||
shardok::GameStateW gameState =
|
||||
shardok::GameStateW::FromByteString(game.action_result(arIndex).state_after_fb());
|
||||
const auto* hexMap = gameState->hex_map();
|
||||
|
||||
for (int terrainIndex = 0; terrainIndex < hexMap->terrain()->size(); terrainIndex++) {
|
||||
const auto* terrain = hexMap->terrain()->Get(terrainIndex);
|
||||
if (terrain->type() == net::eagle0::shardok::storage::fb::Terrain_::Type_STILL_WATER)
|
||||
continue;
|
||||
if (terrain->type() == net::eagle0::shardok::storage::fb::Terrain_::Type_RIVER)
|
||||
continue;
|
||||
|
||||
if (terrain->modifier().bridge().present()) {
|
||||
printf("Caught one! arIndex %d, terrainIndex %d\n", arIndex, terrainIndex);
|
||||
printf("Action result was %s\n",
|
||||
game.action_result(arIndex).action_result().DebugString().c_str());
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
// Created by Dan Crosby on 3/19/20.
|
||||
//
|
||||
|
||||
#include <cstdio>
|
||||
#include <iostream>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/common/FilesystemUtils.hpp"
|
||||
#include "src/main/cpp/net/eagle0/common/byte_vector.hpp"
|
||||
@@ -14,7 +14,7 @@ void ModifyMap(HexMapProto& map);
|
||||
|
||||
int main(const int argc, char** argv) {
|
||||
if (argc < 3) {
|
||||
printf("Usage: %s input_path output_path\n", argv[0]);
|
||||
std::cerr << "Usage: " << argv[0] << " input_path output_path\n";
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -22,21 +22,25 @@ int main(const int argc, char** argv) {
|
||||
const std::string outputPath = argv[2];
|
||||
const byte_vector mapProtoBytes = byte_vector::FromPath(inputPath);
|
||||
net::eagle0::shardok::common::HexMap mapProto;
|
||||
mapProto.ParseFromArray(mapProtoBytes.data(), (int)mapProtoBytes.size());
|
||||
if (!mapProto.ParseFromArray(mapProtoBytes.data(), static_cast<int>(mapProtoBytes.size()))) {
|
||||
std::cerr << "Failed to parse hex map from " << inputPath << '\n';
|
||||
return -1;
|
||||
}
|
||||
|
||||
ModifyMap(mapProto);
|
||||
const std::string outputString = mapProto.SerializeAsString();
|
||||
if (FilesystemUtils::AtomicallySaveToPath(outputPath, byte_vector(outputString))) return 0;
|
||||
|
||||
printf("Failed to save.\n");
|
||||
std::cerr << "Failed to save.\n";
|
||||
return -1;
|
||||
}
|
||||
|
||||
void ModifyMap(HexMapProto& map) {
|
||||
for (int i = 0; i < map.terrain_size(); i++) {
|
||||
const int terrainCount = map.terrain_size();
|
||||
for (int i = 0; i < terrainCount; i++) {
|
||||
const auto existingTerrainType = map.terrain(i).type();
|
||||
const auto newTerrain =
|
||||
(net::eagle0::shardok::common::Terrain_Type)(existingTerrainType + 2);
|
||||
static_cast<net::eagle0::shardok::common::Terrain_Type>(existingTerrainType + 2);
|
||||
map.mutable_terrain(i)->set_type(newTerrain);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,50 +4,51 @@
|
||||
|
||||
#include "MapInfoCalculator.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIWaterCrossingCalculator.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/HexMapHelpers.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/util/MapLoader.hpp"
|
||||
|
||||
using namespace shardok;
|
||||
using std::endl;
|
||||
|
||||
auto LoadMapFb(const string& mapName) -> Wrapper<HexMap> {
|
||||
auto LoadMapFb(const std::string& mapName) -> shardok::Wrapper<shardok::HexMap> {
|
||||
const HexMapProto mapProto = LoadMap(mapName);
|
||||
|
||||
flatbuffers::FlatBufferBuilder fbb{};
|
||||
const auto hexMapOffset = fb::ConvertHexMapProto(fbb, mapProto);
|
||||
const auto hexMapOffset = shardok::fb::ConvertHexMapProto(fbb, mapProto);
|
||||
fbb.Finish(hexMapOffset);
|
||||
return Wrapper<HexMap>(fbb);
|
||||
return shardok::Wrapper<shardok::HexMap>(fbb);
|
||||
}
|
||||
|
||||
auto CalculateMap(
|
||||
const string& mapName,
|
||||
const std::shared_ptr<ActionPointDistancesCache>& apdCache,
|
||||
const BattalionTypeSPtr& battalionType) -> OneMapInfo {
|
||||
const std::string& mapName,
|
||||
const std::shared_ptr<shardok::ActionPointDistancesCache>& apdCache,
|
||||
const shardok::BattalionTypeSPtr& battalionType) -> OneMapInfo {
|
||||
auto hexMap = LoadMapFb(mapName);
|
||||
|
||||
const auto mapId = ActionPointDistancesCache::GetMapId(hexMap);
|
||||
const auto mapId = shardok::ActionPointDistancesCache::GetMapId(hexMap);
|
||||
|
||||
const CoordsSet criticalTileLocations = GetCriticalTileLocations(hexMap);
|
||||
const shardok::CoordsSet criticalTileLocations = shardok::GetCriticalTileLocations(hexMap);
|
||||
|
||||
OneMapInfo info{
|
||||
.castleCount = criticalTileLocations.size(),
|
||||
.name = mapName,
|
||||
.positionsRequiringCrossing = {}};
|
||||
|
||||
for (unsigned int i = 0; i < hexMap->attacker_starting_positions()->size(); i++) {
|
||||
for (std::size_t i = 0; i < hexMap->attacker_starting_positions()->size(); i++) {
|
||||
const auto* positionList = hexMap->attacker_starting_positions()->Get(i);
|
||||
if (positionList->positions()->size() < 1) continue;
|
||||
if (positionList->positions()->empty()) continue;
|
||||
if (positionList->positions()->size() != 10) {
|
||||
printf("Should have 10 starting positions!");
|
||||
abort();
|
||||
std::cerr << "Should have 10 starting positions!" << '\n';
|
||||
std::abort();
|
||||
}
|
||||
|
||||
int positionsRequiringCrossing = 0;
|
||||
for (const auto* startingPosition : *positionList->positions()) {
|
||||
for (const Coords& castlePosition : criticalTileLocations) {
|
||||
if (!CanReach(
|
||||
for (const shardok::Coords& castlePosition : criticalTileLocations) {
|
||||
if (!shardok::CanReach(
|
||||
*startingPosition,
|
||||
castlePosition,
|
||||
hexMap,
|
||||
@@ -59,8 +60,8 @@ auto CalculateMap(
|
||||
};
|
||||
}
|
||||
}
|
||||
info.positionsRequiringCrossing[i] = positionsRequiringCrossing;
|
||||
info.positionsRequiringCrossing[static_cast<int>(i)] = positionsRequiringCrossing;
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,19 +12,15 @@
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistancesCache.hpp"
|
||||
|
||||
using namespace shardok;
|
||||
using std::map;
|
||||
using std::string;
|
||||
|
||||
struct OneMapInfo {
|
||||
struct alignas(64) OneMapInfo {
|
||||
uint32_t castleCount;
|
||||
string name;
|
||||
map<int, int> positionsRequiringCrossing;
|
||||
} __attribute__((aligned(64)));
|
||||
std::string name;
|
||||
std::map<int, int> positionsRequiringCrossing;
|
||||
};
|
||||
|
||||
auto CalculateMap(
|
||||
const string& mapName,
|
||||
const std::shared_ptr<ActionPointDistancesCache>& apdCache,
|
||||
const BattalionTypeSPtr& battalionType) -> OneMapInfo;
|
||||
const std::string& mapName,
|
||||
const std::shared_ptr<shardok::ActionPointDistancesCache>& apdCache,
|
||||
const shardok::BattalionTypeSPtr& battalionType) -> OneMapInfo;
|
||||
|
||||
#endif // EAGLE0_MAPINFOCALCULATOR_HPP
|
||||
|
||||
@@ -2,37 +2,40 @@
|
||||
// Created by Dan Crosby on 4/18/22.
|
||||
//
|
||||
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "MapInfoCalculator.hpp"
|
||||
#include "src/main/cpp/net/eagle0/common/FilesystemUtils.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/util/BattalionTypeRegistrar.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/util/MapLoader.hpp"
|
||||
|
||||
using namespace shardok;
|
||||
using std::endl;
|
||||
|
||||
auto main(const int argc, char** argv) -> int {
|
||||
FilesystemUtils::SetExecPath(argv[0]);
|
||||
|
||||
std::ostream* outputStreamPtr = &std::cout;
|
||||
std::ofstream outputFile{};
|
||||
if (argc > 1) {
|
||||
const string outputPath = argv[1];
|
||||
outputStreamPtr = new std::ofstream(outputPath);
|
||||
const std::string outputPath = argv[1];
|
||||
outputFile.open(outputPath);
|
||||
outputStreamPtr = &outputFile;
|
||||
}
|
||||
std::ostream& outputStream = *outputStreamPtr;
|
||||
|
||||
const auto gameSettings = std::make_shared<GameSettings>();
|
||||
const auto gameSettings = std::make_shared<shardok::GameSettings>();
|
||||
auto setter = gameSettings->GetSetter();
|
||||
BattalionTypeRegistrar::RegisterBattalionTypes(setter);
|
||||
const BattalionTypeSPtr& battalionType = gameSettings->GetGetter().GetBattalionType(
|
||||
shardok::BattalionTypeRegistrar::RegisterBattalionTypes(setter);
|
||||
const shardok::BattalionTypeSPtr& battalionType = gameSettings->GetGetter().GetBattalionType(
|
||||
net::eagle0::shardok::storage::fb::BattalionTypeId_LIGHT_INFANTRY);
|
||||
|
||||
vector<OneMapInfo> mapInfos{};
|
||||
for (const string& mapName : GetMapNames()) {
|
||||
std::shared_ptr<ActionPointDistancesCache> apdCache =
|
||||
std::make_shared<ActionPointDistancesCache>();
|
||||
std::vector<OneMapInfo> mapInfos{};
|
||||
for (const std::string& mapName : GetMapNames()) {
|
||||
std::shared_ptr<shardok::ActionPointDistancesCache> apdCache =
|
||||
std::make_shared<shardok::ActionPointDistancesCache>();
|
||||
|
||||
mapInfos.push_back(CalculateMap(mapName, apdCache, battalionType));
|
||||
}
|
||||
@@ -41,29 +44,29 @@ auto main(const int argc, char** argv) -> int {
|
||||
bool firstMap = true;
|
||||
for (const OneMapInfo& mapInfo : mapInfos) {
|
||||
if (firstMap) {
|
||||
outputStream << endl;
|
||||
outputStream << '\n';
|
||||
firstMap = false;
|
||||
} else {
|
||||
outputStream << "," << endl;
|
||||
outputStream << "," << '\n';
|
||||
}
|
||||
outputStream << " {" << endl;
|
||||
outputStream << R"( "name": ")" << mapInfo.name << "\"," << endl;
|
||||
outputStream << " {" << '\n';
|
||||
outputStream << R"( "name": ")" << mapInfo.name << "\"," << '\n';
|
||||
|
||||
outputStream << " \"castleCount\": " << mapInfo.castleCount << "," << endl;
|
||||
outputStream << " \"castleCount\": " << mapInfo.castleCount << "," << '\n';
|
||||
outputStream << " \"positions\": {";
|
||||
|
||||
bool firstPosition = true;
|
||||
for (const auto& [position, count] : mapInfo.positionsRequiringCrossing) {
|
||||
if (firstPosition) {
|
||||
outputStream << endl;
|
||||
outputStream << '\n';
|
||||
firstPosition = false;
|
||||
} else {
|
||||
outputStream << "," << endl;
|
||||
outputStream << "," << '\n';
|
||||
}
|
||||
|
||||
outputStream << " \"" << position << "\": " << count;
|
||||
}
|
||||
outputStream << endl << " }" << endl << " }";
|
||||
outputStream << '\n' << " }" << '\n' << " }";
|
||||
}
|
||||
outputStream << endl << "]" << endl;
|
||||
outputStream << '\n' << "]" << '\n';
|
||||
}
|
||||
|
||||
@@ -2,9 +2,13 @@
|
||||
// Created by Dan Crosby on 3/29/21.
|
||||
//
|
||||
|
||||
#include <algorithm>
|
||||
#include <climits>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <numeric>
|
||||
#include <mutex>
|
||||
#include <random>
|
||||
#include <stdexcept>
|
||||
#include <thread>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/common/FilesystemUtils.hpp"
|
||||
@@ -14,8 +18,6 @@
|
||||
#include "src/main/protobuf/net/eagle0/common/random_units.pb.h"
|
||||
|
||||
using net::eagle0::common::RandomUnits;
|
||||
using std::shared_ptr;
|
||||
using std::vector;
|
||||
|
||||
constexpr int THREAD_COUNT = 16;
|
||||
|
||||
@@ -28,77 +30,110 @@ std::random_device rd;
|
||||
std::mt19937 g(rd());
|
||||
#endif
|
||||
|
||||
vector<string> fileNames{};
|
||||
std::mutex randomMutex;
|
||||
std::vector<std::string> fileNames{};
|
||||
|
||||
using namespace shardok;
|
||||
|
||||
ShardokGamesManager* manager;
|
||||
std::unique_ptr<shardok::ShardokGamesManager> manager;
|
||||
|
||||
auto randInt(const int lowInc, const int hiInc) {
|
||||
std::lock_guard<std::mutex> lock(randomMutex);
|
||||
return std::uniform_int_distribution(lowInc, hiInc)(g);
|
||||
}
|
||||
|
||||
void runBattle(const vector<shared_ptr<const UnitProto>>& units) {
|
||||
void runBattle(const std::vector<shardok::Unit>& units) {
|
||||
if (fileNames.empty()) { throw std::runtime_error("No map files found"); }
|
||||
if (units.size() < 2) { throw std::runtime_error("Need at least two units to run a battle"); }
|
||||
|
||||
auto mapName = fileNames[randInt(0, (int)fileNames.size() - 1)];
|
||||
auto month = randInt(1, 12);
|
||||
|
||||
auto p1UnitCount = randInt(1, 20);
|
||||
vector<UnitProto> p1Units{};
|
||||
const auto maxP1UnitCount = std::min<size_t>(20, units.size() - 1);
|
||||
auto p1UnitCount = randInt(1, static_cast<int>(maxP1UnitCount));
|
||||
std::vector<shardok::Unit> p1Units{};
|
||||
for (int i = 0; i < p1UnitCount; i++) {
|
||||
auto unit = *units[i];
|
||||
unit.set_player_id(0);
|
||||
auto unit = units[i];
|
||||
unit.mutate_player_id(0);
|
||||
p1Units.push_back(unit);
|
||||
}
|
||||
|
||||
auto p2UnitCount = randInt(1, 20);
|
||||
vector<UnitProto> p2Units{};
|
||||
const auto maxP2UnitCount = std::min<size_t>(20, units.size() - p1UnitCount);
|
||||
auto p2UnitCount = randInt(1, static_cast<int>(maxP2UnitCount));
|
||||
std::vector<shardok::Unit> p2Units{};
|
||||
for (int i = p1UnitCount; i < p1UnitCount + p2UnitCount; i++) {
|
||||
auto unit = *units[i];
|
||||
unit.set_player_id(1);
|
||||
auto unit = units[i];
|
||||
unit.mutate_player_id(1);
|
||||
p2Units.push_back(unit);
|
||||
}
|
||||
|
||||
printf("map name is %s, %d attacking %d\n", mapName.c_str(), p1UnitCount, p2UnitCount);
|
||||
std::cout << "map name is " << mapName << ", " << p1UnitCount << " attacking " << p2UnitCount
|
||||
<< '\n';
|
||||
|
||||
vector<VictoryConditionProto> attVictoryConditions{
|
||||
std::vector<VictoryConditionProto> attVictoryConditions{
|
||||
VictoryConditionProto::VICTORY_CONDITION_HOLDS_CRITICAL_TILES,
|
||||
VictoryConditionProto::VICTORY_CONDITION_LAST_PLAYER_STANDING};
|
||||
vector<VictoryConditionProto> defVictoryConditions{
|
||||
std::vector<VictoryConditionProto> defVictoryConditions{
|
||||
VictoryConditionProto::VICTORY_CONDITION_LAST_PLAYER_STANDING,
|
||||
VictoryConditionProto::VICTORY_CONDITION_WIN_AFTER_MAX_ROUNDS};
|
||||
|
||||
auto p1 = PlayerInfoWithUnits(0, true, 5, false, 10000, attVictoryConditions, {}, p1Units);
|
||||
auto p2 = PlayerInfoWithUnits(1, false, 7, false, 10000, defVictoryConditions, {}, p2Units);
|
||||
auto p1 = shardok::PlayerInfoWithUnits(
|
||||
0,
|
||||
true,
|
||||
5,
|
||||
false,
|
||||
10000,
|
||||
attVictoryConditions,
|
||||
{},
|
||||
p1Units,
|
||||
false);
|
||||
auto p2 = shardok::PlayerInfoWithUnits(
|
||||
1,
|
||||
true,
|
||||
7,
|
||||
true,
|
||||
10000,
|
||||
defVictoryConditions,
|
||||
{},
|
||||
p2Units,
|
||||
false);
|
||||
|
||||
string gameId = std::to_string(randInt(INT_MIN, INT_MAX));
|
||||
std::string gameId = std::to_string(randInt(INT_MIN, INT_MAX));
|
||||
|
||||
manager->LockedCreateSpecifiedGame(gameId, {p1, p2}, mapName, "", month, "");
|
||||
manager->UnlockedCreateSpecifiedGame(
|
||||
gameId,
|
||||
{p1, p2},
|
||||
mapName,
|
||||
"",
|
||||
month,
|
||||
TutorialBattleConfigProto{},
|
||||
"");
|
||||
}
|
||||
|
||||
auto LoadUnits() -> vector<shared_ptr<const UnitProto>> {
|
||||
auto LoadUnits() -> std::vector<shardok::Unit> {
|
||||
const auto bytes = byte_vector::FromPath("/tmp/random_heroes.bin");
|
||||
|
||||
RandomUnits commonUnits;
|
||||
commonUnits.ParseFromArray(bytes.data(), (int)bytes.size());
|
||||
printf("Loaded %d units\n", commonUnits.units_size());
|
||||
if (!commonUnits.ParseFromArray(bytes.data(), static_cast<int>(bytes.size()))) {
|
||||
throw std::runtime_error("Failed to parse /tmp/random_heroes.bin");
|
||||
}
|
||||
std::cout << "Loaded " << commonUnits.units_size() << " units\n";
|
||||
|
||||
vector<shared_ptr<const UnitProto>> units{};
|
||||
std::vector<shardok::Unit> units{};
|
||||
units.reserve(commonUnits.units_size());
|
||||
|
||||
for (const auto& commonUnit : commonUnits.units()) {
|
||||
// FIXME: assumes 2 players
|
||||
units.push_back(std::make_shared<const UnitProto>(ConvertUnit(commonUnit, 0, {0, 1})));
|
||||
units.push_back(shardok::ConvertUnit(commonUnit, 0, {0, 1}));
|
||||
}
|
||||
std::shuffle(units.begin(), units.end(), g);
|
||||
|
||||
return units;
|
||||
}
|
||||
|
||||
[[noreturn]] void runBattlesThread(const vector<shared_ptr<const UnitProto>>& units) {
|
||||
[[noreturn]] void runBattlesThread(const std::vector<shardok::Unit>& units) {
|
||||
while (true) { runBattle(units); }
|
||||
}
|
||||
|
||||
auto main(int argc, char** argv) -> int {
|
||||
auto main(int /*argc*/, char** argv) -> int {
|
||||
#if FIXED_SEED
|
||||
std::srand(4564564);
|
||||
g.seed(378473);
|
||||
@@ -110,12 +145,12 @@ auto main(int argc, char** argv) -> int {
|
||||
|
||||
auto randomUnits = LoadUnits();
|
||||
|
||||
printf("Map file directory is %s\n", FilesystemUtils::MapFilesDirectory().c_str());
|
||||
std::cout << "Map file directory is " << FilesystemUtils::MapFilesDirectory() << '\n';
|
||||
fileNames = FilesystemUtils::FilesInDirectory(FilesystemUtils::MapFilesDirectory(), ".e0m");
|
||||
|
||||
for (auto& fileName : fileNames) { fileName.resize(fileName.size() - 4); }
|
||||
|
||||
manager = new ShardokGamesManager(nullptr, std::vector<std::string>());
|
||||
manager = std::make_unique<shardok::ShardokGamesManager>(std::vector<std::string>());
|
||||
|
||||
std::thread threads[THREAD_COUNT];
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// Created by Dan Crosby on 6/19/23.
|
||||
//
|
||||
|
||||
#ifndef EAGLE0_AIATTACKGROUPS_HPP
|
||||
#define EAGLE0_AIATTACKGROUPS_HPP
|
||||
#ifndef EAGLE0_SHARDOK_AI_AI_ATTACK_GROUPS_HPP
|
||||
#define EAGLE0_SHARDOK_AI_AI_ATTACK_GROUPS_HPP
|
||||
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
@@ -20,7 +20,6 @@ namespace shardok {
|
||||
using GameState = net::eagle0::shardok::storage::fb::GameState;
|
||||
using Unit = net::eagle0::shardok::storage::fb::Unit;
|
||||
using net::eagle0::shardok::storage::fb::PlayerInfo;
|
||||
using std::vector;
|
||||
|
||||
using BattalionTypeGetter = std::function<BattalionTypeSPtr(BattalionTypeId)>;
|
||||
|
||||
@@ -33,9 +32,9 @@ struct TargetAndAttackLocations {
|
||||
|
||||
struct TargetPriorityList {
|
||||
UnitId attackingUnitId;
|
||||
vector<TargetAndAttackLocations> priorityOrder;
|
||||
std::vector<TargetAndAttackLocations> priorityOrder;
|
||||
|
||||
TargetPriorityList(const UnitId uid, const vector<TargetAndAttackLocations>& po)
|
||||
TargetPriorityList(const UnitId uid, const std::vector<TargetAndAttackLocations>& po)
|
||||
: attackingUnitId(uid),
|
||||
priorityOrder(po) {}
|
||||
};
|
||||
@@ -64,17 +63,17 @@ auto EffectiveDistance(
|
||||
|
||||
// Chooses a list of targets in priority order for each unit.
|
||||
auto GenerateTargetPriorities(
|
||||
const vector<const Unit*>& occupants,
|
||||
const std::vector<const Unit*>& occupants,
|
||||
const HexMap* map,
|
||||
const CoordsSet& targets,
|
||||
PlayerId attackerId,
|
||||
const vector<const Unit*>& remainingUnits,
|
||||
const std::vector<const Unit*>& remainingUnits,
|
||||
const APDCache& apdCache,
|
||||
const ALCache& alCache,
|
||||
const BattalionTypeGetter& battalionTypeGetter,
|
||||
ActionPoints braveWaterCost,
|
||||
bool isLateGame = false) -> vector<TargetPriorityList>;
|
||||
bool isLateGame = false) -> std::vector<TargetPriorityList>;
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
#endif // EAGLE0_AIATTACKGROUPS_HPP
|
||||
#endif // EAGLE0_SHARDOK_AI_AI_ATTACK_GROUPS_HPP
|
||||
|
||||
@@ -17,10 +17,8 @@
|
||||
namespace shardok {
|
||||
|
||||
using Unit = net::eagle0::shardok::storage::fb::Unit;
|
||||
using std::shared_ptr;
|
||||
using std::vector;
|
||||
|
||||
class AttackLocations {
|
||||
class alignas(128) AttackLocations {
|
||||
private:
|
||||
const MapIndex rowCount;
|
||||
const MapIndex columnCount;
|
||||
@@ -147,9 +145,9 @@ public:
|
||||
adjacentLocations += cs;
|
||||
allLocations += cs;
|
||||
}
|
||||
} __attribute__((aligned(128)));
|
||||
};
|
||||
|
||||
struct AttackLocationsCache {
|
||||
struct alignas(128) AttackLocationsCache {
|
||||
private:
|
||||
const MapIndex rowCount;
|
||||
const MapIndex columnCount;
|
||||
@@ -165,8 +163,8 @@ public:
|
||||
|
||||
auto CachedLocations(const Coords& singleTarget, bool lateGame) -> const AttackLocations&;
|
||||
auto CachedLocations(const CoordsSet& targets, bool lateGame) -> AttackLocations;
|
||||
auto CachedLocations(const vector<const Unit*>& enemies, bool lateGame) -> AttackLocations;
|
||||
} __attribute__((aligned(128)));
|
||||
auto CachedLocations(const std::vector<const Unit*>& enemies, bool lateGame) -> AttackLocations;
|
||||
};
|
||||
|
||||
using ALCache = std::unique_ptr<AttackLocationsCache>;
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// Created by Dan Crosby on 1/9/22.
|
||||
//
|
||||
|
||||
#ifndef EAGLE0_AIATTACKERSTRATEGYSELECTOR_HPP
|
||||
#define EAGLE0_AIATTACKERSTRATEGYSELECTOR_HPP
|
||||
#ifndef EAGLE0_SHARDOK_AI_AI_ATTACKER_STRATEGY_SELECTOR_HPP
|
||||
#define EAGLE0_SHARDOK_AI_AI_ATTACKER_STRATEGY_SELECTOR_HPP
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIAttackLocations.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AICommonTypes.hpp"
|
||||
@@ -32,4 +32,4 @@ public:
|
||||
};
|
||||
|
||||
} // namespace shardok
|
||||
#endif // EAGLE0_AIATTACKERSTRATEGYSELECTOR_HPP
|
||||
#endif // EAGLE0_SHARDOK_AI_AI_ATTACKER_STRATEGY_SELECTOR_HPP
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <future>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
|
||||
#include "AICommandFilter.hpp"
|
||||
@@ -22,11 +24,11 @@ namespace shardok {
|
||||
// No need to forward declare internal functions - use the public interface instead
|
||||
|
||||
// Helper constants and static variables
|
||||
static const std::vector<double> _averageSequence = {0.5};
|
||||
static const auto _averageGenerator = std::make_shared<SequenceRandomGenerator>(_averageSequence);
|
||||
static const std::vector<double> kAverageSequence = {0.5};
|
||||
static const auto kAverageGenerator = std::make_shared<SequenceRandomGenerator>(kAverageSequence);
|
||||
|
||||
#define MULTITHREAD true
|
||||
#define LOGGING_ 0
|
||||
constexpr bool kMultithread = true;
|
||||
constexpr bool kLogging = false;
|
||||
|
||||
// Helper function to determine if a command type is deterministic
|
||||
static auto IsDeterministic(const CommandType type) -> bool {
|
||||
@@ -49,6 +51,7 @@ static auto IsDeterministic(const CommandType type) -> bool {
|
||||
case net::eagle0::shardok::common::FORTIFY_COMMAND:
|
||||
case net::eagle0::shardok::common::BECOME_OUTLAW_COMMAND:
|
||||
case net::eagle0::shardok::common::HOLY_WAVE_COMMAND:
|
||||
case net::eagle0::shardok::common::EVACUATE_PRISONERS_COMMAND:
|
||||
case net::eagle0::shardok::common::REPAIR_COMMAND: return true;
|
||||
default: return false;
|
||||
}
|
||||
@@ -236,15 +239,16 @@ auto AICommandEvaluator::EvaluateWithRandomness(
|
||||
return lookaheadFuture.get();
|
||||
};
|
||||
|
||||
#if MULTITHREAD
|
||||
auto launchPolicy = remainingLookahead == 1 ? std::launch::async : std::launch::deferred;
|
||||
returnValue.lookaheadScore = std::async(launchPolicy, lookaheadLambda);
|
||||
#else
|
||||
std::promise<EvaluationResult> p;
|
||||
returnValue.lookaheadScore = p.get_future();
|
||||
auto lambdaResult = lookaheadLambda();
|
||||
p.set_value(lambdaResult);
|
||||
#endif
|
||||
if constexpr (kMultithread) {
|
||||
auto launchPolicy =
|
||||
remainingLookahead == 1 ? std::launch::async : std::launch::deferred;
|
||||
returnValue.lookaheadScore = std::async(launchPolicy, lookaheadLambda);
|
||||
} else {
|
||||
std::promise<EvaluationResult> p;
|
||||
returnValue.lookaheadScore = p.get_future();
|
||||
auto lambdaResult = lookaheadLambda();
|
||||
p.set_value(lambdaResult);
|
||||
}
|
||||
}
|
||||
|
||||
return returnValue;
|
||||
@@ -301,22 +305,23 @@ auto AICommandEvaluator::FindBestCommand(
|
||||
minDistToEnemies = 0.0; // No enemies found
|
||||
}
|
||||
|
||||
#if LOGGING_
|
||||
// Log command count and distance metrics for performance analysis
|
||||
const auto allCommandCount = guessedDescriptors->size();
|
||||
const auto filteredCommandCount = filteredIndices.size();
|
||||
const int currentRound = gameState->current_round();
|
||||
if constexpr (kLogging) {
|
||||
// Log command count and distance metrics for performance analysis
|
||||
const auto allCommandCount = guessedDescriptors->size();
|
||||
const auto filteredCommandCount = filteredIndices.size();
|
||||
const int currentRound = gameState->current_round();
|
||||
|
||||
printf("AI_COMMAND_COUNT: Round %d, Player %d, Defender %d, MinDist %.1f, Commands %zu -> %zu "
|
||||
"(%.1f%% filtered)\n",
|
||||
currentRound,
|
||||
static_cast<int>(pid),
|
||||
isDefender ? 1 : 0,
|
||||
minDistToEnemies,
|
||||
allCommandCount,
|
||||
filteredCommandCount,
|
||||
100.0 * (allCommandCount - filteredCommandCount) / allCommandCount);
|
||||
#endif
|
||||
const auto flags = std::cout.flags();
|
||||
const auto precision = std::cout.precision();
|
||||
std::cout << "AI_COMMAND_COUNT: Round " << currentRound << ", Player "
|
||||
<< static_cast<int>(pid) << ", Defender " << (isDefender ? 1 : 0) << ", MinDist "
|
||||
<< std::fixed << std::setprecision(1) << minDistToEnemies << ", Commands "
|
||||
<< allCommandCount << " -> " << filteredCommandCount << " ("
|
||||
<< 100.0 * (allCommandCount - filteredCommandCount) / allCommandCount
|
||||
<< "% filtered)\n";
|
||||
std::cout.flags(flags);
|
||||
std::cout.precision(precision);
|
||||
}
|
||||
|
||||
const auto commandCount = filteredIndices.size();
|
||||
|
||||
@@ -351,7 +356,7 @@ auto AICommandEvaluator::FindBestCommand(
|
||||
originalIndex,
|
||||
remainingLookahead,
|
||||
maxRepeatCount,
|
||||
_averageGenerator,
|
||||
kAverageGenerator,
|
||||
guessedEngine,
|
||||
attackerStrategy,
|
||||
allCastleCoords,
|
||||
@@ -519,7 +524,7 @@ auto AICommandEvaluator::EvaluateCommand(
|
||||
commandIndex,
|
||||
remainingLookahead,
|
||||
maxRepeatCount,
|
||||
_averageGenerator,
|
||||
kAverageGenerator,
|
||||
guessedEngine,
|
||||
attackerStrategy,
|
||||
allCastleCoords,
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
// Separated from AIScoreCalculator to isolate pure state scoring from lookahead logic.
|
||||
//
|
||||
|
||||
#ifndef EAGLE0_AICOMMANDEVALUATOR_HPP
|
||||
#define EAGLE0_AICOMMANDEVALUATOR_HPP
|
||||
#ifndef EAGLE0_SHARDOK_AI_AI_COMMAND_EVALUATOR_HPP
|
||||
#define EAGLE0_SHARDOK_AI_AI_COMMAND_EVALUATOR_HPP
|
||||
|
||||
#include <chrono>
|
||||
#include <future>
|
||||
@@ -114,4 +114,4 @@ private:
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
#endif // EAGLE0_AICOMMANDEVALUATOR_HPP
|
||||
#endif // EAGLE0_SHARDOK_AI_AI_COMMAND_EVALUATOR_HPP
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// Filter obviously bad commands to reduce search space for AI
|
||||
//
|
||||
|
||||
#ifndef EAGLE0_AICOMMANDFILTER_HPP
|
||||
#define EAGLE0_AICOMMANDFILTER_HPP
|
||||
#ifndef EAGLE0_SHARDOK_AI_AI_COMMAND_FILTER_HPP
|
||||
#define EAGLE0_SHARDOK_AI_AI_COMMAND_FILTER_HPP
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
@@ -116,4 +116,4 @@ private:
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
#endif // EAGLE0_AICOMMANDFILTER_HPP
|
||||
#endif // EAGLE0_SHARDOK_AI_AI_COMMAND_FILTER_HPP
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
// Common type definitions used across AI utility functions
|
||||
//
|
||||
|
||||
#ifndef EAGLE0_AICOMMONTYPES_HPP
|
||||
#define EAGLE0_AICOMMONTYPES_HPP
|
||||
#ifndef EAGLE0_SHARDOK_AI_AI_COMMON_TYPES_HPP
|
||||
#define EAGLE0_SHARDOK_AI_AI_COMMON_TYPES_HPP
|
||||
|
||||
#include <functional>
|
||||
|
||||
@@ -20,4 +20,4 @@ using BattalionTypeGetter = std::function<BattalionTypeSPtr(BattalionTypeId)>;
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
#endif // EAGLE0_AICOMMONTYPES_HPP
|
||||
#endif // EAGLE0_SHARDOK_AI_AI_COMMON_TYPES_HPP
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// AI System Types and Configuration
|
||||
//
|
||||
|
||||
#ifndef EAGLE0_AI_CONFIG_HPP
|
||||
#define EAGLE0_AI_CONFIG_HPP
|
||||
#ifndef EAGLE0_SHARDOK_AI_AI_CONFIG_HPP
|
||||
#define EAGLE0_SHARDOK_AI_AI_CONFIG_HPP
|
||||
|
||||
namespace shardok {
|
||||
|
||||
@@ -22,4 +22,4 @@ enum class ScoringCalculatorType {
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
#endif // EAGLE0_AI_CONFIG_HPP
|
||||
#endif // EAGLE0_SHARDOK_AI_AI_CONFIG_HPP
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// Created by Dan Crosby on 8/30/23.
|
||||
//
|
||||
|
||||
#ifndef EAGLE0_AIDEFENDERSTRATEGYSELECTOR_HPP
|
||||
#define EAGLE0_AIDEFENDERSTRATEGYSELECTOR_HPP
|
||||
#ifndef EAGLE0_SHARDOK_AI_AI_DEFENDER_STRATEGY_SELECTOR_HPP
|
||||
#define EAGLE0_SHARDOK_AI_AI_DEFENDER_STRATEGY_SELECTOR_HPP
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIAttackLocations.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AICommonTypes.hpp"
|
||||
@@ -26,4 +26,4 @@ public:
|
||||
};
|
||||
} // namespace shardok
|
||||
|
||||
#endif // EAGLE0_AIDEFENDERSTRATEGYSELECTOR_HPP
|
||||
#endif // EAGLE0_SHARDOK_AI_AI_DEFENDER_STRATEGY_SELECTOR_HPP
|
||||
|
||||
@@ -55,12 +55,12 @@ auto DefenderDistanceBuf(
|
||||
const bool includeUndead) -> double {
|
||||
const auto &locationsToAttackMe = alCache->CachedLocations(defenderLocation, lateGame);
|
||||
|
||||
struct WithoutAndWith {
|
||||
struct alignas(8) WithoutAndWith {
|
||||
int without;
|
||||
int with;
|
||||
|
||||
WithoutAndWith(const int a, const int b) : without(a), with(b) {}
|
||||
} __attribute__((aligned(8)));
|
||||
};
|
||||
|
||||
vector<WithoutAndWith> pointCosts{};
|
||||
pointCosts.reserve(attackerUnits.size());
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// Created by Dan Crosby on 5/10/21.
|
||||
//
|
||||
|
||||
#ifndef EAGLE0_AIDISTANCEDEBUF_HPP
|
||||
#define EAGLE0_AIDISTANCEDEBUF_HPP
|
||||
#ifndef EAGLE0_SHARDOK_AI_AI_DISTANCE_DEBUF_HPP
|
||||
#define EAGLE0_SHARDOK_AI_AI_DISTANCE_DEBUF_HPP
|
||||
|
||||
#include "AIAttackLocations.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AICommonTypes.hpp"
|
||||
@@ -13,14 +13,11 @@
|
||||
|
||||
namespace shardok {
|
||||
|
||||
using std::shared_ptr;
|
||||
using std::vector;
|
||||
|
||||
auto DefenderDistanceBuf(
|
||||
const Coords &defenderLocation,
|
||||
const HexMap *hexMap,
|
||||
const MapId &mapId,
|
||||
const vector<const Unit *> &attackerUnits,
|
||||
const std::vector<const Unit *> &attackerUnits,
|
||||
const APDCache &apdCache,
|
||||
const ALCache &alCache,
|
||||
const BattalionTypeGetter &battalionTypeGetter,
|
||||
@@ -29,4 +26,4 @@ auto DefenderDistanceBuf(
|
||||
bool includeUndead) -> double;
|
||||
|
||||
} // namespace shardok
|
||||
#endif // EAGLE0_AIDISTANCEDEBUF_HPP
|
||||
#endif // EAGLE0_SHARDOK_AI_AI_DISTANCE_DEBUF_HPP
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// Temporary AI experiment switches used by benchmark-only EXPERIMENTAL runs.
|
||||
//
|
||||
|
||||
#ifndef EAGLE0_AI_EXPERIMENT_CONFIG_HPP
|
||||
#define EAGLE0_AI_EXPERIMENT_CONFIG_HPP
|
||||
#ifndef EAGLE0_SHARDOK_AI_AI_EXPERIMENT_CONFIG_HPP
|
||||
#define EAGLE0_SHARDOK_AI_AI_EXPERIMENT_CONFIG_HPP
|
||||
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
@@ -20,4 +20,4 @@ inline auto IsAIExperiment(const int id) -> bool { return CurrentAIExperimentId(
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
#endif // EAGLE0_AI_EXPERIMENT_CONFIG_HPP
|
||||
#endif // EAGLE0_SHARDOK_AI_AI_EXPERIMENT_CONFIG_HPP
|
||||
|
||||
@@ -8,12 +8,29 @@
|
||||
|
||||
#include "AIFleeDecisionCalculator.hpp"
|
||||
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIScoreUtilities.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIUnitScoreCalculator.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
namespace {
|
||||
|
||||
void WritePercent(std::ostream& output, const double value) {
|
||||
const auto flags = output.flags();
|
||||
const auto precision = output.precision();
|
||||
|
||||
output << std::fixed << std::setprecision(1) << value;
|
||||
|
||||
output.flags(flags);
|
||||
output.precision(precision);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto AIFleeDecisionCalculator::GetFleeCommandIndex(
|
||||
const CommandList::const_iterator& fleeCommand,
|
||||
const CommandListSPtr& availableCommands) -> size_t {
|
||||
@@ -144,15 +161,14 @@ auto AIFleeDecisionCalculator::EvaluateFleeVsFight(
|
||||
const int fleeSuccessChance = (*fleeCommand)->GetOddsPercentile();
|
||||
|
||||
if (enableDebugLogging) {
|
||||
printf("AI FinalRound: Evaluating flee (odds=%d%%)...\n", fleeSuccessChance);
|
||||
std::cout << "AI FinalRound: Evaluating flee (odds=" << fleeSuccessChance << "%)...\n";
|
||||
}
|
||||
|
||||
// Check if flee odds are good enough to attempt
|
||||
if (fleeSuccessChance >= minimumFleeOddsThreshold) {
|
||||
if (enableDebugLogging) {
|
||||
printf("AI FinalRound: Good flee odds (%d%% >= %d%%), choosing flee\n",
|
||||
fleeSuccessChance,
|
||||
minimumFleeOddsThreshold);
|
||||
std::cout << "AI FinalRound: Good flee odds (" << fleeSuccessChance
|
||||
<< "% >= " << minimumFleeOddsThreshold << "%), choosing flee\n";
|
||||
}
|
||||
return FleeDecision{
|
||||
true,
|
||||
@@ -166,9 +182,9 @@ auto AIFleeDecisionCalculator::EvaluateFleeVsFight(
|
||||
// If combat situation is hopeless, even bad flee odds are better than certain death
|
||||
if (combatWinChance <= 0.05 && fleeSuccessChance >= desperateFleeThreshold) {
|
||||
if (enableDebugLogging) {
|
||||
printf("AI FinalRound: Combat hopeless (%.1f%%), desperate flee attempt (%d%%)\n",
|
||||
combatWinChance * 100,
|
||||
fleeSuccessChance);
|
||||
std::cout << "AI FinalRound: Combat hopeless (";
|
||||
WritePercent(std::cout, combatWinChance * 100);
|
||||
std::cout << "%), desperate flee attempt (" << fleeSuccessChance << "%)\n";
|
||||
}
|
||||
return FleeDecision{
|
||||
true,
|
||||
@@ -188,20 +204,21 @@ auto AIFleeDecisionCalculator::EvaluateFleeVsFight(
|
||||
const double adjustedCombatThreshold = combatWinChance * FLEE_VS_COMBAT_MARGIN;
|
||||
|
||||
if (enableDebugLogging) {
|
||||
printf("AI FinalRound: Flee=%d%%, Combat=%.1f%%, Threshold=%.1f%% -> ",
|
||||
fleeSuccessChance,
|
||||
combatWinChance * 100,
|
||||
adjustedCombatThreshold * 100);
|
||||
std::cout << "AI FinalRound: Flee=" << fleeSuccessChance << "%, Combat=";
|
||||
WritePercent(std::cout, combatWinChance * 100);
|
||||
std::cout << "%, Threshold=";
|
||||
WritePercent(std::cout, adjustedCombatThreshold * 100);
|
||||
std::cout << "% -> ";
|
||||
}
|
||||
|
||||
if (fleeChance > adjustedCombatThreshold) {
|
||||
if (enableDebugLogging) { printf("FLEE (better odds)\n"); }
|
||||
if (enableDebugLogging) { std::cout << "FLEE (better odds)\n"; }
|
||||
return FleeDecision{
|
||||
true,
|
||||
GetFleeCommandIndex(fleeCommand, availableCommands),
|
||||
"Flee has better expected outcome"};
|
||||
} else {
|
||||
if (enableDebugLogging) { printf("FIGHT (better expected outcome)\n"); }
|
||||
if (enableDebugLogging) { std::cout << "FIGHT (better expected outcome)\n"; }
|
||||
// Return 0 to indicate we should use standard command selection
|
||||
return FleeDecision{
|
||||
false,
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
// and flee vs fight evaluation for final round scenarios
|
||||
//
|
||||
|
||||
#ifndef AIFleeDecisionCalculator_hpp
|
||||
#define AIFleeDecisionCalculator_hpp
|
||||
#ifndef EAGLE0_SHARDOK_AI_AI_FLEE_DECISION_CALCULATOR_HPP
|
||||
#define EAGLE0_SHARDOK_AI_AI_FLEE_DECISION_CALCULATOR_HPP
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCommand.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
|
||||
@@ -63,4 +63,4 @@ private:
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
#endif /* AIFleeDecisionCalculator_hpp */
|
||||
#endif // EAGLE0_SHARDOK_AI_AI_FLEE_DECISION_CALCULATOR_HPP
|
||||
@@ -226,6 +226,7 @@ double AIHeuristicWeighting::GetCommandWeight(
|
||||
// === ZERO WEIGHT - NEVER SELECT (0.0) ===
|
||||
// Explicitly bad actions
|
||||
case CommandType::FLEE_COMMAND: return 0.0; // Never flee in simulation
|
||||
case CommandType::EVACUATE_PRISONERS_COMMAND: return 0.0;
|
||||
case CommandType::RETREAT_COMMAND: return 0.0;
|
||||
case CommandType::BECOME_OUTLAW_COMMAND: return 0.0; // Never become outlaw
|
||||
case CommandType::DISMISS_UNIT_COMMAND:
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
// Provides O(1) weights based on command type and context
|
||||
//
|
||||
|
||||
#ifndef EAGLE0_AI_HEURISTIC_WEIGHTING_HPP
|
||||
#define EAGLE0_AI_HEURISTIC_WEIGHTING_HPP
|
||||
#ifndef EAGLE0_SHARDOK_AI_AI_HEURISTIC_WEIGHTING_HPP
|
||||
#define EAGLE0_SHARDOK_AI_AI_HEURISTIC_WEIGHTING_HPP
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-redundant-constexpr-static-def"
|
||||
@@ -37,4 +37,4 @@ public:
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
#endif // EAGLE0_AI_HEURISTIC_WEIGHTING_HPP
|
||||
#endif // EAGLE0_SHARDOK_AI_AI_HEURISTIC_WEIGHTING_HPP
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user