mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-29 06:15:41 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
813109ee81 | ||
|
|
4162635431 | ||
|
|
d8e2e887c0 | ||
|
|
c6336979c8 | ||
|
|
a7f630d89c | ||
|
|
fe18ffe9a3 | ||
|
|
f7ee55b888 | ||
|
|
d94f731c50 | ||
|
|
791d0b9b29 | ||
|
|
80417fd3bf | ||
|
|
4c25b16cf1 | ||
|
|
6165bb695e | ||
|
|
65501b046e | ||
|
|
0419dc23fc | ||
|
|
4df0e75137 | ||
|
|
c35126c6d1 | ||
|
|
809c1e256a | ||
|
|
a7990f65aa | ||
|
|
39a374fc42 | ||
|
|
0f72494fef | ||
|
|
9dba9deaa1 | ||
|
|
6ff2a91417 | ||
|
|
3add561a82 | ||
|
|
a7c8cb11fc | ||
|
|
865d974a51 | ||
|
|
4898a1fe80 | ||
|
|
3b819db9ee | ||
|
|
154702ad35 | ||
|
|
98eb64749f | ||
|
|
067aadfec1 | ||
|
|
80d17ee750 | ||
|
|
4e59580d5e | ||
|
|
4950bd3609 | ||
|
|
f913116132 | ||
|
|
82fc200d51 | ||
|
|
af1e467cb8 | ||
|
|
c7d5f36ad5 | ||
|
|
78e5d25b89 | ||
|
|
f672126a00 | ||
|
|
4ff32ec0b2 | ||
|
|
ecc61c7d2c | ||
|
|
49185c103a | ||
|
|
71a6c9f791 | ||
|
|
6a91935a1f | ||
|
|
a148a8a2a3 | ||
|
|
a588c2dd9c | ||
|
|
6ae78186e2 | ||
|
|
a9e51e619d | ||
|
|
912f53c42d | ||
|
|
e371368e0b | ||
|
|
9fa1cb9e5d | ||
|
|
16a52d5557 | ||
|
|
2641be9c91 | ||
|
|
132cf36673 | ||
|
|
9b42eb90a0 | ||
|
|
e07b077b7f | ||
|
|
0766b63b9e | ||
|
|
46400dd6e1 | ||
|
|
85b2fe39aa | ||
|
|
5e9109a523 | ||
|
|
2785015212 | ||
|
|
e5be07b3b8 | ||
|
|
68873cc219 | ||
|
|
54283cbeb1 | ||
|
|
d30dd07c28 | ||
|
|
34c3b244fd | ||
|
|
07163dec15 | ||
|
|
c7c7167b6a | ||
|
|
e6bac582b1 | ||
|
|
e406229055 | ||
|
|
b0d1a8181b | ||
|
|
422abdb208 | ||
|
|
b3614f70b8 | ||
|
|
18bcc871e5 | ||
|
|
2b6d5e8258 | ||
|
|
62d8cc08ab | ||
|
|
3bb4cd84dc | ||
|
|
d17bb209a5 | ||
|
|
20bf47ab01 | ||
|
|
741bc690fb | ||
|
|
bedc104d0b | ||
|
|
6410ad2eef | ||
|
|
8164e91125 | ||
|
|
70f40aea25 | ||
|
|
aec99378ad | ||
|
|
cd2e00cd47 | ||
|
|
504374291d | ||
|
|
6b052bf3ad | ||
|
|
0e1536842e | ||
|
|
c6b73d24be | ||
|
|
ccde86cb81 | ||
|
|
8d54d80f4d | ||
|
|
5c1d68960b | ||
|
|
4a33d4b30f | ||
|
|
5d21ee0915 | ||
|
|
ce82c952c5 | ||
|
|
3b5deebb92 | ||
|
|
027c5b6598 | ||
|
|
8a29847cc4 | ||
|
|
33f4c41fdd | ||
|
|
f7d79639b5 | ||
|
|
da0d1c04fd | ||
|
|
0c0ff85513 | ||
|
|
c2c18d8a07 | ||
|
|
5e7223aa8e | ||
|
|
14a475032e | ||
|
|
3dce099b2a | ||
|
|
009a4ffe53 | ||
|
|
7781bbcc46 | ||
|
|
e6438302e2 | ||
|
|
a919e8edf0 | ||
|
|
a59b732b04 | ||
|
|
077e0d2bc3 | ||
|
|
3ad7c3e567 | ||
|
|
62fd16cec5 | ||
|
|
89d1d15979 | ||
|
|
1bea610232 | ||
|
|
8cce072614 | ||
|
|
fb74935ad9 | ||
|
|
36915b8184 | ||
|
|
e07cdc8123 | ||
|
|
fe4e205a07 | ||
|
|
4506ea2672 | ||
|
|
ba69263f06 | ||
|
|
2a660f05d7 | ||
|
|
4dd30a9863 | ||
|
|
1f18f3296d | ||
|
|
467d2e2c82 | ||
|
|
73b8087e07 | ||
|
|
76be4a49ea | ||
|
|
27aae790ad | ||
|
|
fdf7f2a88d | ||
|
|
f1cf03e49e | ||
|
|
5213fb4805 | ||
|
|
a884b60991 | ||
|
|
1fdc679dfd | ||
|
|
afabe092c2 | ||
|
|
3624279c4e | ||
|
|
eca0289a43 | ||
|
|
90bedd4be5 | ||
|
|
2231cf1b9b | ||
|
|
30a68bdca0 | ||
|
|
4f69ea79b4 | ||
|
|
665d2291ef | ||
|
|
a6ab25fcee | ||
|
|
e2a82f7225 | ||
|
|
5e5244ead4 | ||
|
|
74d3af589e | ||
|
|
a6641d5eb1 | ||
|
|
3ecb665709 | ||
|
|
9b033230c9 | ||
|
|
bc813b8659 | ||
|
|
0fe61c71c9 | ||
|
|
315c1daf82 | ||
|
|
45abe81540 | ||
|
|
8141770268 | ||
|
|
fa7775a023 | ||
|
|
b3bf291a7d | ||
|
|
557669546b | ||
|
|
42958cb34e | ||
|
|
91f02da9cf | ||
|
|
8d8b55e2c4 | ||
|
|
947cedc0f0 | ||
|
|
4e5206220c | ||
|
|
acd299c346 | ||
|
|
74b816d3f4 | ||
|
|
4abdefd40d | ||
|
|
08f7e4ebfc | ||
|
|
afbf4c226c | ||
|
|
0afa842efe | ||
|
|
e78ae6ad75 | ||
|
|
9631435dea | ||
|
|
5ae910b19b | ||
|
|
dc9b1171fb | ||
|
|
508eb64bfb | ||
|
|
9aa7be4620 | ||
|
|
f3d887e370 | ||
|
|
ce4ed6a305 | ||
|
|
2b5e4aa96e | ||
|
|
41b9f5526c | ||
|
|
6b24776e0a | ||
|
|
b1b44ebb6c | ||
|
|
45a2d50b57 | ||
|
|
84f329ab80 | ||
|
|
fcf9246562 | ||
|
|
0d361f5ad7 | ||
|
|
217d495747 | ||
|
|
ca80ad0057 |
@@ -14,6 +14,9 @@ common --worker_sandboxing
|
||||
common --local_test_jobs=64
|
||||
common --jobs=64
|
||||
|
||||
# Rebuild remote-only intermediate inputs if the remote cache loses them.
|
||||
common --rewind_lost_inputs
|
||||
|
||||
# Keep these C++ standard flags in sync with tools/copts.bzl and scripts/shardok_cpp_config.sh.
|
||||
common --cxxopt="--std=c++23"
|
||||
common --cxxopt="-Wno-deprecated-non-prototype"
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
name: Gitea Offsite Backup
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
full_restore_verify:
|
||||
description: 'Download and validate a complete restored snapshot'
|
||||
required: true
|
||||
default: false
|
||||
type: boolean
|
||||
maintenance:
|
||||
description: 'Apply retention and prune unreferenced Restic packs'
|
||||
required: true
|
||||
default: false
|
||||
type: boolean
|
||||
schedule:
|
||||
- cron: '23 10 * * *'
|
||||
- cron: '53 11 * * 0'
|
||||
|
||||
concurrency:
|
||||
group: gitea-offsite-backup
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
backup:
|
||||
if: (github.event_name == 'workflow_dispatch' && inputs.maintenance == false) || (github.event_name == 'schedule' && github.event.schedule == '23 10 * * *')
|
||||
runs-on: [self-hosted, macOS, ARM64, halfdan]
|
||||
timeout-minutes: 240
|
||||
|
||||
steps:
|
||||
- name: Prepare non-LFS checkout
|
||||
env:
|
||||
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig-no-lfs
|
||||
GIT_CONFIG_NOSYSTEM: '1'
|
||||
run: |
|
||||
git config --global --unset-all filter.lfs.process || true
|
||||
git config --global filter.lfs.smudge "cat"
|
||||
git config --global filter.lfs.clean "cat"
|
||||
git config --global filter.lfs.required false
|
||||
if [ -d ".git" ]; then
|
||||
git config --local --unset-all filter.lfs.process || true
|
||||
git config --local filter.lfs.smudge "cat"
|
||||
git config --local filter.lfs.clean "cat"
|
||||
git config --local filter.lfs.required false
|
||||
fi
|
||||
rm -f .git/hooks/post-checkout .git/hooks/post-merge .git/hooks/pre-push
|
||||
|
||||
- uses: actions/checkout@v7
|
||||
env:
|
||||
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig-no-lfs
|
||||
GIT_CONFIG_NOSYSTEM: '1'
|
||||
with:
|
||||
persist-credentials: false
|
||||
lfs: false
|
||||
clean: false
|
||||
|
||||
- name: Back up Gitea
|
||||
env:
|
||||
DO_SPACES_ACCESS_KEY: ${{ secrets.GITEA_DO_SPACES_ACCESS_KEY }}
|
||||
DO_SPACES_ENDPOINT: ${{ secrets.DO_SPACES_ENDPOINT }}
|
||||
DO_SPACES_SECRET_KEY: ${{ secrets.GITEA_DO_SPACES_SECRET_KEY }}
|
||||
FULL_RESTORE_VERIFY: ${{ inputs.full_restore_verify }}
|
||||
GITEA_BACKUP_SSH_KEY: ${{ secrets.GITEA_BACKUP_SSH_KEY }}
|
||||
RESTIC_PASSWORD: ${{ secrets.GITEA_RESTIC_PASSWORD }}
|
||||
run: ./ci/github_actions/backup_gitea.sh backup
|
||||
|
||||
maintenance:
|
||||
if: (github.event_name == 'schedule' && github.event.schedule == '53 11 * * 0') || (github.event_name == 'workflow_dispatch' && inputs.maintenance)
|
||||
runs-on: [self-hosted, macOS, ARM64, halfdan]
|
||||
timeout-minutes: 240
|
||||
|
||||
steps:
|
||||
- name: Prepare non-LFS checkout
|
||||
env:
|
||||
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig-no-lfs
|
||||
GIT_CONFIG_NOSYSTEM: '1'
|
||||
run: |
|
||||
git config --global --unset-all filter.lfs.process || true
|
||||
git config --global filter.lfs.smudge "cat"
|
||||
git config --global filter.lfs.clean "cat"
|
||||
git config --global filter.lfs.required false
|
||||
|
||||
- uses: actions/checkout@v7
|
||||
env:
|
||||
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig-no-lfs
|
||||
GIT_CONFIG_NOSYSTEM: '1'
|
||||
with:
|
||||
persist-credentials: false
|
||||
lfs: false
|
||||
clean: false
|
||||
|
||||
- name: Apply retention and verify repository
|
||||
env:
|
||||
DO_SPACES_ACCESS_KEY: ${{ secrets.GITEA_DO_SPACES_ACCESS_KEY }}
|
||||
DO_SPACES_ENDPOINT: ${{ secrets.DO_SPACES_ENDPOINT }}
|
||||
DO_SPACES_SECRET_KEY: ${{ secrets.GITEA_DO_SPACES_SECRET_KEY }}
|
||||
GITEA_BACKUP_SSH_KEY: ${{ secrets.GITEA_BACKUP_SSH_KEY }}
|
||||
RESTIC_PASSWORD: ${{ secrets.GITEA_RESTIC_PASSWORD }}
|
||||
run: ./ci/github_actions/backup_gitea.sh maintenance
|
||||
@@ -166,7 +166,7 @@ jobs:
|
||||
run: ./ci/github_actions/ensure_unity_installed.sh mac
|
||||
|
||||
- name: Ensure .NET SDK installed
|
||||
uses: actions/setup-dotnet@v5
|
||||
uses: actions/setup-dotnet@v6
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
|
||||
@@ -214,7 +214,7 @@ jobs:
|
||||
|
||||
- name: Set up Go
|
||||
if: matrix.artifact == 'go'
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: false
|
||||
|
||||
@@ -10,6 +10,8 @@ on:
|
||||
- 'src/main/go/net/eagle0/build/**'
|
||||
- 'src/main/resources/net/eagle0/shardok/**'
|
||||
- 'ci/BUILD.bazel'
|
||||
- 'ci/shardok_perf.lock.json'
|
||||
- 'ci/shardok_perf.yaml'
|
||||
- 'ci/github_actions/ensure_bazel_installed.sh'
|
||||
- '.github/actions/setup-bazel/**'
|
||||
- 'go.mod'
|
||||
@@ -68,12 +70,18 @@ jobs:
|
||||
- name: Ensure Bazel installed
|
||||
uses: ./.github/actions/setup-bazel
|
||||
|
||||
- name: Build Shardok ARM64 binary (cross-compile for Linux ARM64)
|
||||
- name: Build optimized, profileable Shardok ARM64 binary
|
||||
run: |
|
||||
set -ex
|
||||
|
||||
echo "=== Building shardok-server binary for linux-aarch64 ==="
|
||||
bazel build \
|
||||
-c opt \
|
||||
--strip=never \
|
||||
--copt=-gline-tables-only \
|
||||
--copt=-fno-omit-frame-pointer \
|
||||
--copt=-mno-omit-leaf-frame-pointer \
|
||||
--linkopt=-Wl,--build-id=sha1 \
|
||||
--platforms=//:linux_arm64 \
|
||||
--extra_toolchains=@llvm_toolchain_linux_arm64//:cc-toolchain-aarch64-linux \
|
||||
//src/main/cpp/net/eagle0/shardok:shardok-server
|
||||
@@ -103,6 +111,15 @@ jobs:
|
||||
else
|
||||
echo "WARNING: Binary e_machine is $E_MACHINE (expected 00b7 for aarch64)"
|
||||
fi
|
||||
|
||||
BINARY_DESCRIPTION=$(file "$LINUX_BIN")
|
||||
echo "Binary description: $BINARY_DESCRIPTION"
|
||||
for REQUIRED_TEXT in "ARM aarch64" "BuildID" "with debug_info" "not stripped"; do
|
||||
if [[ "$BINARY_DESCRIPTION" != *"$REQUIRED_TEXT"* ]]; then
|
||||
echo "ERROR: Profileable release binary is missing '$REQUIRED_TEXT'"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
elif [ "$MAGIC" = "cfaeedfe" ] || [ "$MAGIC" = "cffaedfe" ]; then
|
||||
echo "ERROR: Binary is Mach-O format (macOS) - cross-compilation failed!"
|
||||
exit 1
|
||||
@@ -111,13 +128,19 @@ jobs:
|
||||
file "$LINUX_BIN" || true
|
||||
fi
|
||||
|
||||
- name: Build Shardok ARM64 Docker image
|
||||
- name: Build optimized, profileable Shardok ARM64 Docker image
|
||||
id: build-shardok
|
||||
run: |
|
||||
set -ex
|
||||
|
||||
# Keep this in sync with the standalone binary build above.
|
||||
bazel build \
|
||||
-c opt \
|
||||
--strip=never \
|
||||
--copt=-gline-tables-only \
|
||||
--copt=-fno-omit-frame-pointer \
|
||||
--copt=-mno-omit-leaf-frame-pointer \
|
||||
--linkopt=-Wl,--build-id=sha1 \
|
||||
--platforms=//:linux_arm64 \
|
||||
--extra_toolchains=@llvm_toolchain_linux_arm64//:cc-toolchain-aarch64-linux \
|
||||
//ci:shardok_server_image_arm64
|
||||
@@ -262,6 +285,12 @@ jobs:
|
||||
echo "Loaded image ID: $(docker image inspect --format '{{.Id}}' "$SHARDOK_IMAGE")"
|
||||
echo "Shardok binary SHA256:"
|
||||
docker run --rm --entrypoint sha256sum "$SHARDOK_IMAGE" /app/shardok-server
|
||||
echo "Verifying perf capture permissions:"
|
||||
docker run --rm \
|
||||
--cap-add PERFMON \
|
||||
--entrypoint perf \
|
||||
"$SHARDOK_IMAGE" \
|
||||
stat --event cpu-clock:u -- perf version
|
||||
|
||||
# Stop and remove any container using port 40042 or named shardok*
|
||||
docker ps -q --filter "publish=40042" | xargs -r docker stop
|
||||
@@ -272,6 +301,7 @@ jobs:
|
||||
docker run -d \
|
||||
--name shardok-ai \
|
||||
--restart unless-stopped \
|
||||
--cap-add PERFMON \
|
||||
-p 40042:40042 \
|
||||
-v /opt/eagle0/data:/data \
|
||||
-v /etc/shardok:/etc/shardok:ro \
|
||||
|
||||
@@ -153,7 +153,7 @@ jobs:
|
||||
- name: Ensure Unity version installed
|
||||
run: ./ci/github_actions/ensure_unity_installed.sh windows
|
||||
- name: Ensure .NET SDK installed
|
||||
uses: actions/setup-dotnet@v5
|
||||
uses: actions/setup-dotnet@v6
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
- name: Ensure Bazel installed
|
||||
@@ -270,7 +270,7 @@ jobs:
|
||||
- name: Ensure Unity version installed
|
||||
run: ./ci/github_actions/ensure_unity_installed.sh windows
|
||||
- name: Ensure .NET SDK installed
|
||||
uses: actions/setup-dotnet@v5
|
||||
uses: actions/setup-dotnet@v6
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
- name: Ensure Bazel installed
|
||||
|
||||
+25
-16
@@ -17,9 +17,9 @@ SLF4J_VERSION = "2.0.18"
|
||||
# Core Build Tools
|
||||
#
|
||||
|
||||
bazel_dep(name = "bazel_skylib", version = "1.9.0")
|
||||
bazel_dep(name = "bazel_skylib", version = "1.9.2")
|
||||
bazel_dep(name = "aspect_rules_esbuild", version = "0.26.0")
|
||||
bazel_dep(name = "aspect_rules_js", version = "3.2.2")
|
||||
bazel_dep(name = "aspect_rules_js", version = "3.3.0")
|
||||
bazel_dep(name = "bazel_jar_jar", version = "0.1.15", repo_name = "bazel_jar_jar")
|
||||
bazel_dep(name = "platforms", version = "1.1.0")
|
||||
bazel_dep(name = "rules_dotnet", version = "0.22.1")
|
||||
@@ -33,7 +33,7 @@ multiple_version_override(
|
||||
module_name = "aspect_rules_js",
|
||||
versions = [
|
||||
"2.3.8",
|
||||
"3.2.2",
|
||||
"3.3.0",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -142,8 +142,8 @@ sysroot(
|
||||
# Language Support - Go
|
||||
#
|
||||
|
||||
bazel_dep(name = "rules_go", version = "0.61.1", repo_name = "io_bazel_rules_go")
|
||||
bazel_dep(name = "gazelle", version = "0.51.3", repo_name = "bazel_gazelle")
|
||||
bazel_dep(name = "rules_go", version = "0.62.0", repo_name = "io_bazel_rules_go")
|
||||
bazel_dep(name = "gazelle", version = "0.52.2", repo_name = "bazel_gazelle")
|
||||
|
||||
go_sdk = use_extension("@io_bazel_rules_go//go:extensions.bzl", "go_sdk")
|
||||
go_sdk.download(version = "1.25.11")
|
||||
@@ -169,7 +169,7 @@ use_repo(
|
||||
# Language Support - Rust
|
||||
#
|
||||
|
||||
bazel_dep(name = "rules_rust", version = "0.71.3")
|
||||
bazel_dep(name = "rules_rust", version = "0.72.0")
|
||||
|
||||
rust = use_extension("@rules_rust//rust:extensions.bzl", "rust")
|
||||
rust.toolchain(edition = "2021")
|
||||
@@ -189,7 +189,7 @@ use_repo(crate, "map_generator_crates")
|
||||
# Platform Support - Apple/iOS
|
||||
#
|
||||
|
||||
bazel_dep(name = "apple_support", version = "2.7.0", repo_name = "build_bazel_apple_support")
|
||||
bazel_dep(name = "apple_support", version = "2.8.0", repo_name = "build_bazel_apple_support")
|
||||
bazel_dep(name = "rules_apple", version = "4.5.3", repo_name = "build_bazel_rules_apple")
|
||||
|
||||
multiple_version_override(
|
||||
@@ -237,9 +237,9 @@ use_repo(
|
||||
|
||||
bazel_dep(name = "rules_proto", version = "7.1.0")
|
||||
bazel_dep(name = "rules_cc", version = "0.2.22")
|
||||
bazel_dep(name = "grpc", version = "1.82.0")
|
||||
bazel_dep(name = "grpc", version = "1.83.0")
|
||||
|
||||
# gRPC 1.81.1's generated upb module map trips Clang's layering check in the
|
||||
# gRPC's generated upb module map trips Clang's layering check in the
|
||||
# Linux ARM64 cross-toolchain even though the normal Bazel deps are present.
|
||||
single_version_override(
|
||||
module_name = "grpc",
|
||||
@@ -263,16 +263,25 @@ bazel_dep(name = "googletest", version = "1.17.0.bcr.2")
|
||||
|
||||
bazel_dep(name = "rules_oci", version = "2.3.0")
|
||||
bazel_dep(name = "aspect_bazel_lib", version = "2.22.5")
|
||||
bazel_dep(name = "rules_distroless", version = "0.8.0")
|
||||
|
||||
oci = use_extension("@rules_oci//oci:extensions.bzl", "oci")
|
||||
|
||||
apt = use_extension("@rules_distroless//apt:extensions.bzl", "apt")
|
||||
apt.install(
|
||||
lock = "//ci:shardok_perf.lock.json",
|
||||
name = "shardok_perf",
|
||||
manifest = "//ci:shardok_perf.yaml",
|
||||
)
|
||||
use_repo(apt, "shardok_perf")
|
||||
|
||||
# Base image for Eagle (Java 25 JRE - smaller runtime, no dev tools needed)
|
||||
# Digest pinned for Bazel repository cache hit; update with:
|
||||
# TOKEN=$(curl -s "https://auth.docker.io/token?service=registry.docker.io&scope=repository:library/eclipse-temurin:pull" | python3 -c "import json,sys; print(json.load(sys.stdin)['token'])")
|
||||
# curl -s -H "Authorization: Bearer $TOKEN" -H "Accept: application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.list.v2+json" "https://registry-1.docker.io/v2/library/eclipse-temurin/manifests/25-jre" | python3 -c "import json,sys; m=json.load(sys.stdin); [print(p['digest']) for p in m.get('manifests',[]) if p['platform']['architecture']=='amd64']"
|
||||
oci.pull(
|
||||
name = "eclipse_temurin_25_jre",
|
||||
digest = "sha256:a0053c48b1a74412b14a111d6fd547799f8a6a5c77e2318f698b996ec899b0ce",
|
||||
digest = "sha256:882787e465d6c0c4c08daa8d9e209d57ec052228eff8244c959f271af24ad27b",
|
||||
image = "docker.io/library/eclipse-temurin",
|
||||
platforms = ["linux/amd64"],
|
||||
tag = "26-jre",
|
||||
@@ -281,7 +290,7 @@ oci.pull(
|
||||
# Base image for JFR sidecar (Java 25 JDK - includes jcmd for JFR dumps)
|
||||
oci.pull(
|
||||
name = "eclipse_temurin_25_jdk",
|
||||
digest = "sha256:8e0469892385b62feab689f211fde7eeeb6537418c8fbe613147427b53870ec3",
|
||||
digest = "sha256:625889835aab66a096f999ee16cd7061ac846b155dc7adbed693686bbae5c59a",
|
||||
image = "docker.io/library/eclipse-temurin",
|
||||
platforms = ["linux/amd64"],
|
||||
tag = "26-jdk",
|
||||
@@ -312,8 +321,8 @@ use_repo(oci, "alpine_linux", "alpine_linux_linux_amd64", "eclipse_temurin_25_jd
|
||||
# Java/Scala Dependencies
|
||||
#
|
||||
|
||||
bazel_dep(name = "rules_java", version = "9.6.1")
|
||||
bazel_dep(name = "rules_jvm_external", version = "7.0")
|
||||
bazel_dep(name = "rules_java", version = "9.7.0")
|
||||
bazel_dep(name = "rules_jvm_external", version = "7.1")
|
||||
|
||||
maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")
|
||||
maven.install(
|
||||
@@ -347,7 +356,7 @@ maven.install(
|
||||
"org.scalamock:scalamock_3:7.5.5",
|
||||
|
||||
# Developer tools
|
||||
"org.scalameta:scalafmt-cli_2.13:3.11.3",
|
||||
"org.scalameta:scalafmt-cli_2.13:3.11.5",
|
||||
|
||||
# AWS SDK
|
||||
"software.amazon.awssdk:s3-transfer-manager:%s" % AWS_SDK_VERSION,
|
||||
@@ -378,10 +387,10 @@ maven.install(
|
||||
"com.nimbusds:nimbus-jose-jwt:10.9.1",
|
||||
|
||||
# Error tracking
|
||||
"io.sentry:sentry:8.48.0",
|
||||
"io.sentry:sentry:8.50.1",
|
||||
|
||||
# SQLite for client text storage
|
||||
"org.xerial:sqlite-jdbc:3.53.2.0",
|
||||
"org.xerial:sqlite-jdbc:3.53.2.1",
|
||||
|
||||
# Postgres for production history benchmarking and migration work
|
||||
"com.zaxxer:HikariCP:7.1.0",
|
||||
|
||||
Generated
+66
-215
@@ -34,8 +34,8 @@
|
||||
"https://bcr.bazel.build/modules/apple_support/1.24.2/MODULE.bazel": "0e62471818affb9f0b26f128831d5c40b074d32e6dda5a0d3852847215a41ca4",
|
||||
"https://bcr.bazel.build/modules/apple_support/2.2.0/MODULE.bazel": "5e32bc42e413a4544df7afee7fa4c7aff73e670a44e56e8e45ce1954332034ad",
|
||||
"https://bcr.bazel.build/modules/apple_support/2.3.0/MODULE.bazel": "d48f824ae8eeea5f837eb3038cef3615075d996d3e82eb9187192c2820605a81",
|
||||
"https://bcr.bazel.build/modules/apple_support/2.7.0/MODULE.bazel": "02eadc24a8fbfd61754002c322480487d03d8ee4490631c21414098cbc47c0f9",
|
||||
"https://bcr.bazel.build/modules/apple_support/2.7.0/source.json": "63b9d43a4456a6cd1634ed97b4e5e0bc09b3ed10db5a09acd5e355c021239422",
|
||||
"https://bcr.bazel.build/modules/apple_support/2.8.0/MODULE.bazel": "c45f5176057092afba0be6c48a9ea02101666802f8be536947e099e94757f8ed",
|
||||
"https://bcr.bazel.build/modules/apple_support/2.8.0/source.json": "b7f6d87669c206adaa7d82cc44733349c63e341b80a0e049496ebd91d78c3472",
|
||||
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.11.0/MODULE.bazel": "cb1ba9f9999ed0bc08600c221f532c1ddd8d217686b32ba7d45b0713b5131452",
|
||||
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.14.0/MODULE.bazel": "2b31ffcc9bdc8295b2167e07a757dbbc9ac8906e7028e5170a3708cecaac119f",
|
||||
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.19.3/MODULE.bazel": "253d739ba126f62a5767d832765b12b59e9f8d2bc88cc1572f4a73e46eb298ca",
|
||||
@@ -53,12 +53,13 @@
|
||||
"https://bcr.bazel.build/modules/aspect_rules_js/2.3.8/MODULE.bazel": "74bf20a7a6bd5f2be09607fdb4196cfd6f203422ea271752ec2b1afe95426101",
|
||||
"https://bcr.bazel.build/modules/aspect_rules_js/2.3.8/source.json": "411ec9d79d6f5fe8a083359588c21d01a5b48d88a2cbd334a4c90365015b7836",
|
||||
"https://bcr.bazel.build/modules/aspect_rules_js/3.0.3/MODULE.bazel": "28a30e8fc33bf64a67835d64d124f6e05a7d59648dcb27b110fb3502f761e503",
|
||||
"https://bcr.bazel.build/modules/aspect_rules_js/3.2.2/MODULE.bazel": "e844196321b64537cfade338f361633e7d5fe084eee3214fc531b9e7b9838813",
|
||||
"https://bcr.bazel.build/modules/aspect_rules_js/3.2.2/source.json": "0e650e493b3bc784dc3f8240ff00b4b32258cde6d021ef6b1db4e387bcfd6a9a",
|
||||
"https://bcr.bazel.build/modules/aspect_rules_js/3.3.0/MODULE.bazel": "42ef593509e813482063e09f05be1f8690722dc8bc39f1f4ae4c0944aa7e33da",
|
||||
"https://bcr.bazel.build/modules/aspect_rules_js/3.3.0/source.json": "7503ad32423dc3d293f8563e2faa026cb850ca9f45b368b6959ee2fdf359ac28",
|
||||
"https://bcr.bazel.build/modules/aspect_rules_ts/3.6.0/MODULE.bazel": "d0045b5eabb012be550a609589b3e5e47eba682344b19cfd9365d4d896ed07df",
|
||||
"https://bcr.bazel.build/modules/aspect_rules_ts/3.6.0/source.json": "5593e3f1cd0dd5147f7748e163307fd5c2e1077913d6945b58739ad8d770a290",
|
||||
"https://bcr.bazel.build/modules/aspect_tools_telemetry/0.3.3/MODULE.bazel": "37c764292861c2f70314efa9846bb6dbb44fc0308903b3285da6528305450183",
|
||||
"https://bcr.bazel.build/modules/aspect_tools_telemetry/0.3.3/source.json": "605086bbc197743a0d360f7ddc550a1d4dfa0441bc807236e17170f636153348",
|
||||
"https://bcr.bazel.build/modules/aspect_tools_telemetry/0.4.2/MODULE.bazel": "f31aa84151d31e98cffd43eb7217ccff5ec52bdd5f2d10db8f053aeb23342eca",
|
||||
"https://bcr.bazel.build/modules/aspect_tools_telemetry/0.4.2/source.json": "d027d264e6b6e7fc421e38189f4374fcd14a67e0bd6e0e705de8c2185c3787e1",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.1.0/MODULE.bazel": "cfd42ff3b815a5f39554d97182657f8c4b9719568eb7fded2b9135f084bf760b",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.1.1/MODULE.bazel": "27b8c79ef57efe08efccbd9dd6ef70d61b4798320b8d3c134fd571f78963dbcd",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.10.0/MODULE.bazel": "f75e8807570484a99be90abcd52b5e1f390362c258bcb73106f4544957a48101",
|
||||
@@ -68,6 +69,7 @@
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.17.0/MODULE.bazel": "039de32d21b816b47bd42c778e0454217e9c9caac4a3cf8e15c7231ee3ddee4d",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.18.0/MODULE.bazel": "1be0ae2557ab3a72a57aeb31b29be347bcdc5d2b1eb1e70f39e3851a7e97041a",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.19.0/MODULE.bazel": "59adcdf28230d220f0067b1f435b8537dd033bfff8db21335ef9217919c7fb58",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.20.0/MODULE.bazel": "8b85300b9c8594752e0721a37210e34879d23adc219ed9dc8f4104a4a1750920",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.21.0/MODULE.bazel": "675642261665d8eea09989aa3b8afb5c37627f1be178382c320d1b46afba5e3b",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.25.0/MODULE.bazel": "e2e60a10a6da64bbf533f15ca652bf61a033e41c2ed734d79a9a08ba87f68c1a",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.27.0/MODULE.bazel": "621eeee06c4458a9121d1f104efb80f39d34deff4984e778359c60eaf1a8cb65",
|
||||
@@ -83,7 +85,6 @@
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.4.1/MODULE.bazel": "e45b6bb2350aff3e442ae1111c555e27eac1d915e77775f6fdc4b351b758b5d7",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.41.0/MODULE.bazel": "6e0f87fafed801273c371d41e22a15a6f8abf83fdd7f87d5e44ad317b94433d0",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.42.1/MODULE.bazel": "275a59b5406ff18c01739860aa70ad7ccb3cfb474579411decca11c93b951080",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.47.0/MODULE.bazel": "e34df3cb35b1684cfa69923a61ae3803595babd3942cd306a488d51400886b30",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.50.0/MODULE.bazel": "2083ef9c7a469f520890483ccf8e0189d6e71e2117e7752e15e6554433d5ae3e",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.50.0/source.json": "e0ee3debde2789ff56e4452e612d126925ba9ab64d4bde79c67f099d2902df9b",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.9.0/MODULE.bazel": "885151d58d90d8d9c811eb75e3288c11f850e1d6b481a8c9f766adee4712358b",
|
||||
@@ -91,11 +92,13 @@
|
||||
"https://bcr.bazel.build/modules/bazel_jar_jar/0.1.11.bcr.1/MODULE.bazel": "bb319e2fbb19e5223e5d4235f56fc46674b7b7c765cde3096ca80e7faafb539d",
|
||||
"https://bcr.bazel.build/modules/bazel_jar_jar/0.1.15/MODULE.bazel": "422c026647ead49d81271fe55cfe6fe4a4c90011d1ae0b747efdd7e0b30ec367",
|
||||
"https://bcr.bazel.build/modules/bazel_jar_jar/0.1.15/source.json": "9ce5c5bc2b5fe84f66757cf418325fd59948dc0e98f3c9ba59d2efb1b5b31150",
|
||||
"https://bcr.bazel.build/modules/bazel_lib/3.0.0-beta.1/MODULE.bazel": "407729e232f611c3270005b016b437005daa7b1505826798ea584169a476e878",
|
||||
"https://bcr.bazel.build/modules/bazel_lib/3.0.0-rc.0/MODULE.bazel": "d6e00979a98ac14ada5e31c8794708b41434d461e7e7ca39b59b765e6d233b18",
|
||||
"https://bcr.bazel.build/modules/bazel_lib/3.0.0/MODULE.bazel": "22b70b80ac89ad3f3772526cd9feee2fa412c2b01933fea7ed13238a448d370d",
|
||||
"https://bcr.bazel.build/modules/bazel_lib/3.1.0/MODULE.bazel": "6809765c14e3c766a9b9286c7b0ec56ed87a73326e48fe01749f0c0fdcfe3287",
|
||||
"https://bcr.bazel.build/modules/bazel_lib/3.2.2/MODULE.bazel": "e2c890c8a515d6bca9c66d47718aa9e44b458fde64ec7204b8030bf2d349058c",
|
||||
"https://bcr.bazel.build/modules/bazel_lib/3.2.2/source.json": "9e84e115c20e14652c5c21401ae85ff4daa8702e265b5c0b3bf89353f17aa212",
|
||||
"https://bcr.bazel.build/modules/bazel_lib/3.5.0/MODULE.bazel": "1eba0ba67d418d99fb682c36147a9e1725abbc8fc25544be64702e32eda2be29",
|
||||
"https://bcr.bazel.build/modules/bazel_lib/3.5.0/source.json": "98d49ec7e2b15298c990f09ec8cdea8d4788c2885497aa9cbb0a74e36e994df8",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.0.3/MODULE.bazel": "bcb0fd896384802d1ad283b4e4eb4d718eebd8cb820b0a2c3a347fb971afd9d8",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.1.1/MODULE.bazel": "1add3e7d93ff2e6998f9e118022c84d163917d912f5afafb3058e3d2f1545b5e",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.2.0/MODULE.bazel": "44fe84260e454ed94ad326352a698422dbe372b21a1ac9f3eab76eb531223686",
|
||||
@@ -110,7 +113,8 @@
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.8.1/MODULE.bazel": "88ade7293becda963e0e3ea33e7d54d3425127e0a326e0d17da085a5f1f03ff6",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.8.2/MODULE.bazel": "69ad6927098316848b34a9142bcc975e018ba27f08c4ff403f50c1b6e646ca67",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.9.0/MODULE.bazel": "72997b29dfd95c3fa0d0c48322d05590418edef451f8db8db5509c57875fb4b7",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.9.0/source.json": "7ad77c1e8c1b84222d9b3f3cae016a76639435744c19330b0b37c0a3c9da7dc0",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.9.2/MODULE.bazel": "8c51259b0f4481475586dbfede7591e57b75702e637f840f6138eec80c34b270",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.9.2/source.json": "41cbde7546542dee2f26e3f10bf1c4ac57909943194b7ac48cc5238a01893aa8",
|
||||
"https://bcr.bazel.build/modules/bazel_worker_api/0.0.1/MODULE.bazel": "02a13b77321773b2042e70ee5e4c5e099c8ddee4cf2da9cd420442c36938d4bd",
|
||||
"https://bcr.bazel.build/modules/bazel_worker_api/0.0.10/MODULE.bazel": "a426df551b40c3997c351d05f00f0d1f86b618ddb646012f1e9c72efce8ed939",
|
||||
"https://bcr.bazel.build/modules/bazel_worker_api/0.0.11/MODULE.bazel": "fe09a8d3fb25c95414249f72cb0936201bf7e3c3f0e302175b2fc81e68bc6069",
|
||||
@@ -169,7 +173,8 @@
|
||||
"https://bcr.bazel.build/modules/gazelle/0.46.0/MODULE.bazel": "3dec215dacf2427df87b524a2c99da387882a18d753f0b1b38675992bd0a99c6",
|
||||
"https://bcr.bazel.build/modules/gazelle/0.47.0/MODULE.bazel": "b61bb007c4efad134aa30ee7f4a8e2a39b22aa5685f005edaa022fbd1de43ebc",
|
||||
"https://bcr.bazel.build/modules/gazelle/0.51.3/MODULE.bazel": "618a729142f66de1e2cb776d026413763be5b80d5e3d29ffb9d3d90c5defde90",
|
||||
"https://bcr.bazel.build/modules/gazelle/0.51.3/source.json": "fbe5312a01fb4a2a58caff4a0d40dcf384b6f0bda75e85546663360da1d7538a",
|
||||
"https://bcr.bazel.build/modules/gazelle/0.52.2/MODULE.bazel": "9eefee4f094bb97f16287880f6390f8207748cbe40013c9722a0126aaa189b3d",
|
||||
"https://bcr.bazel.build/modules/gazelle/0.52.2/source.json": "ab0117753010067b2d7aeb43873706a9780eca3262cbde6ca6f3021c23a0171a",
|
||||
"https://bcr.bazel.build/modules/google_benchmark/1.8.2/MODULE.bazel": "a70cf1bba851000ba93b58ae2f6d76490a9feb74192e57ab8e8ff13c34ec50cb",
|
||||
"https://bcr.bazel.build/modules/google_benchmark/1.8.4/MODULE.bazel": "c6d54a11dcf64ee63545f42561eda3fd94c1b5f5ebe1357011de63ae33739d5e",
|
||||
"https://bcr.bazel.build/modules/google_benchmark/1.8.5/MODULE.bazel": "9ba9b31b984022828a950e3300410977eda2e35df35584c6b0b2d0c2e52766b7",
|
||||
@@ -219,8 +224,8 @@
|
||||
"https://bcr.bazel.build/modules/grpc/1.71.0/MODULE.bazel": "7fcab2c05530373f1a442c362b17740dd0c75b6a2a975eec8f5bf4c70a37928a",
|
||||
"https://bcr.bazel.build/modules/grpc/1.74.1/MODULE.bazel": "09523be10ba2bfd999683671d0f8f22fb5b20ec77ad89b05ef58ff19a1b65c82",
|
||||
"https://bcr.bazel.build/modules/grpc/1.76.0.bcr.1/MODULE.bazel": "09b252536112acccdc7547cdfe16526a46408f570263f71491c813315f2efc45",
|
||||
"https://bcr.bazel.build/modules/grpc/1.82.0/MODULE.bazel": "42ef206e15c69b2fc3f3c6d14f285962f6a30faaed68436eb82132083a881d6d",
|
||||
"https://bcr.bazel.build/modules/grpc/1.82.0/source.json": "459a30fc244c7a06bb34ce3ee7ea7f4a4cf682801523fcf1a4b88097eda7fe48",
|
||||
"https://bcr.bazel.build/modules/grpc/1.83.0/MODULE.bazel": "45e5a9f3330c510719d4ff168f1acebe1e74a304b32d4e25771b01735b672420",
|
||||
"https://bcr.bazel.build/modules/grpc/1.83.0/source.json": "3bd4c62372667c55dbfeb7c26f7bacbd9c182f1a9fa01172b0d742d07a712703",
|
||||
"https://bcr.bazel.build/modules/helly25_bzl/0.4.3/MODULE.bazel": "9c20052fd3f1fb767c48b78c1bbc46f501a4ca69d14558429d1234e68e449f68",
|
||||
"https://bcr.bazel.build/modules/helly25_bzl/0.4.3/source.json": "e8c54d81e72633fb6f1d23c7e2d44e0b39b1caef2a256141136a2f63e6204b78",
|
||||
"https://bcr.bazel.build/modules/jq.bzl/0.1.0/MODULE.bazel": "2ce69b1af49952cd4121a9c3055faa679e748ce774c7f1fda9657f936cae902f",
|
||||
@@ -334,12 +339,14 @@
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.2.15/MODULE.bazel": "6a0a4a75a57aa6dc888300d848053a58c6b12a29f89d4304e1c41448514ec6e8",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.2.16/MODULE.bazel": "9242fa89f950c6ef7702801ab53922e99c69b02310c39fb6e62b2bd30df2a1d4",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.2.17/MODULE.bazel": "1849602c86cb60da8613d2de887f9566a6d354a6df6d7009f9d04a14402f9a84",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.2.19/MODULE.bazel": "d5e0f05b63273281a16654eb6b1a8742a75ec153ac8b4f0419949d6e401e46f0",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.2.20/MODULE.bazel": "f5c07bce5ddcb99be21a0812ff5aadb439e688b7449c6542152363b2fd859c1a",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.2.22/MODULE.bazel": "94df4328edef9e44d38de5e73b037cd348e75e7ae55f4e21bf07878c41a31ebb",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.2.22/source.json": "b2d6d6f9c332ce269ad75b89c6f3168d809a66173c9040210fb9bcc733ab42fa",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.2.4/MODULE.bazel": "1ff1223dfd24f3ecf8f028446d4a27608aa43c3f41e346d22838a4223980b8cc",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.2.8/MODULE.bazel": "f1df20f0bf22c28192a794f29b501ee2018fa37a3862a1a2132ae2940a23a642",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.2.9/MODULE.bazel": "34263f1dca62ea664265438cef714d7db124c03e1ed55ebb4f1dc860164308d1",
|
||||
"https://bcr.bazel.build/modules/rules_distroless/0.8.0/MODULE.bazel": "a3e473943b685190e20668f5149f62a116fdcbeba66c7b9778f71d2aed4533c1",
|
||||
"https://bcr.bazel.build/modules/rules_distroless/0.8.0/source.json": "a6931e20d97a13675693adb8b6cd3a0e6eb5218db43dfc83b75566ddc4158efc",
|
||||
"https://bcr.bazel.build/modules/rules_dotnet/0.20.5/MODULE.bazel": "0ca90c23f5c8e7b7c353bd673062f8716eca7c525e4fd28da16247672251bd77",
|
||||
"https://bcr.bazel.build/modules/rules_dotnet/0.22.1/MODULE.bazel": "0faeb8532d178e34ad0e461d7ec1a9d08f3e5c6c81b8791d5fff5aee356adcb2",
|
||||
"https://bcr.bazel.build/modules/rules_dotnet/0.22.1/source.json": "a1d2ad8cd886f716d7707925f3628178830c7df644a47fce711732d9e03fde0c",
|
||||
@@ -360,8 +367,8 @@
|
||||
"https://bcr.bazel.build/modules/rules_go/0.53.0/MODULE.bazel": "a4ed760d3ac0dbc0d7b967631a9a3fd9100d28f7d9fcf214b4df87d4bfff5f9a",
|
||||
"https://bcr.bazel.build/modules/rules_go/0.58.3/MODULE.bazel": "5582119a4a39558d8d1b1634bcae46043d4f43a31415e861c3551b2860040b5e",
|
||||
"https://bcr.bazel.build/modules/rules_go/0.59.0/MODULE.bazel": "b7e43e7414a3139a7547d1b4909b29085fbe5182b6c58cbe1ed4c6272815aeae",
|
||||
"https://bcr.bazel.build/modules/rules_go/0.61.1/MODULE.bazel": "b599cc67f98e8dbe040631129fbd23f190a557f7be506d4781619d0fd4dbbbec",
|
||||
"https://bcr.bazel.build/modules/rules_go/0.61.1/source.json": "ff52d46fca8e2ac87c610c11f05c3a9f0fa08dc4294664c6a31449092409c5aa",
|
||||
"https://bcr.bazel.build/modules/rules_go/0.62.0/MODULE.bazel": "8ee616065c3d2b2f7ac0880108316ce8d0c332b3a30aad24e95c0bc124ec853e",
|
||||
"https://bcr.bazel.build/modules/rules_go/0.62.0/source.json": "36d781c558eb7d3bd49dc5e190455714f174232372f15207ab200b4348bda3e6",
|
||||
"https://bcr.bazel.build/modules/rules_java/4.0.0/MODULE.bazel": "5a78a7ae82cd1a33cef56dc578c7d2a46ed0dca12643ee45edbb8417899e6f74",
|
||||
"https://bcr.bazel.build/modules/rules_java/5.1.0/MODULE.bazel": "324b6478b0343a3ce7a9add8586ad75d24076d6d43d2f622990b9c1cfd8a1b15",
|
||||
"https://bcr.bazel.build/modules/rules_java/5.3.5/MODULE.bazel": "a4ec4f2db570171e3e5eb753276ee4b389bae16b96207e9d3230895c99644b86",
|
||||
@@ -385,11 +392,12 @@
|
||||
"https://bcr.bazel.build/modules/rules_java/8.6.1/MODULE.bazel": "f4808e2ab5b0197f094cabce9f4b006a27766beb6a9975931da07099560ca9c2",
|
||||
"https://bcr.bazel.build/modules/rules_java/8.6.3/MODULE.bazel": "e90505b7a931d194245ffcfb6ff4ca8ef9d46b4e830d12e64817752e0198e2ed",
|
||||
"https://bcr.bazel.build/modules/rules_java/8.7.1/MODULE.bazel": "123a57f84c7f80d6f66b0c2486db3460ed8c4389f788ccbd35bb489b1ab23634",
|
||||
"https://bcr.bazel.build/modules/rules_java/8.8.0/MODULE.bazel": "de589d0880911ac007abd521b9f0ddcd8b0dbd05c8553e6f8124a050b83acf7d",
|
||||
"https://bcr.bazel.build/modules/rules_java/8.9.0/MODULE.bazel": "e17c876cb53dcd817b7b7f0d2985b710610169729e8c371b2221cacdcd3dce4a",
|
||||
"https://bcr.bazel.build/modules/rules_java/9.1.0/MODULE.bazel": "ee63f27e36a3fada80342869361182f120a9819c74320e8e65b1e04ba0cd7a9d",
|
||||
"https://bcr.bazel.build/modules/rules_java/9.3.0/MODULE.bazel": "f657c72d65ac449caae9abf2e68e66c0d36f9416848c4c4903d0b3234229e7f2",
|
||||
"https://bcr.bazel.build/modules/rules_java/9.6.1/MODULE.bazel": "6b0b7172ce598e37e31d1e24f2a492a5249b88304bd25b02b465ee22e3aa3752",
|
||||
"https://bcr.bazel.build/modules/rules_java/9.6.1/source.json": "d577c30fe3005821ac39c6ed43eb5accc77ff67c7b80f4043101aef4b027a903",
|
||||
"https://bcr.bazel.build/modules/rules_java/9.7.0/MODULE.bazel": "3ce6bd55fdd4fcb3323197736b7d976e0eedcc0eea78ca6186d20d314ba12e12",
|
||||
"https://bcr.bazel.build/modules/rules_java/9.7.0/source.json": "23b356565156e0fbc71e5dad7cf8e1b951466b7ae3fd731cb1d2c95ca82b1d70",
|
||||
"https://bcr.bazel.build/modules/rules_jvm_external/5.2/MODULE.bazel": "d9351ba35217ad0de03816ef3ed63f89d411349353077348a45348b096615036",
|
||||
"https://bcr.bazel.build/modules/rules_jvm_external/5.3/MODULE.bazel": "bf93870767689637164657731849fb887ad086739bd5d360d90007a581d5527d",
|
||||
"https://bcr.bazel.build/modules/rules_jvm_external/6.0/MODULE.bazel": "37c93a5a78d32e895d52f86a8d0416176e915daabd029ccb5594db422e87c495",
|
||||
@@ -401,8 +409,8 @@
|
||||
"https://bcr.bazel.build/modules/rules_jvm_external/6.7/MODULE.bazel": "e717beabc4d091ecb2c803c2d341b88590e9116b8bf7947915eeb33aab4f96dd",
|
||||
"https://bcr.bazel.build/modules/rules_jvm_external/6.8/MODULE.bazel": "b5afe861e867e4c8e5b88e401cb7955bd35924258f97b1862cc966cbcf4f1a62",
|
||||
"https://bcr.bazel.build/modules/rules_jvm_external/6.9/MODULE.bazel": "07c5db05527db7744a54fcffd653e1550d40e0540207a7f7e6d0a4de5bef8274",
|
||||
"https://bcr.bazel.build/modules/rules_jvm_external/7.0/MODULE.bazel": "421482bdbcf05709f933c96b867a599deb517f2804ceb3e74511880610cfbf71",
|
||||
"https://bcr.bazel.build/modules/rules_jvm_external/7.0/source.json": "714cd003eadf5be5c83268311fe8e951db39f802babeaddc536b3560dc8f6faf",
|
||||
"https://bcr.bazel.build/modules/rules_jvm_external/7.1/MODULE.bazel": "e151b1beaa45bfc9e5de4df637d980e27039eb2b1092a95972bf2adbf99ac1a0",
|
||||
"https://bcr.bazel.build/modules/rules_jvm_external/7.1/source.json": "8cb571a9631c39bf29c1842958542f14bba1c704fe2bcde4fa54679b2a6b3d6f",
|
||||
"https://bcr.bazel.build/modules/rules_kotlin/1.9.0/MODULE.bazel": "ef85697305025e5a61f395d4eaede272a5393cee479ace6686dba707de804d59",
|
||||
"https://bcr.bazel.build/modules/rules_kotlin/1.9.5/MODULE.bazel": "043a16a572f610558ec2030db3ff0c9938574e7dd9f58bded1bb07c0192ef025",
|
||||
"https://bcr.bazel.build/modules/rules_kotlin/1.9.6/MODULE.bazel": "d269a01a18ee74d0335450b10f62c9ed81f2321d7958a2934e44272fe82dcef3",
|
||||
@@ -463,13 +471,14 @@
|
||||
"https://bcr.bazel.build/modules/rules_python/1.6.0/MODULE.bazel": "7e04ad8f8d5bea40451cf80b1bd8262552aa73f841415d20db96b7241bd027d8",
|
||||
"https://bcr.bazel.build/modules/rules_python/1.6.3/MODULE.bazel": "a7b80c42cb3de5ee2a5fa1abc119684593704fcd2fec83165ebe615dec76574f",
|
||||
"https://bcr.bazel.build/modules/rules_python/1.7.0/MODULE.bazel": "d01f995ecd137abf30238ad9ce97f8fc3ac57289c8b24bd0bf53324d937a14f8",
|
||||
"https://bcr.bazel.build/modules/rules_python/1.7.0/source.json": "028a084b65dcf8f4dc4f82f8778dbe65df133f234b316828a82e060d81bdce32",
|
||||
"https://bcr.bazel.build/modules/rules_python/2.0.2/MODULE.bazel": "9d26ecb43f248c2663fb29f3c9b833aa86c31e736096e58c7077f80038f8d32f",
|
||||
"https://bcr.bazel.build/modules/rules_python/2.0.2/source.json": "36a74dedc1498bfbf72855568a24e6aa30c70b5ac7e114bc244c62b3a58b47fc",
|
||||
"https://bcr.bazel.build/modules/rules_robolectric/4.14.1.2/MODULE.bazel": "d44fec647d0aeb67b9f3b980cf68ba634976f3ae7ccd6c07d790b59b87a4f251",
|
||||
"https://bcr.bazel.build/modules/rules_robolectric/4.14.1.2/source.json": "37c10335f2361c337c5c1f34ed36d2da70534c23088062b33a8bdaab68aa9dea",
|
||||
"https://bcr.bazel.build/modules/rules_rust/0.66.0/MODULE.bazel": "86ef763a582f4739a27029bdcc6c562258ed0ea6f8d58294b049e215ceb251b3",
|
||||
"https://bcr.bazel.build/modules/rules_rust/0.69.0/MODULE.bazel": "4326fec48f2fef0d514de46346f7f77e200c82936dd08b91c9ef039fbdad5c10",
|
||||
"https://bcr.bazel.build/modules/rules_rust/0.71.3/MODULE.bazel": "e2390c96f77d65f00c769bf665678c5424188e9c777239cfaae2a8d2dde7b981",
|
||||
"https://bcr.bazel.build/modules/rules_rust/0.71.3/source.json": "5eb5d8068571725bc893045f8137ed7937988f23d73c53ea443470e8047598ad",
|
||||
"https://bcr.bazel.build/modules/rules_rust/0.72.0/MODULE.bazel": "e49a6d6525cf5a28d52afe17e4b12e62498652b40ab30cec26be39eb4f0303c7",
|
||||
"https://bcr.bazel.build/modules/rules_rust/0.72.0/source.json": "a3871759cac97efb50ea066bcf6487ed639c181e71aa22d26aaa658890703bc2",
|
||||
"https://bcr.bazel.build/modules/rules_scala/7.2.6/MODULE.bazel": "027a152217e87140ccbea1e73528b35907c59806390603b80704125243d40007",
|
||||
"https://bcr.bazel.build/modules/rules_scala/7.2.6/source.json": "935f098a206b79c503bc160cb5ab06d9cfd6a73543351f975aa886d6aa8c7228",
|
||||
"https://bcr.bazel.build/modules/rules_shell/0.1.2/MODULE.bazel": "66e4ca3ce084b04af0b9ff05ff14cab4e5df7503973818bb91cbc6cda08d32fc",
|
||||
@@ -520,6 +529,7 @@
|
||||
"https://bcr.bazel.build/modules/xds/0.0.0-20251210-ee656c7/MODULE.bazel": "03e1c50b16fc2403de5dd98ff8e2dc0b404865740434b8705c7c0853241f943d",
|
||||
"https://bcr.bazel.build/modules/xds/0.0.0-20251210-ee656c7/source.json": "69b87f3bff38ba29399c012f3e3fc0f239ac4e5c1ae66ebd9481be6d8461da55",
|
||||
"https://bcr.bazel.build/modules/yq.bzl/0.1.1/MODULE.bazel": "9039681f9bcb8958ee2c87ffc74bdafba9f4369096a2b5634b88abc0eaefa072",
|
||||
"https://bcr.bazel.build/modules/yq.bzl/0.3.1/MODULE.bazel": "9bcb7151b3cd4681b89d350530eaf7b45e32a44dda94843b8932b0cb1cd4594a",
|
||||
"https://bcr.bazel.build/modules/yq.bzl/0.3.2/MODULE.bazel": "0384efa70e8033d842ea73aa4b7199fa099709e236a7264345c03937166670b6",
|
||||
"https://bcr.bazel.build/modules/yq.bzl/0.3.4/MODULE.bazel": "d3a270662f5d766cd7229732d65a5a5bc485240c3007343dd279edfb60c9ae27",
|
||||
"https://bcr.bazel.build/modules/yq.bzl/0.3.4/source.json": "786dafdc2843722da3416e4343ee1a05237227f068590779a6e8496a2064c0f9",
|
||||
@@ -538,20 +548,20 @@
|
||||
"moduleExtensions": {
|
||||
"@@aspect_rules_esbuild+//esbuild:extensions.bzl%esbuild": {
|
||||
"general": {
|
||||
"bzlTransitiveDigest": "kfk2SGJZK8VZ9E8vcWrg8wbnoY1kBI/mhYkz7AtXcnc=",
|
||||
"bzlTransitiveDigest": "2TgYqOVPOdEP28Y0P/m1fsmCztTsmsVJHCnTQzAsgZA=",
|
||||
"usagesDigest": "LSQ+zZp7JNgnBONTxxXnwGr4NTh2qtQYk7qwXXz5qWo=",
|
||||
"recordedInputs": [
|
||||
"REPO_MAPPING:aspect_rules_esbuild+,aspect_rules_js aspect_rules_js+3.2.2",
|
||||
"REPO_MAPPING:aspect_rules_esbuild+,aspect_rules_js aspect_rules_js+3.3.0",
|
||||
"REPO_MAPPING:aspect_rules_esbuild+,aspect_tools_telemetry_report aspect_tools_telemetry++telemetry+aspect_tools_telemetry_report",
|
||||
"REPO_MAPPING:aspect_rules_esbuild+,bazel_skylib bazel_skylib+",
|
||||
"REPO_MAPPING:aspect_rules_js+3.2.2,aspect_rules_js aspect_rules_js+3.2.2",
|
||||
"REPO_MAPPING:aspect_rules_js+3.2.2,aspect_tools_telemetry_report aspect_tools_telemetry++telemetry+aspect_tools_telemetry_report",
|
||||
"REPO_MAPPING:aspect_rules_js+3.2.2,bazel_features bazel_features+",
|
||||
"REPO_MAPPING:aspect_rules_js+3.2.2,bazel_lib bazel_lib+",
|
||||
"REPO_MAPPING:aspect_rules_js+3.2.2,bazel_skylib bazel_skylib+",
|
||||
"REPO_MAPPING:aspect_rules_js+3.2.2,bazel_tools bazel_tools",
|
||||
"REPO_MAPPING:aspect_rules_js+3.2.2,protobuf protobuf+",
|
||||
"REPO_MAPPING:aspect_rules_js+3.2.2,tar.bzl tar.bzl+",
|
||||
"REPO_MAPPING:aspect_rules_js+3.3.0,aspect_rules_js aspect_rules_js+3.3.0",
|
||||
"REPO_MAPPING:aspect_rules_js+3.3.0,aspect_tools_telemetry_report aspect_tools_telemetry++telemetry+aspect_tools_telemetry_report",
|
||||
"REPO_MAPPING:aspect_rules_js+3.3.0,bazel_features bazel_features+",
|
||||
"REPO_MAPPING:aspect_rules_js+3.3.0,bazel_lib bazel_lib+",
|
||||
"REPO_MAPPING:aspect_rules_js+3.3.0,bazel_skylib bazel_skylib+",
|
||||
"REPO_MAPPING:aspect_rules_js+3.3.0,bazel_tools bazel_tools",
|
||||
"REPO_MAPPING:aspect_rules_js+3.3.0,protobuf protobuf+",
|
||||
"REPO_MAPPING:aspect_rules_js+3.3.0,tar.bzl tar.bzl+",
|
||||
"REPO_MAPPING:bazel_features+,bazel_features_globals bazel_features++version_extension+bazel_features_globals",
|
||||
"REPO_MAPPING:bazel_features+,bazel_features_version bazel_features++version_extension+bazel_features_version",
|
||||
"REPO_MAPPING:bazel_lib+,bazel_lib bazel_lib+",
|
||||
@@ -607,7 +617,7 @@
|
||||
}
|
||||
},
|
||||
"npm__esbuild_0.19.9": {
|
||||
"repoRuleId": "@@aspect_rules_js+3.2.2//npm/private:npm_import.bzl%npm_import_rule",
|
||||
"repoRuleId": "@@aspect_rules_js+3.3.0//npm/private:npm_import.bzl%npm_import_rule",
|
||||
"attributes": {
|
||||
"key": "npm__esbuild_0.19.9",
|
||||
"package": "esbuild",
|
||||
@@ -619,7 +629,7 @@
|
||||
}
|
||||
},
|
||||
"npm__esbuild_0.19.9__links": {
|
||||
"repoRuleId": "@@aspect_rules_js+3.2.2//npm/private:npm_import.bzl%npm_import_links_rule",
|
||||
"repoRuleId": "@@aspect_rules_js+3.3.0//npm/private:npm_import.bzl%npm_import_links_rule",
|
||||
"attributes": {
|
||||
"key": "npm__esbuild_0.19.9",
|
||||
"package": "esbuild",
|
||||
@@ -637,11 +647,12 @@
|
||||
},
|
||||
"@@aspect_tools_telemetry+//:extension.bzl%telemetry": {
|
||||
"general": {
|
||||
"bzlTransitiveDigest": "cl5A2O84vDL6Tt+Qga8FCj1DUDGqn+e7ly5rZ+4xvcc=",
|
||||
"usagesDigest": "cKpV04nnte17GA1AdBa4rDlTwDX7NmUaevRM3bvVo2w=",
|
||||
"bzlTransitiveDigest": "4w9RM0xjdKo1crk5zL20a/TuhqO0P1z1LsuXDneBXD4=",
|
||||
"usagesDigest": "wB+5bTQ6JfbGKqgpIdzeavsJXRudtVh397OCTHT6UoU=",
|
||||
"recordedInputs": [
|
||||
"REPO_MAPPING:aspect_tools_telemetry+,bazel_lib bazel_lib+",
|
||||
"REPO_MAPPING:aspect_tools_telemetry+,bazel_skylib bazel_skylib+"
|
||||
"REPO_MAPPING:aspect_tools_telemetry+,bazel_skylib bazel_skylib+",
|
||||
"ENV:ASPECT_TOOLS_TELEMETRY_TEST \\0"
|
||||
],
|
||||
"generatedRepoSpecs": {
|
||||
"aspect_tools_telemetry_report": {
|
||||
@@ -649,11 +660,16 @@
|
||||
"attributes": {
|
||||
"deps": {
|
||||
"aspect_rules_esbuild": "0.26.0",
|
||||
"aspect_rules_js": "3.2.2",
|
||||
"aspect_tools_telemetry": "0.3.3"
|
||||
}
|
||||
"aspect_rules_js": "3.3.0",
|
||||
"aspect_tools_telemetry": "0.4.2"
|
||||
},
|
||||
"last_notice": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"moduleExtensionMetadata": {
|
||||
"useAllRepos": "NO",
|
||||
"reproducible": false
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -778,7 +794,7 @@
|
||||
"@@rules_oci+//oci:extensions.bzl%oci": {
|
||||
"general": {
|
||||
"bzlTransitiveDigest": "BDXSA33CE6vRqjKESIa/t5DWnrmDfPNvmQyK33mpgFk=",
|
||||
"usagesDigest": "DSmv+O+cO86ySvuNe6EQRa8nh5IQr5JY0+QMJPNPjkY=",
|
||||
"usagesDigest": "+fLPmQIzW3zD0eLxnM4mihSmPCR+wUZh121rVq2a5DE=",
|
||||
"recordedInputs": [
|
||||
"REPO_MAPPING:aspect_bazel_lib+,bazel_tools bazel_tools",
|
||||
"REPO_MAPPING:bazel_features+,bazel_tools bazel_tools",
|
||||
@@ -796,7 +812,7 @@
|
||||
"scheme": "https",
|
||||
"registry": "index.docker.io",
|
||||
"repository": "library/eclipse-temurin",
|
||||
"identifier": "sha256:a0053c48b1a74412b14a111d6fd547799f8a6a5c77e2318f698b996ec899b0ce",
|
||||
"identifier": "sha256:882787e465d6c0c4c08daa8d9e209d57ec052228eff8244c959f271af24ad27b",
|
||||
"platform": "linux/amd64",
|
||||
"target_name": "eclipse_temurin_25_jre_linux_amd64",
|
||||
"bazel_tags": []
|
||||
@@ -810,7 +826,7 @@
|
||||
"scheme": "https",
|
||||
"registry": "index.docker.io",
|
||||
"repository": "library/eclipse-temurin",
|
||||
"identifier": "sha256:a0053c48b1a74412b14a111d6fd547799f8a6a5c77e2318f698b996ec899b0ce",
|
||||
"identifier": "sha256:882787e465d6c0c4c08daa8d9e209d57ec052228eff8244c959f271af24ad27b",
|
||||
"platforms": {
|
||||
"@@platforms//cpu:x86_64": "@eclipse_temurin_25_jre_linux_amd64"
|
||||
},
|
||||
@@ -825,7 +841,7 @@
|
||||
"scheme": "https",
|
||||
"registry": "index.docker.io",
|
||||
"repository": "library/eclipse-temurin",
|
||||
"identifier": "sha256:8e0469892385b62feab689f211fde7eeeb6537418c8fbe613147427b53870ec3",
|
||||
"identifier": "sha256:625889835aab66a096f999ee16cd7061ac846b155dc7adbed693686bbae5c59a",
|
||||
"platform": "linux/amd64",
|
||||
"target_name": "eclipse_temurin_25_jdk_linux_amd64",
|
||||
"bazel_tags": []
|
||||
@@ -839,7 +855,7 @@
|
||||
"scheme": "https",
|
||||
"registry": "index.docker.io",
|
||||
"repository": "library/eclipse-temurin",
|
||||
"identifier": "sha256:8e0469892385b62feab689f211fde7eeeb6537418c8fbe613147427b53870ec3",
|
||||
"identifier": "sha256:625889835aab66a096f999ee16cd7061ac846b155dc7adbed693686bbae5c59a",
|
||||
"platforms": {
|
||||
"@@platforms//cpu:x86_64": "@eclipse_temurin_25_jdk_linux_amd64"
|
||||
},
|
||||
@@ -1051,178 +1067,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@@rules_python+//python/extensions:config.bzl%config": {
|
||||
"general": {
|
||||
"bzlTransitiveDigest": "dzD8Q2YmrP3fz8saWLHPmlwPLO91ImtTmP/c9JKTStM=",
|
||||
"usagesDigest": "ZVSXMAGpD+xzVNPuvF1IoLBkty7TROO0+akMapt1pAg=",
|
||||
"recordedInputs": [
|
||||
"REPO_MAPPING:rules_python+,bazel_tools bazel_tools",
|
||||
"REPO_MAPPING:rules_python+,pypi__build rules_python++config+pypi__build",
|
||||
"REPO_MAPPING:rules_python+,pypi__click rules_python++config+pypi__click",
|
||||
"REPO_MAPPING:rules_python+,pypi__colorama rules_python++config+pypi__colorama",
|
||||
"REPO_MAPPING:rules_python+,pypi__importlib_metadata rules_python++config+pypi__importlib_metadata",
|
||||
"REPO_MAPPING:rules_python+,pypi__installer rules_python++config+pypi__installer",
|
||||
"REPO_MAPPING:rules_python+,pypi__more_itertools rules_python++config+pypi__more_itertools",
|
||||
"REPO_MAPPING:rules_python+,pypi__packaging rules_python++config+pypi__packaging",
|
||||
"REPO_MAPPING:rules_python+,pypi__pep517 rules_python++config+pypi__pep517",
|
||||
"REPO_MAPPING:rules_python+,pypi__pip rules_python++config+pypi__pip",
|
||||
"REPO_MAPPING:rules_python+,pypi__pip_tools rules_python++config+pypi__pip_tools",
|
||||
"REPO_MAPPING:rules_python+,pypi__pyproject_hooks rules_python++config+pypi__pyproject_hooks",
|
||||
"REPO_MAPPING:rules_python+,pypi__setuptools rules_python++config+pypi__setuptools",
|
||||
"REPO_MAPPING:rules_python+,pypi__tomli rules_python++config+pypi__tomli",
|
||||
"REPO_MAPPING:rules_python+,pypi__wheel rules_python++config+pypi__wheel",
|
||||
"REPO_MAPPING:rules_python+,pypi__zipp rules_python++config+pypi__zipp"
|
||||
],
|
||||
"generatedRepoSpecs": {
|
||||
"rules_python_internal": {
|
||||
"repoRuleId": "@@rules_python+//python/private:internal_config_repo.bzl%internal_config_repo",
|
||||
"attributes": {
|
||||
"transition_setting_generators": {},
|
||||
"transition_settings": []
|
||||
}
|
||||
},
|
||||
"pypi__build": {
|
||||
"repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
|
||||
"attributes": {
|
||||
"url": "https://files.pythonhosted.org/packages/e2/03/f3c8ba0a6b6e30d7d18c40faab90807c9bb5e9a1e3b2fe2008af624a9c97/build-1.2.1-py3-none-any.whl",
|
||||
"sha256": "75e10f767a433d9a86e50d83f418e83efc18ede923ee5ff7df93b6cb0306c5d4",
|
||||
"type": "zip",
|
||||
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
|
||||
}
|
||||
},
|
||||
"pypi__click": {
|
||||
"repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
|
||||
"attributes": {
|
||||
"url": "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl",
|
||||
"sha256": "ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28",
|
||||
"type": "zip",
|
||||
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
|
||||
}
|
||||
},
|
||||
"pypi__colorama": {
|
||||
"repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
|
||||
"attributes": {
|
||||
"url": "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl",
|
||||
"sha256": "4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6",
|
||||
"type": "zip",
|
||||
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
|
||||
}
|
||||
},
|
||||
"pypi__importlib_metadata": {
|
||||
"repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
|
||||
"attributes": {
|
||||
"url": "https://files.pythonhosted.org/packages/2d/0a/679461c511447ffaf176567d5c496d1de27cbe34a87df6677d7171b2fbd4/importlib_metadata-7.1.0-py3-none-any.whl",
|
||||
"sha256": "30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570",
|
||||
"type": "zip",
|
||||
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
|
||||
}
|
||||
},
|
||||
"pypi__installer": {
|
||||
"repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
|
||||
"attributes": {
|
||||
"url": "https://files.pythonhosted.org/packages/e5/ca/1172b6638d52f2d6caa2dd262ec4c811ba59eee96d54a7701930726bce18/installer-0.7.0-py3-none-any.whl",
|
||||
"sha256": "05d1933f0a5ba7d8d6296bb6d5018e7c94fa473ceb10cf198a92ccea19c27b53",
|
||||
"type": "zip",
|
||||
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
|
||||
}
|
||||
},
|
||||
"pypi__more_itertools": {
|
||||
"repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
|
||||
"attributes": {
|
||||
"url": "https://files.pythonhosted.org/packages/50/e2/8e10e465ee3987bb7c9ab69efb91d867d93959095f4807db102d07995d94/more_itertools-10.2.0-py3-none-any.whl",
|
||||
"sha256": "686b06abe565edfab151cb8fd385a05651e1fdf8f0a14191e4439283421f8684",
|
||||
"type": "zip",
|
||||
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
|
||||
}
|
||||
},
|
||||
"pypi__packaging": {
|
||||
"repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
|
||||
"attributes": {
|
||||
"url": "https://files.pythonhosted.org/packages/49/df/1fceb2f8900f8639e278b056416d49134fb8d84c5942ffaa01ad34782422/packaging-24.0-py3-none-any.whl",
|
||||
"sha256": "2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5",
|
||||
"type": "zip",
|
||||
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
|
||||
}
|
||||
},
|
||||
"pypi__pep517": {
|
||||
"repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
|
||||
"attributes": {
|
||||
"url": "https://files.pythonhosted.org/packages/25/6e/ca4a5434eb0e502210f591b97537d322546e4833dcb4d470a48c375c5540/pep517-0.13.1-py3-none-any.whl",
|
||||
"sha256": "31b206f67165b3536dd577c5c3f1518e8fbaf38cbc57efff8369a392feff1721",
|
||||
"type": "zip",
|
||||
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
|
||||
}
|
||||
},
|
||||
"pypi__pip": {
|
||||
"repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
|
||||
"attributes": {
|
||||
"url": "https://files.pythonhosted.org/packages/8a/6a/19e9fe04fca059ccf770861c7d5721ab4c2aebc539889e97c7977528a53b/pip-24.0-py3-none-any.whl",
|
||||
"sha256": "ba0d021a166865d2265246961bec0152ff124de910c5cc39f1156ce3fa7c69dc",
|
||||
"type": "zip",
|
||||
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
|
||||
}
|
||||
},
|
||||
"pypi__pip_tools": {
|
||||
"repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
|
||||
"attributes": {
|
||||
"url": "https://files.pythonhosted.org/packages/0d/dc/38f4ce065e92c66f058ea7a368a9c5de4e702272b479c0992059f7693941/pip_tools-7.4.1-py3-none-any.whl",
|
||||
"sha256": "4c690e5fbae2f21e87843e89c26191f0d9454f362d8acdbd695716493ec8b3a9",
|
||||
"type": "zip",
|
||||
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
|
||||
}
|
||||
},
|
||||
"pypi__pyproject_hooks": {
|
||||
"repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
|
||||
"attributes": {
|
||||
"url": "https://files.pythonhosted.org/packages/ae/f3/431b9d5fe7d14af7a32340792ef43b8a714e7726f1d7b69cc4e8e7a3f1d7/pyproject_hooks-1.1.0-py3-none-any.whl",
|
||||
"sha256": "7ceeefe9aec63a1064c18d939bdc3adf2d8aa1988a510afec15151578b232aa2",
|
||||
"type": "zip",
|
||||
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
|
||||
}
|
||||
},
|
||||
"pypi__setuptools": {
|
||||
"repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
|
||||
"attributes": {
|
||||
"url": "https://files.pythonhosted.org/packages/90/99/158ad0609729111163fc1f674a5a42f2605371a4cf036d0441070e2f7455/setuptools-78.1.1-py3-none-any.whl",
|
||||
"sha256": "c3a9c4211ff4c309edb8b8c4f1cbfa7ae324c4ba9f91ff254e3d305b9fd54561",
|
||||
"type": "zip",
|
||||
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
|
||||
}
|
||||
},
|
||||
"pypi__tomli": {
|
||||
"repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
|
||||
"attributes": {
|
||||
"url": "https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl",
|
||||
"sha256": "939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
|
||||
"type": "zip",
|
||||
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
|
||||
}
|
||||
},
|
||||
"pypi__wheel": {
|
||||
"repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
|
||||
"attributes": {
|
||||
"url": "https://files.pythonhosted.org/packages/7d/cd/d7460c9a869b16c3dd4e1e403cce337df165368c71d6af229a74699622ce/wheel-0.43.0-py3-none-any.whl",
|
||||
"sha256": "55c570405f142630c6b9f72fe09d9b67cf1477fcf543ae5b8dcb1f5b7377da81",
|
||||
"type": "zip",
|
||||
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
|
||||
}
|
||||
},
|
||||
"pypi__zipp": {
|
||||
"repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
|
||||
"attributes": {
|
||||
"url": "https://files.pythonhosted.org/packages/da/55/a03fd7240714916507e1fcf7ae355bd9d9ed2e6db492595f1a67f61681be/zipp-3.18.2-py3-none-any.whl",
|
||||
"sha256": "dce197b859eb796242b0622af1b8beb0a722d52aa2f57133ead08edd5bf5374e",
|
||||
"type": "zip",
|
||||
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@@rules_python+//python/uv:uv.bzl%uv": {
|
||||
"general": {
|
||||
"bzlTransitiveDigest": "ijW9KS7qsIY+yBVvJ+Nr1mzwQox09j13DnE3iIwaeTM=",
|
||||
"usagesDigest": "H8dQoNZcoqP+Mu0tHZTi4KHATzvNkM5ePuEqoQdklIU=",
|
||||
"bzlTransitiveDigest": "I8FPZMevE2oI/peSpMBRVIN++WOtfjtJVjbPsBZQ87A=",
|
||||
"usagesDigest": "R0pij/Kri0Z/7GcM2UBot6XmOqgduje0j8y9wNMWjLA=",
|
||||
"recordedInputs": [
|
||||
"REPO_MAPPING:rules_python+,bazel_tools bazel_tools",
|
||||
"REPO_MAPPING:rules_python+,platforms platforms"
|
||||
@@ -1272,7 +1120,7 @@
|
||||
"@@yq.bzl+//yq:extensions.bzl%yq": {
|
||||
"general": {
|
||||
"bzlTransitiveDigest": "tDqk+ntWTdxNAWPDjRY1uITgHbti2jcXR5ZdinltBs0=",
|
||||
"usagesDigest": "z7g7IZt6aaEeYztkX2Hs5qlSifRJcDTGz9K/CD4yOFI=",
|
||||
"usagesDigest": "J2+fvKYoYDe7ZNdlF7t4fyZ6nEbQ3oyoUn15ElkxCMI=",
|
||||
"recordedInputs": [],
|
||||
"generatedRepoSpecs": {
|
||||
"yq_darwin_amd64": {
|
||||
@@ -1349,6 +1197,9 @@
|
||||
}
|
||||
},
|
||||
"facts": {
|
||||
"@@aspect_tools_telemetry+//:extension.bzl%telemetry": {
|
||||
"notice_version": "1"
|
||||
},
|
||||
"@@rules_go+//go:extensions.bzl%go_sdk": {
|
||||
"1.22.4": {
|
||||
"aix_ppc64": [
|
||||
|
||||
+2
-1
@@ -214,7 +214,7 @@ oci_push(
|
||||
#
|
||||
# Shardok Server ARM64 Docker Image (for the Hetzner deployment host)
|
||||
#
|
||||
# Build: bazel build //ci:shardok_server_image_arm64 --platforms=//:linux_arm64 --extra_toolchains=@llvm_toolchain_linux_arm64//:cc-toolchain-aarch64-linux
|
||||
# Build: ./scripts/build_shardok_linux_arm64_ci.sh //ci:shardok_server_image_arm64
|
||||
# Load: bazel run //ci:shardok_server_load_arm64
|
||||
# Push: bazel run //ci:shardok_server_push_arm64
|
||||
#
|
||||
@@ -235,6 +235,7 @@ oci_image(
|
||||
"40052/tcp",
|
||||
],
|
||||
tars = [
|
||||
"@shardok_perf//:flat",
|
||||
# Note: busybox_layer_arm64 omitted - busybox.net has SSL issues
|
||||
# Health checks can use the shardok-server binary itself or be added later
|
||||
":shardok_binary_layer_arm64",
|
||||
|
||||
@@ -46,6 +46,45 @@ fi
|
||||
SCHEME="Unity-iPhone"
|
||||
echo "Using scheme: $SCHEME"
|
||||
|
||||
# Xcode can be upgraded on a self-hosted runner before its matching iOS platform
|
||||
# component is installed. In that state, xcrun may exist but the generic iOS
|
||||
# destination is ineligible, and archive fails with exit code 70. Probe the exact
|
||||
# destination used below and repair the Xcode installation before starting the
|
||||
# expensive archive.
|
||||
if ! xcodebuild \
|
||||
-project "$XCODEPROJ" \
|
||||
-scheme "$SCHEME" \
|
||||
-showBuildSettings \
|
||||
-destination "generic/platform=iOS" >/dev/null; then
|
||||
echo "The generic iOS destination is unavailable; installing Xcode's iOS platform..."
|
||||
|
||||
if ! xcodebuild -checkFirstLaunchStatus; then
|
||||
echo "Completing pending Xcode first-launch setup..."
|
||||
xcodebuild -runFirstLaunch
|
||||
fi
|
||||
|
||||
xcodebuild -downloadPlatform iOS
|
||||
fi
|
||||
|
||||
echo "Installed Xcode SDKs:"
|
||||
xcodebuild -showsdks
|
||||
IOS_SDK_PATH=$(xcrun --sdk iphoneos --show-sdk-path)
|
||||
if [ ! -d "$IOS_SDK_PATH" ]; then
|
||||
echo "Error: Xcode reported an invalid iOS SDK path: $IOS_SDK_PATH"
|
||||
exit 1
|
||||
fi
|
||||
echo "Using iOS SDK: $IOS_SDK_PATH"
|
||||
|
||||
if ! xcodebuild \
|
||||
-project "$XCODEPROJ" \
|
||||
-scheme "$SCHEME" \
|
||||
-showBuildSettings \
|
||||
-destination "generic/platform=iOS" >/dev/null; then
|
||||
echo "Error: The generic iOS destination is still unavailable after platform setup."
|
||||
xcodebuild -project "$XCODEPROJ" -scheme "$SCHEME" -showdestinations || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Archive without signing - we'll sign during export
|
||||
# This avoids issues with provisioning profiles on framework targets
|
||||
xcodebuild archive \
|
||||
|
||||
Executable
+296
@@ -0,0 +1,296 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
BACKUP_MODE=${1:-backup}
|
||||
GITEA_BACKUP_SSH_HOST=${GITEA_BACKUP_SSH_HOST:-gitea-backup-source}
|
||||
GITEA_HTTP_URL=${GITEA_HTTP_URL:-http://zog:3000/admin/eagle0.git}
|
||||
GITEA_VERSION_URL=${GITEA_VERSION_URL:-http://zog:3000/api/v1/version}
|
||||
GITEA_REMOTE_ROOT=${GITEA_REMOTE_ROOT:-/volume2/docker/gitea}
|
||||
GITEA_BACKUP_BUCKET=${GITEA_BACKUP_BUCKET:-eagle0-gitea-backups}
|
||||
GITEA_BACKUP_PREFIX=${GITEA_BACKUP_PREFIX:-restic}
|
||||
RUN_ID=${GITHUB_RUN_ID:-manual}
|
||||
GITEA_DATA_DIRECTORIES=(
|
||||
actions_artifacts
|
||||
actions_log
|
||||
attachments
|
||||
avatars
|
||||
conf
|
||||
home
|
||||
packages
|
||||
repo-avatars
|
||||
)
|
||||
|
||||
require_environment_variable() {
|
||||
local variable_name=$1
|
||||
if [ -z "${!variable_name:-}" ]; then
|
||||
echo "ERROR: Required environment variable '$variable_name' is not set."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
require_environment_variable RUNNER_TEMP
|
||||
require_environment_variable DO_SPACES_ENDPOINT
|
||||
require_environment_variable DO_SPACES_ACCESS_KEY
|
||||
require_environment_variable DO_SPACES_SECRET_KEY
|
||||
require_environment_variable GITEA_BACKUP_SSH_KEY
|
||||
require_environment_variable RESTIC_PASSWORD
|
||||
|
||||
case "$RUN_ID" in
|
||||
*[!A-Za-z0-9._-]*)
|
||||
echo "ERROR: Unsafe run identifier '$RUN_ID'."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
STAGING_ROOT="$RUNNER_TEMP/eagle0-gitea-backup"
|
||||
VERIFY_ROOT="$RUNNER_TEMP/eagle0-gitea-restore-verification"
|
||||
REMOTE_SNAPSHOT_DIR="$GITEA_REMOTE_ROOT/backup-staging"
|
||||
REMOTE_DATABASE="$GITEA_REMOTE_ROOT/data/gitea/gitea.db"
|
||||
REMOTE_DATABASE_SNAPSHOT="$REMOTE_SNAPSHOT_DIR/gitea-$RUN_ID.db"
|
||||
RESTIC_REPOSITORY="s3:${DO_SPACES_ENDPOINT%/}/$GITEA_BACKUP_BUCKET/$GITEA_BACKUP_PREFIX"
|
||||
SSH_CONFIG="$RUNNER_TEMP/eagle0-gitea-backup-ssh-config"
|
||||
SSH_PRIVATE_KEY="$RUNNER_TEMP/eagle0-gitea-backup-ssh-key"
|
||||
|
||||
export AWS_ACCESS_KEY_ID="$DO_SPACES_ACCESS_KEY"
|
||||
export AWS_SECRET_ACCESS_KEY="$DO_SPACES_SECRET_KEY"
|
||||
export AWS_DEFAULT_REGION=us-east-1
|
||||
export RESTIC_REPOSITORY
|
||||
export RESTIC_PASSWORD
|
||||
export RESTIC_CACHE_DIR="$RUNNER_TEMP/restic-cache"
|
||||
export HOMEBREW_NO_AUTO_UPDATE=1
|
||||
export HOMEBREW_NO_INSTALL_CLEANUP=1
|
||||
|
||||
COMMON_PATHS=(/opt/homebrew/bin /usr/local/bin)
|
||||
for common_path in "${COMMON_PATHS[@]}"; do
|
||||
if [ -d "$common_path" ]; then
|
||||
export PATH="$common_path:$PATH"
|
||||
fi
|
||||
done
|
||||
|
||||
ensure_tool() {
|
||||
local command_name=$1
|
||||
local brew_package=$2
|
||||
|
||||
if command -v "$command_name" >/dev/null 2>&1; then
|
||||
return
|
||||
fi
|
||||
|
||||
if ! command -v brew >/dev/null 2>&1; then
|
||||
echo "ERROR: '$command_name' is unavailable and Homebrew is not installed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
brew install "$brew_package"
|
||||
}
|
||||
|
||||
safe_reset_directory() {
|
||||
local directory=$1
|
||||
|
||||
case "$directory" in
|
||||
"$RUNNER_TEMP"/eagle0-gitea-*)
|
||||
rm -rf "$directory"
|
||||
mkdir -p "$directory"
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: Refusing to reset unsafe directory '$directory'."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
remove_remote_database_snapshot() {
|
||||
ssh -F "$SSH_CONFIG" -o BatchMode=yes -o ConnectTimeout=15 "$GITEA_BACKUP_SSH_HOST" \
|
||||
"rm -f '$REMOTE_DATABASE_SNAPSHOT'" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
remove_remote_database_snapshot
|
||||
|
||||
case "$STAGING_ROOT" in
|
||||
"$RUNNER_TEMP"/eagle0-gitea-*)
|
||||
rm -rf "$STAGING_ROOT"
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$VERIFY_ROOT" in
|
||||
"$RUNNER_TEMP"/eagle0-gitea-*)
|
||||
rm -rf "$VERIFY_ROOT"
|
||||
;;
|
||||
esac
|
||||
|
||||
rm -f "$SSH_CONFIG" "$SSH_PRIVATE_KEY"
|
||||
}
|
||||
|
||||
trap cleanup EXIT
|
||||
|
||||
umask 077
|
||||
printf '%s\n' "$GITEA_BACKUP_SSH_KEY" > "$SSH_PRIVATE_KEY"
|
||||
printf '%s\n' \
|
||||
"Host $GITEA_BACKUP_SSH_HOST" \
|
||||
" HostName zog" \
|
||||
" User \"Dan Crosby\"" \
|
||||
" IdentityFile $SSH_PRIVATE_KEY" \
|
||||
" IdentitiesOnly yes" \
|
||||
" HostKeyAlias zog" \
|
||||
> "$SSH_CONFIG"
|
||||
|
||||
ensure_tool aws awscli
|
||||
ensure_tool restic restic
|
||||
ensure_tool sqlite3 sqlite
|
||||
|
||||
require_backup_bucket() {
|
||||
if ! aws s3api head-bucket \
|
||||
--bucket "$GITEA_BACKUP_BUCKET" \
|
||||
--endpoint-url "$DO_SPACES_ENDPOINT" >/dev/null 2>&1; then
|
||||
echo "ERROR: DigitalOcean Space '$GITEA_BACKUP_BUCKET' is unavailable."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
initialize_repository_if_needed() {
|
||||
if restic cat config >/dev/null 2>&1; then
|
||||
return
|
||||
fi
|
||||
|
||||
echo "Initializing encrypted Restic repository."
|
||||
restic init
|
||||
}
|
||||
|
||||
require_initialized_repository() {
|
||||
if restic cat config >/dev/null 2>&1; then
|
||||
return
|
||||
fi
|
||||
|
||||
echo "ERROR: The encrypted Restic repository is not initialized."
|
||||
exit 1
|
||||
}
|
||||
|
||||
prepare_staging_data() {
|
||||
safe_reset_directory "$STAGING_ROOT"
|
||||
mkdir -p "$STAGING_ROOT/gitea"
|
||||
mkdir -p "$STAGING_ROOT/repositories/admin"
|
||||
|
||||
ssh -F "$SSH_CONFIG" -o BatchMode=yes -o ConnectTimeout=15 "$GITEA_BACKUP_SSH_HOST" true
|
||||
ssh -F "$SSH_CONFIG" -o BatchMode=yes -o ConnectTimeout=15 "$GITEA_BACKUP_SSH_HOST" \
|
||||
"mkdir -p '$REMOTE_SNAPSHOT_DIR'"
|
||||
ssh -F "$SSH_CONFIG" -o BatchMode=yes -o ConnectTimeout=15 "$GITEA_BACKUP_SSH_HOST" \
|
||||
"sqlite3 '$REMOTE_DATABASE' \".backup '$REMOTE_DATABASE_SNAPSHOT'\""
|
||||
|
||||
scp -O -F "$SSH_CONFIG" -o BatchMode=yes -o ConnectTimeout=15 -pr \
|
||||
"$GITEA_BACKUP_SSH_HOST:$GITEA_REMOTE_ROOT/config" \
|
||||
"$STAGING_ROOT/"
|
||||
|
||||
for directory in "${GITEA_DATA_DIRECTORIES[@]}"; do
|
||||
scp -O -F "$SSH_CONFIG" -o BatchMode=yes -o ConnectTimeout=15 -pr \
|
||||
"$GITEA_BACKUP_SSH_HOST:$GITEA_REMOTE_ROOT/data/gitea/$directory" \
|
||||
"$STAGING_ROOT/gitea/"
|
||||
done
|
||||
|
||||
scp -O -F "$SSH_CONFIG" -o BatchMode=yes -o ConnectTimeout=15 -p \
|
||||
"$GITEA_BACKUP_SSH_HOST:$REMOTE_DATABASE_SNAPSHOT" \
|
||||
"$STAGING_ROOT/gitea/gitea.db"
|
||||
|
||||
remove_remote_database_snapshot
|
||||
|
||||
scp -O -F "$SSH_CONFIG" -o BatchMode=yes -o ConnectTimeout=15 -pr \
|
||||
"$GITEA_BACKUP_SSH_HOST:$GITEA_REMOTE_ROOT/data/lfs" \
|
||||
"$STAGING_ROOT/"
|
||||
|
||||
git clone --mirror \
|
||||
"$GITEA_HTTP_URL" \
|
||||
"$STAGING_ROOT/repositories/admin/eagle0.git"
|
||||
|
||||
local database_check
|
||||
database_check=$(sqlite3 "$STAGING_ROOT/gitea/gitea.db" "PRAGMA integrity_check;")
|
||||
if [ "$database_check" != "ok" ]; then
|
||||
echo "ERROR: Gitea database snapshot failed SQLite integrity checking."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git --git-dir="$STAGING_ROOT/repositories/admin/eagle0.git" fsck --full
|
||||
|
||||
local gitea_version
|
||||
gitea_version=$(curl --fail --silent "$GITEA_VERSION_URL")
|
||||
local main_commit
|
||||
main_commit=$(git --git-dir="$STAGING_ROOT/repositories/admin/eagle0.git" rev-parse refs/heads/main)
|
||||
local captured_at
|
||||
captured_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
printf '%s\n' \
|
||||
"captured_at=$captured_at" \
|
||||
"gitea_version=$gitea_version" \
|
||||
"main_commit=$main_commit" \
|
||||
"source_host=$GITEA_BACKUP_SSH_HOST" \
|
||||
"source_root=$GITEA_REMOTE_ROOT" \
|
||||
> "$STAGING_ROOT/manifest.txt"
|
||||
}
|
||||
|
||||
verify_restored_snapshot() {
|
||||
safe_reset_directory "$VERIFY_ROOT"
|
||||
restic restore latest --host zog --tag gitea --target "$VERIFY_ROOT"
|
||||
|
||||
local restored_root="$VERIFY_ROOT/${STAGING_ROOT#/}"
|
||||
local database_check
|
||||
database_check=$(sqlite3 "$restored_root/gitea/gitea.db" "PRAGMA integrity_check;")
|
||||
if [ "$database_check" != "ok" ]; then
|
||||
echo "ERROR: Restored Gitea database failed SQLite integrity checking."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git --git-dir="$restored_root/repositories/admin/eagle0.git" fsck --full
|
||||
test -s "$restored_root/manifest.txt"
|
||||
test -d "$restored_root/lfs"
|
||||
echo "Full restore verification completed successfully."
|
||||
}
|
||||
|
||||
run_backup() {
|
||||
require_backup_bucket
|
||||
initialize_repository_if_needed
|
||||
prepare_staging_data
|
||||
|
||||
restic backup "$STAGING_ROOT" --host zog --tag gitea
|
||||
restic forget \
|
||||
--host zog \
|
||||
--tag gitea \
|
||||
--group-by host,tags \
|
||||
--keep-daily 14 \
|
||||
--keep-weekly 8 \
|
||||
--keep-monthly 12 \
|
||||
--keep-yearly 3
|
||||
|
||||
if [ "${FULL_RESTORE_VERIFY:-false}" = "true" ]; then
|
||||
restic check --read-data
|
||||
verify_restored_snapshot
|
||||
else
|
||||
restic check --read-data-subset=5%
|
||||
fi
|
||||
|
||||
restic snapshots --host zog --tag gitea
|
||||
}
|
||||
|
||||
run_maintenance() {
|
||||
require_initialized_repository
|
||||
restic forget \
|
||||
--host zog \
|
||||
--tag gitea \
|
||||
--keep-daily 14 \
|
||||
--keep-weekly 8 \
|
||||
--keep-monthly 12 \
|
||||
--keep-yearly 3 \
|
||||
--prune
|
||||
restic check --read-data-subset=10%
|
||||
}
|
||||
|
||||
case "$BACKUP_MODE" in
|
||||
backup)
|
||||
run_backup
|
||||
;;
|
||||
maintenance)
|
||||
run_maintenance
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 [backup|maintenance]"
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,15 @@
|
||||
version: 1
|
||||
|
||||
sources:
|
||||
- channel: resolute main universe
|
||||
url: https://ports.ubuntu.com/ubuntu-ports
|
||||
- channel: resolute-security main universe
|
||||
url: https://ports.ubuntu.com/ubuntu-ports
|
||||
- channel: resolute-updates main universe
|
||||
url: https://ports.ubuntu.com/ubuntu-ports
|
||||
|
||||
archs:
|
||||
- "arm64"
|
||||
|
||||
packages:
|
||||
- "linux-perf"
|
||||
@@ -45,7 +45,7 @@ services:
|
||||
EAGLE_POSTGRES_IDLE_TIMEOUT_MILLIS: "${EAGLE_POSTGRES_IDLE_TIMEOUT_MILLIS:-120000}"
|
||||
EAGLE_POSTGRES_MAX_LIFETIME_MILLIS: "${EAGLE_POSTGRES_MAX_LIFETIME_MILLIS:-1500000}"
|
||||
EAGLE_POSTGRES_KEEPALIVE_TIME_MILLIS: "${EAGLE_POSTGRES_KEEPALIVE_TIME_MILLIS:-300000}"
|
||||
EAGLE_POSTGRES_LEAK_DETECTION_THRESHOLD_MILLIS: "${EAGLE_POSTGRES_LEAK_DETECTION_THRESHOLD_MILLIS:-60000}"
|
||||
EAGLE_POSTGRES_LEAK_DETECTION_THRESHOLD_MILLIS: "${EAGLE_POSTGRES_LEAK_DETECTION_THRESHOLD_MILLIS:-0}"
|
||||
# JWT public key for token validation (auth service handles signing)
|
||||
# Reads from /etc/eagle0/keys/public.pem via shared volume
|
||||
# Auth token for Shardok on Hetzner (required)
|
||||
@@ -108,7 +108,7 @@ services:
|
||||
EAGLE_POSTGRES_IDLE_TIMEOUT_MILLIS: "${EAGLE_POSTGRES_IDLE_TIMEOUT_MILLIS:-120000}"
|
||||
EAGLE_POSTGRES_MAX_LIFETIME_MILLIS: "${EAGLE_POSTGRES_MAX_LIFETIME_MILLIS:-1500000}"
|
||||
EAGLE_POSTGRES_KEEPALIVE_TIME_MILLIS: "${EAGLE_POSTGRES_KEEPALIVE_TIME_MILLIS:-300000}"
|
||||
EAGLE_POSTGRES_LEAK_DETECTION_THRESHOLD_MILLIS: "${EAGLE_POSTGRES_LEAK_DETECTION_THRESHOLD_MILLIS:-60000}"
|
||||
EAGLE_POSTGRES_LEAK_DETECTION_THRESHOLD_MILLIS: "${EAGLE_POSTGRES_LEAK_DETECTION_THRESHOLD_MILLIS:-0}"
|
||||
# JWT public key for token validation (auth service handles signing)
|
||||
# Reads from /etc/eagle0/keys/public.pem via shared volume
|
||||
SHARDOK_AUTH_TOKEN: "${SHARDOK_AUTH_TOKEN:-}"
|
||||
@@ -234,6 +234,10 @@ services:
|
||||
- "auth:40033"
|
||||
- "--jfr-sidecar-addr"
|
||||
- "${JFR_SIDECAR_ADDR:-jfr-sidecar:8081}" # Can be switched for blue-green
|
||||
- "--shardok-profile-addr"
|
||||
- "${SHARDOK_ADDRESS}"
|
||||
- "--shardok-profile-tls"
|
||||
- "true"
|
||||
- "--http-port"
|
||||
- "8080"
|
||||
- "--maps-dir"
|
||||
@@ -247,6 +251,8 @@ services:
|
||||
DO_SPACES_SECRET_KEY: "${ADMIN_S3_SECRET_KEY:-}"
|
||||
# GitHub PAT for map editor PR creation (scoped to contents:write + pull_requests:write)
|
||||
GITHUB_TOKEN: "${GITHUB_TOKEN:-}"
|
||||
# Reuse Eagle's Shardok endpoint and auth token for admin-only CPU profiling.
|
||||
SHARDOK_AUTH_TOKEN: "${SHARDOK_AUTH_TOKEN:-}"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
# No external port - accessed via nginx at admin.eagle0.net
|
||||
|
||||
@@ -156,7 +156,7 @@ CREATE TABLE action_changed_provinces (
|
||||
support_delta REAL,
|
||||
-- round properties
|
||||
set_has_acted INTEGER, -- 0/1/NULL
|
||||
set_ruler_is_traveling INTEGER,
|
||||
set_ruler_is_visiting_town INTEGER,
|
||||
-- ruling faction changes
|
||||
clear_ruling_faction_id INTEGER NOT NULL DEFAULT 0,
|
||||
new_ruling_faction_id INTEGER,
|
||||
|
||||
@@ -72,7 +72,7 @@ Only post a selected command that is currently listed as available. A successful
|
||||
Some actions enter a temporary subcommand state. For example, trading is operated as:
|
||||
|
||||
```text
|
||||
post-json <province-id> TravelSelectedCommand {}
|
||||
post-json <province-id> VisitTownSelectedCommand {}
|
||||
post-json <province-id> TradeSelectedCommand {"tradeType":"BUY_FOOD","amount":<amount>}
|
||||
post-return <province-id>
|
||||
```
|
||||
@@ -131,6 +131,17 @@ Shardok update with no available commands clears the previous command list.
|
||||
|
||||
Use `shardok-commands-json` or `shardok-units-json` when the concise output omits a needed field.
|
||||
|
||||
The initial Eagle state includes the complete strategic province graph. Inspect it with:
|
||||
|
||||
```text
|
||||
eagle-map
|
||||
eagle-map-json
|
||||
```
|
||||
|
||||
Both forms list every province and all of its neighbors by ID and name. Use the JSON form when exact IDs or
|
||||
machine-readable relationships are needed; unlike March destinations, this is the complete map graph and is not
|
||||
filtered by the commands currently available.
|
||||
|
||||
During battle setup, `shardok-commands` prints any server-recommended unit placements in order and identifies the
|
||||
ordinary current command index for the next recommendation:
|
||||
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
# Tutorial Playthrough Notes
|
||||
|
||||
This is a working guide for playing through the Eagle0 tutorial without losing. It is written from live text-client
|
||||
testing, so treat it as a practical checklist rather than a complete design reference.
|
||||
|
||||
## Core Rule
|
||||
|
||||
Do not risk Sadar Rakon unless the outcome is overwhelmingly favorable. In tutorial games, Sadar is the human faction
|
||||
leader; if he is captured, the server treats the human player as defeated and the game ends.
|
||||
|
||||
## Strategic Goals
|
||||
|
||||
- Survive the opening tutorial battle.
|
||||
- Rebuild Onmaa enough to stabilize support and loyalty.
|
||||
- Expand carefully from Onmaa/Nikemi with enough force to avoid leader capture.
|
||||
- Trigger the major strategic tutorial event where The Eagle appears. In code, this happens when Sadar controls at
|
||||
least four provinces or Hedrick's faction has no provinces.
|
||||
|
||||
## Tactical Safety Rules
|
||||
|
||||
- Avoid sending Sadar alone into a tactical battle.
|
||||
- If a battle turns into a long chase, reassess instead of blindly sweeping the map.
|
||||
- Prefer preserving Sadar over forcing a marginal province capture.
|
||||
- When the text client shows no attack command, do not assume the enemy is helpless; they may still be able to win by
|
||||
holding, hiding, or outlasting the attacker.
|
||||
|
||||
## Known Failure
|
||||
|
||||
In one previous Kojaria attempt, Sadar attacked with one battalion, chased Hedrick across the map, failed to force a
|
||||
decisive win, and was captured. The client did not clearly announce game over at the time; the save later showed Kojaria
|
||||
held by Hedrick and Sadar listed under `captured_heroes`.
|
||||
|
||||
## Live Run Log
|
||||
|
||||
### Fresh Tutorial Run
|
||||
|
||||
- Started: game `-3568010721410464682`.
|
||||
- Leader: Sadar Rakon.
|
||||
- Current objective: start the tutorial from the opening battle and record safe choices.
|
||||
- Opening battle setup:
|
||||
- Rakon's Loyalists at `(8,10)`.
|
||||
- Onmaa Defenders at `(7,10)`.
|
||||
- Hunters of the Steppe at `(8,11)`.
|
||||
- Rationale: keep the two infantry units together on the west-facing line and keep the longbowmen directly behind
|
||||
them. Tarn's force begins west/southwest of the defender setup area, and Elena/John arrive later, so the opening
|
||||
goal is to preserve the core army rather than overextend.
|
||||
- Opening battle, day 1: no attacks were available. Ended the turn rather than starting fires or moving forward.
|
||||
- Opening battle, day 2:
|
||||
- Used Hunters of the Steppe for an archery attack against the visible unit at `(7,9)`.
|
||||
- Used Rakon's Loyalists for an archery attack against the unit at `(6,10)`.
|
||||
- Used Onmaa Defenders for melee against the unit at `(6,10)`.
|
||||
- Avoided the offered duel from Rakon's Loyalists against the unit at `(7,9)`. If that unit is Tarn, the duel risks
|
||||
Sadar directly before reinforcements have arrived.
|
||||
- Opening battle, days 3-5:
|
||||
- On day 3, only Rakon's Loyalists and Hunters of the Steppe had commands. Used both for archery against the unit at
|
||||
`(6,10)`.
|
||||
- On days 4 and 5, only Hunters of the Steppe had commands. Used archery against the nearest non-Tarn pressure unit
|
||||
rather than retreating or entering melee.
|
||||
- This is a danger signal: if Sadar's unit disappears from the command list this early, the opening formation may have
|
||||
absorbed too much damage. Keep future attempts more defensive and consider moving Sadar back instead of using every
|
||||
available attack.
|
||||
- Elena Fyar arrived on day 6 as actor `7`, confirming the `elena_reinforcement_round_5` tutorial event. She spawned
|
||||
far southwest of the fight with Holy Wave available. Moved her toward the battle first instead of spending Holy Wave
|
||||
while isolated; the move consumed her action for the turn.
|
||||
- John Ranil arrived on day 8 as actor `8`, confirming the `ranil_reinforcement_round_7` tutorial event.
|
||||
- After John arrived:
|
||||
- Moved Hunters of the Steppe back to `(10,11)` instead of taking a weak melee attack.
|
||||
- Moved Elena to `(8,7)` to close with the enemy line.
|
||||
- Moved John to `(6,4)` to approach from the west.
|
||||
- Do not expect reinforcements to appear in immediate melee range. They need one or more turns of marching before they
|
||||
can rescue the original defender units.
|
||||
- Day 10 Holy Wave timing:
|
||||
- Moved John adjacent to Elena first.
|
||||
- Used Elena's Holy Wave only after John was adjacent, so the action could inspire more than Elena herself.
|
||||
- If Holy Wave is available immediately when Elena appears, do not spend it unless she is already adjacent to a
|
||||
friendly unit or there is a specific emergency reason.
|
||||
- Day 11 reinforcement attack:
|
||||
- Elena got melee commands against `(7,9)` and `(8,10)`. Chose `(8,10)` first because `(7,9)` had repeatedly looked
|
||||
like the Tarn/front-leader unit.
|
||||
- Moved John to `(9,9)`, which opened a charge against `(8,10)`.
|
||||
- Used John's charge and then his follow-up melee against `(8,10)`.
|
||||
- Practical rule: once Elena and John arrive, pair their attacks on the same non-Tarn unit before taking leader-risky
|
||||
engagements.
|
||||
- Probable defeat/reset signal:
|
||||
- Continued John melee attacks against `(8,10)` through day 14.
|
||||
- After the day-14 melee, the Shardok stream emitted `shardok-reset` for the same battle id
|
||||
`ce7be1272b8b4456_1_df540967`.
|
||||
- The battle token reset to `6` and commands returned to the original opening placement commands, while Eagle status
|
||||
still reported `BATTLE_IN_PROGRESS`.
|
||||
- This most likely means the battle was lost, but the text client did not surface the loss explicitly. Treat a reset
|
||||
after a long battle as a probable defeat signal unless server state proves otherwise.
|
||||
- For the live run, replay the reset battle with a more conservative setup and keep Sadar out of the first contact
|
||||
line.
|
||||
- Replay setup after reset:
|
||||
- Rakon's Loyalists at `(10,11)`.
|
||||
- Onmaa Defenders at `(8,10)`.
|
||||
- Hunters of the Steppe at `(9,11)`.
|
||||
- Rationale: put Sadar deep, use Onmaa Defenders as the first contact screen, and keep archers close enough to shoot
|
||||
without becoming the front line.
|
||||
- Replay tactical adjustment:
|
||||
- Day 2: enemy units still reached Sadar quickly. Moved Rakon's Loyalists away instead of dueling or meleeing.
|
||||
- Day 3: moved Sadar again, then moved Hunters of the Steppe out of melee contact.
|
||||
- Day 4-5: continued withdrawing Sadar north/east and pulled the archer back instead of trading melee.
|
||||
- Elena arrived with Sadar still alive and mobile. Move Elena east immediately; do not spend Holy Wave while isolated.
|
||||
- John arrived after Elena. Use Elena for contact, march John east, and let Sadar contribute only with archery while
|
||||
he remains out of melee.
|
||||
- Second probable defeat/reset:
|
||||
- The replay kept Sadar alive through both reinforcements and reached day 20.
|
||||
- After repeated John melee against the remaining contact at `(6,7)`, Shardok emitted another `shardok-reset` for the
|
||||
same battle id and returned to the original placement command list.
|
||||
- This reproduced the reset behavior from the first attempt and likely means the replay was also lost. Stop treating
|
||||
the current live game as a successful tutorial source until the loss condition is visible in the text client or
|
||||
confirmed from server state.
|
||||
- Failed castle-hold variant:
|
||||
- Onmaa castle tiles are `(6,12)`, `(8,11)`, and `(10,11)`.
|
||||
- Rakon's Loyalists at `(10,11)`.
|
||||
- Onmaa Defenders at `(8,11)`.
|
||||
- Hunters of the Steppe at `(6,12)`.
|
||||
- Early result: this was much stronger than the previous formations. When no attacks were available, ended turn
|
||||
instead of moving off castles. When longbow shots were offered, used Hunters of the Steppe from `(6,12)`. When
|
||||
enemies stepped adjacent, used ordinary castle melee, but continued to avoid duels.
|
||||
- What failed:
|
||||
- The battle still reset after day 22, returning the same battle id to token `6`.
|
||||
- The late phase degraded into too much Holy Wave/Reduce and too little direct pressure. Treat that as the wrong
|
||||
plan for this tutorial battle.
|
||||
|
||||
### Successful Opening Battle
|
||||
|
||||
- Won in game `-3568010721410464682`, battle `ce7be1272b8b4456_1_df540967`.
|
||||
- Winning setup:
|
||||
- Sadar/Rakon's Loyalists at `(10,11)`.
|
||||
- Onmaa Defenders at `(8,11)`.
|
||||
- Hunters of the Steppe at `(6,12)`.
|
||||
- Main rule: hold the three castle tiles with the starting units until the enemy gives useful attacks. Do not walk the
|
||||
starting units off the castles just because movement commands are available.
|
||||
- Early turns:
|
||||
- End the first defender turn when only movement/fire commands are available.
|
||||
- Use Hunters of the Steppe for archery whenever possible, usually into the non-Ikhaan targets at `(5,10)`,
|
||||
`(5,11)`, or `(7,10)`.
|
||||
- Use ordinary melee from castle tiles when the enemy steps adjacent.
|
||||
- Sadar may duel non-Ikhaan targets. In the winning run, actor `4` used `CHALLENGE_DUEL` against targetUnit `2`.
|
||||
- Avoid Sadar's duel against targetUnit `3` at `(9,10)`; that target behaved like Ikhaan Tarn. Ordinary melee into
|
||||
that adjacent target was still used later, but the duel was skipped every time.
|
||||
- Reinforcements:
|
||||
- Elena arrived as actor `7`. March her directly toward the castle line instead of spending Holy Wave while isolated:
|
||||
`(11,3)`, `(8,5)`, `(8,8)`, then `(8,11)`.
|
||||
- John arrived as actor `8`. Rush him toward the castles: `(6,4)`, `(10,7)`, then `(8,10)`.
|
||||
- Once John and Elena reach the line, use their melee attacks immediately. In the winning run both repeatedly attacked
|
||||
the non-Ikhaan target at `(7,10)`.
|
||||
- After that target dropped, chase the remaining left-side contact with John and Elena together. John moved to
|
||||
`(4,11)`, Elena to `(5,10)`, and John finished the battle with melee at `(5,11)`.
|
||||
- Do not make Holy Wave or Reduce the plan. The successful run won without using Holy Wave or Reduce in the decisive
|
||||
phase; direct archery, castle melee, and reinforcement melee were enough.
|
||||
- Post-battle aftermath:
|
||||
- The battle resolved to Eagle commands and offered three captured heroes: `201`, `202`, and `203`.
|
||||
- Recruit attempts were refused for all three, then each was imprisoned.
|
||||
- After the captured-hero prompts cleared, normal strategic commands returned at province `14`, starting with
|
||||
`ImproveAvailableCommand`.
|
||||
|
||||
### Strategic Play After Opening Battle
|
||||
|
||||
- Immediate recovery priorities:
|
||||
- Improve Onmaa's economy and agriculture before starting a war.
|
||||
- Use Alms sparingly if support is low, but watch food and treasury.
|
||||
- Train existing heroes/units before marching. The opening win leaves enough time to build before attacking.
|
||||
- Troop organization notes:
|
||||
- Onmaa initially exposed only `LightInfantry` and `Longbowmen` as constructible in the command surface.
|
||||
- An invalid organize attempt tried to top up preexisting heavy units and hit the server error
|
||||
`HeavyInfantry was not among constructible battalion types Vector(LightInfantry, Longbowmen)`.
|
||||
- Safe commands in the live run were:
|
||||
- Top up the existing light infantry battalion.
|
||||
- Create a new longbow battalion. The text-client payload that worked used numeric enum value `4` for Longbowmen.
|
||||
- Do not assume existing heavy battalions can be reinforced just because they already exist in the province.
|
||||
- Marching rule:
|
||||
- Do not march Sadar in the first expansion army. Leave him in Onmaa and send the non-leader army instead.
|
||||
- The live run sent Elena, John, Agamemnon, and Old Marek from Onmaa to Nikemi, then on to Kojaria.
|
||||
- Send supplies forward after the army reaches Nikemi; do not strand the field army without food.
|
||||
|
||||
### Failed Hedrick Attack
|
||||
|
||||
- Target: Kojaria, held by Hedrick the Hedge-Merchant's faction.
|
||||
- Army: four non-Sadar heroes, about 1954 troops, including the new untrained longbow battalion.
|
||||
- Strategic safety result:
|
||||
- This was better than marching Sadar alone. A reset here did not immediately imply player defeat because Sadar
|
||||
stayed home.
|
||||
- It still appears to be a losing tactical plan.
|
||||
- Tactical setup:
|
||||
- Placed the army on the west edge and advanced toward Hedrick's single 1000-troop stack.
|
||||
- A first attempt surrounded Hedrick and used repeated melee. It reset after an infantry melee, likely a loss.
|
||||
- A second attempt used more spacing:
|
||||
- Move John/engineer to the firing lane first.
|
||||
- Use archery whenever it appears.
|
||||
- Move cavalry into contact only after ranged damage is available.
|
||||
- Avoid actor 0 infantry melee and avoid making Reduce/Holy Wave the core plan.
|
||||
- What worked better:
|
||||
- The stable pattern was archery from actor 0 when available, archery from the new longbows when available, and
|
||||
cavalry melee from Old Marek.
|
||||
- Extinguishing fire with the longbows was useful when the enemy lit the cavalry contact square.
|
||||
- What failed:
|
||||
- Even with repeated archery and cavalry melee, the battle dragged to day 12 and then reset after another cavalry
|
||||
melee.
|
||||
- A single late Reduce did not visibly break the stalemate.
|
||||
- The new longbow battalion had `0.0` training and `0.0` armament in the stale strategic snapshot, so it may have
|
||||
contributed much less than its troop count suggested.
|
||||
- Practical conclusion:
|
||||
- Do not attack Hedrick immediately after creating a raw longbow battalion.
|
||||
- Build a stronger army first: train and arm the new longbows, improve the economy/agriculture enough to support
|
||||
better reinforcements, and consider taking easier provinces or waiting until the army has more developed units.
|
||||
- If attacking Hedrick, treat his 1000-stack as a boss fight. The current safe-looking four-hero army was not enough
|
||||
to finish it reliably.
|
||||
@@ -287,8 +287,8 @@ You play **Sadar Rakon**, formerly Ikhaan Tarn's most trusted lieutenant. Tarn's
|
||||
| 5 | `tutorial_support_deadline` | December nag if support < 40: have Elena give alms now |
|
||||
| 6 | `guidance_expand` (script `tutorial_expansion`) | Once support is stable: keep developing, taxes coming in January |
|
||||
| 7 | `tutorial_taxes_collected` | January: gold/food collected. Now expand — March your warlord + most heroes to a neighbor, leave 1–2 behind |
|
||||
| 8 | `tutorial_ready_to_join` | A free hero in `{provinceName}` is ready to recruit → Travel + Recruit |
|
||||
| 9 | `tutorial_in_town` | When you Travel: explains Trade / Arm Troops / Divine / Recruit / Manage Prisoners; *Return* ends the turn |
|
||||
| 8 | `tutorial_ready_to_join` | A free hero in `{provinceName}` is ready to recruit → Visit Town + Recruit |
|
||||
| 9 | `tutorial_in_town` | When you Visit Town: explains Trade / Arm Troops / Divine / Recruit / Manage Prisoners; *Return* ends the turn |
|
||||
| 10 | `tutorial_faction_appears` | **The twist**: Fracture Covenant lands at Ingia and Soria. *The Eagle* commands them. Tarn may be with them |
|
||||
| 11 | `tutorial_hero_departed` | First King's hero abandons service — "something deeper is at work" |
|
||||
| 12 | `tutorial_hero_departed_again` | Second one in a different month — "something is wrong, I can feel it" (foreshadowing) |
|
||||
|
||||
@@ -78,7 +78,7 @@ This section lists quests with direct `QuestCommandChooser` handlers in `Fulfill
|
||||
|-------|---------|-------|
|
||||
| `GrandArmyQuest` | `GrandArmyQuestCommandChooser` | Uses OrganizeTroops to top off existing battalions and hire Light Infantry. Only executes if resources are sufficient to fully meet the troop target. |
|
||||
| `BattalionDiversityQuest` | `BattalionDiversityQuestCommandChooser` | Hires one battalion of a missing type to reach 3+ distinct types. Only attempts if the province already has 2+ existing types, has sufficient development (`meetsRequirements`), and gold/food surplus. |
|
||||
| `UpgradeBattalionQuest` | `UpgradeBattalionQuestCommandChooser` | Walks a priority tree per turn: arm-completes (with optional travel and/or food sale setup) → train-completes → march in a pre-qualified neighbor battalion → organize/top-off of the target type → march in an unqualified neighbor battalion → develop Economy/Agriculture → train progress → develop Infrastructure → arm progress. Only toggles Travel when it directly enables an arm-completes finisher. |
|
||||
| `UpgradeBattalionQuest` | `UpgradeBattalionQuestCommandChooser` | Walks a priority tree per turn: arm-completes (with optional Visit Town and/or food sale setup) → train-completes → march in a pre-qualified neighbor battalion → organize/top-off of the target type → march in an unqualified neighbor battalion → develop Economy/Agriculture → train progress → develop Infrastructure → arm progress. Only chooses Visit Town when it directly enables an arm-completes finisher. |
|
||||
|
||||
### Reconnaissance Quests
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ These values do different jobs; they are not interchangeable ratings that simply
|
||||
- **Training** scales the battalion type's melee and missile damage bonuses. Higher training therefore means more damage per troop. It also affects some command-specific odds, including the chance to stun on a [Charge](/shardok-commands/#charge).
|
||||
- **Armament** scales the battalion type's resistances and reduces incoming normal damage. Each type and damage category has its own resistance profile, so the same ARM value does not protect every battalion equally. Penetrating damage bypasses this normal armament reduction. If the unit can use [Archery](/shardok-commands/#archery), armament also determines its starting volleys: currently one volley per 10 ARM, rounded down, plus any bonus from its battalion type.
|
||||
- **Morale** changes the battalion's damage swing. Most units also need at least 10 morale to use normal tactical commands.
|
||||
- **Hero vigor** is an action constraint and current energy reserve, not battalion armor. A hero-bearing unit normally needs at least 10 vigor to act, and special effects can spend or reduce it.
|
||||
- **Hero vigor** is an action constraint and current energy reserve, not battalion armor. It affects maximum AP and command access; at zero vigor, the hero-led unit is captured and removed from active battle. See [Vigor](/heroes/#vigor).
|
||||
- **Troop count** multiplies battalion damage: more surviving troops deliver more total battalion damage. It also determines how many soldiers remain available to absorb casualties.
|
||||
- **Battalion type** supplies the base damage, training bonus, resistance profile, armament benefit, movement costs, charge multiplier, and special capabilities.
|
||||
- **Hero stats** contribute separately. Charisma and bravery affect battalion combat swings, while a hero also contributes personal damage and enables stat- or profession-based commands.
|
||||
@@ -55,13 +55,15 @@ An exceptional formation created through necromantic effects rather than ordinar
|
||||
|
||||
## Heroes and units
|
||||
|
||||
A Shardok unit normally combines one hero and one battalion. The hero supplies stats, vigor, profession commands, and special capabilities; the battalion supplies troops, type, training, armament, movement, morale, and damage profile. A profession alone may not be enough: casting requires a casting-capable battalion, and Ranger stealth requires a stealth-capable one.
|
||||
A Shardok unit normally combines one hero and one battalion. The hero supplies stats, [vigor](/heroes/#vigor), profession commands, personal damage, and special capabilities; the battalion supplies troops, type, training, armament, movement, morale, and its damage profile. The hero's Charisma and Bravery also affect the battalion's combat swing. A profession alone may not be enough: casting requires a casting-capable battalion, and Ranger stealth requires a stealth-capable one.
|
||||
|
||||
The two parts remain one tactical unit. If the battalion reaches zero troops, it is destroyed. If the attached hero reaches zero vigor, the hero-led unit is captured and removed from active battle; the surviving battalion does not detach and continue fighting independently.
|
||||
|
||||
## Creating and changing battalions
|
||||
|
||||
[**Organize Troops**](/commands/#organize-troops) creates, resizes, merges, transfers, or dismisses battalions. New troops cost gold and begin without training or armament. Transfers preserve weighted training and armament. Empty battalions are removed.
|
||||
|
||||
Use [**Train**](/commands/#train) to improve eligible battalions in the province and [**Arm Troops**](/commands/#arm-troops) while traveling to buy higher armament. Province economy and agriculture affect which types can be raised; effective infrastructure limits equipment.
|
||||
Use [**Train**](/commands/#train) to improve eligible battalions in the province and [**Arm Troops**](/commands/#arm-troops) while visiting town to buy higher armament. Province economy and agriculture affect which types can be raised; effective infrastructure limits equipment.
|
||||
|
||||
## Movement, food, and battle
|
||||
|
||||
|
||||
@@ -72,7 +72,9 @@ Most command phase actions spend the acting province's turn.
|
||||
|
||||
<b>Available when:</b> a diplomatic option is valid against another faction and an eligible messenger hero can be sent.
|
||||
|
||||
<b>Effect:</b> Send a hero to another faction with a diplomatic proposal. The target faction receives the offer at the end of the current round, after any battles this round have taken place. Truce, invitation, alliance, break-alliance, and ransom options create [incoming diplomacy offers](/factions/#incoming-diplomacy-offers) for the target faction and story-generation requests. Most diplomacy options spend gold, remove the messenger hero from the acting province, and cost the messenger vigor.
|
||||
<b>Effect:</b> Send a hero to another faction with a diplomatic proposal. The target faction receives the offer at the end of the current round, after any battles this round have taken place. Truce, invitation, alliance, break-alliance, and ransom options create [incoming diplomacy offers](/factions/#incoming-diplomacy-offers) for the target faction and story-generation requests. Most diplomacy options spend gold and cost the messenger vigor.
|
||||
|
||||
For truce, invitation, alliance, and break-alliance missions, the envoy leaves the acting province immediately and is unavailable for the rest of the current round, including its battles. The envoy normally returns during **Diplomacy Resolution** near the end of that same round and can act again next round. If the origin province has been lost, the envoy returns elsewhere in the faction when possible. An imprisoned envoy does not return.
|
||||
|
||||
<b>Selection:</b> choose the diplomacy option, target faction, and sent hero.
|
||||
|
||||
@@ -164,7 +166,7 @@ Look for growth or attack opportunities. The vassal first tries to march into ne
|
||||
|
||||
<b>Available when:</b> the acting province has movable heroes, battalions, gold, or food and at least one valid destination.
|
||||
|
||||
<b>Effect:</b> Move heroes, battalions, gold, and food from one province to another. The selected units and supplies leave the origin province immediately, each marching hero spends vigor and gains wisdom XP, and the destination receives an [incoming army](/provinces/#incoming-armies) scheduled for the next round.
|
||||
<b>Effect:</b> Move heroes, battalions, gold, and food from one province to another. March is inter-province movement and always has a destination; [Visit Town](#visit-town) is only a local visit within the current province. The selected units and supplies leave the origin province immediately, each marching hero spends vigor and gains wisdom XP, and the destination receives an [incoming army](/provinces/#incoming-armies) scheduled for the next round.
|
||||
|
||||
If the destination is already ruled by your faction when the army arrives, the heroes, battalions, gold, and food enter the province during the next round's Province Move Resolution phase, before riots, vassal commands, and player commands.
|
||||
|
||||
@@ -279,21 +281,23 @@ Shipments arrive during the next round's Province Move Resolution phase, before
|
||||
|
||||
<b>Selection:</b> choose the acting hero.
|
||||
|
||||
### Travel
|
||||
### Visit Town
|
||||
|
||||
<b>Hero requirements:</b> None.
|
||||
<b>Hero requirements:</b> None. The acting province's ruling hero makes the visit.
|
||||
|
||||
<b>Vigor cost:</b> 15.
|
||||
|
||||
<b>Available when:</b> the acting province can enter the traveling command state.
|
||||
<b>Available when:</b> the acting province's ruling hero has at least 40 vigor, the hero is not already visiting town, and there is no blizzard.
|
||||
|
||||
<b>Effect:</b> Unlock the traveling commands below. Once you are traveling, you can use as many traveling commands as you want in the same round before using Return to come back to camp.
|
||||
<b>Effect:</b> Have the ruling hero leave camp to visit the town inside the acting province. Visit Town does not move any hero, battalion, or supplies to another province, so it has no destination; use [March](#march) for inter-province movement.
|
||||
|
||||
<b>Selection:</b> no additional choices.
|
||||
Visit Town spends the ruling hero's vigor and unlocks whichever town commands below are currently eligible. You can use as many eligible town commands as you want in the same round before using Return to go back to camp. If no town activity currently qualifies, Return may be the only command available.
|
||||
|
||||
## Traveling Commands
|
||||
<b>Selection:</b> no additional choices. Visit Town always uses the current province and never asks you to select a destination.
|
||||
|
||||
You must use Travel before these commands appear. Once you are traveling, you can use as many traveling commands as you want in one round before using Return to come back to camp.
|
||||
## Town Commands
|
||||
|
||||
You must use Visit Town before these local commands appear. The ruling hero remains in the current province; only the available command set changes from camp duties to town activities. While visiting town, you can use as many eligible town commands as you want in one round before using Return to come back to camp.
|
||||
|
||||
### Arm Troops
|
||||
|
||||
@@ -301,7 +305,7 @@ You must use Travel before these commands appear. Once you are traveling, you ca
|
||||
|
||||
<b>Vigor cost:</b> None.
|
||||
|
||||
<b>Available when:</b> you are traveling, the province has gold, and at least one battalion can raise ARM without exceeding effective infrastructure.
|
||||
<b>Available when:</b> the ruling hero is visiting town, the province has gold, and at least one battalion can raise ARM without exceeding effective infrastructure.
|
||||
|
||||
<b>Effect:</b> Spend gold to raise armament on existing battalions. New armament must be higher than the battalion's current armament and cannot exceed the province's effective infrastructure. The province spends gold, the price index may change, and each armed battalion records a backstory event.
|
||||
|
||||
@@ -313,7 +317,7 @@ You must use Travel before these commands appear. Once you are traveling, you ca
|
||||
|
||||
<b>Vigor cost:</b> None.
|
||||
|
||||
<b>Available when:</b> you are traveling and an [unaffiliated hero](/heroes/#unaffiliated-heroes) quest is available in the province.
|
||||
<b>Available when:</b> the ruling hero is visiting town and an [unaffiliated hero](/heroes/#unaffiliated-heroes) quest is available in the province.
|
||||
|
||||
<b>Effect:</b> Decline one of the unaffiliated hero quests available in the acting province.
|
||||
|
||||
@@ -327,7 +331,7 @@ You must use Travel before these commands appear. Once you are traveling, you ca
|
||||
|
||||
<b>Gold cost:</b> 100 gold per selected hero, multiplied by the province [price index](/provinces/#price-index) and rounded to the nearest whole gold for each hero. The total cost is that per-hero cost multiplied by the number of selected heroes. For example, divining 3 heroes at a 1.25 price index costs 375 gold.
|
||||
|
||||
<b>Available when:</b> you are traveling, the province can pay the cost for at least one hero, and there are free heroes who are not yet ready to join your faction but might be won over.
|
||||
<b>Available when:</b> the ruling hero is visiting town, the province can pay the cost for at least one hero, and there are free heroes who are not yet ready to join your faction but might be won over.
|
||||
|
||||
<b>Effect:</b> Spend gold to divine one or more [free heroes](/factions/#free-heroes). For each divined hero, you receive details about what could impress them enough to become recruitable. Fulfill that ask, and the hero can later appear under Recruit Heroes. The province spends gold, and the price index may change.
|
||||
|
||||
@@ -339,7 +343,7 @@ You must use Travel before these commands appear. Once you are traveling, you ca
|
||||
|
||||
<b>Vigor cost:</b> None.
|
||||
|
||||
<b>Available when:</b> you are traveling and the province has prisoner heroes with available management options.
|
||||
<b>Available when:</b> the ruling hero is visiting town and the province has prisoner heroes with available management options.
|
||||
|
||||
<b>Effect:</b> Choose what happens to ordinary prisoners in the province. These are heroes who are already in the prisoner state, not newly captured heroes still waiting for a battle aftermath decision.
|
||||
|
||||
@@ -383,21 +387,23 @@ Ordinary prisoners cannot be returned this way. Faction leader prisoners cannot
|
||||
|
||||
<b>Vigor cost:</b> None.
|
||||
|
||||
<b>Available when:</b> you are traveling and unaffiliated heroes in the province are willing to join.
|
||||
<b>Available when:</b> the province is ruled by one of the acting faction's faction leaders, that ruling faction leader is visiting town, and at least one unaffiliated hero in the province currently qualifies to join that faction. The visitor may be any faction leader; it does not have to be the faction head. Vassals cannot use this command.
|
||||
|
||||
<b>Effect:</b> Recruit available unaffiliated heroes in the acting province. Recruited heroes join the faction at starting loyalty, move from the province's unaffiliated hero list into its ruling faction heroes, and receive a recruitment backstory event.
|
||||
|
||||
<b>Selection:</b> choose the heroes to recruit.
|
||||
|
||||
See [**What `WOULD_JOIN` Means**](/factions/#what-would_join-means) for the recruitment status, traveler exception, and separate **Please Recruit Me** behavior in vassal-ruled provinces.
|
||||
|
||||
### Return
|
||||
|
||||
<b>Hero requirements:</b> None.
|
||||
|
||||
<b>Vigor cost:</b> None.
|
||||
|
||||
<b>Available when:</b> you are traveling.
|
||||
<b>Available when:</b> the ruling hero is visiting town.
|
||||
|
||||
<b>Effect:</b> Return from the traveling command state to the normal strategic flow.
|
||||
<b>Effect:</b> Return the ruling hero from the current province's town to camp and end that province's turn.
|
||||
|
||||
<b>Selection:</b> no additional choices.
|
||||
|
||||
@@ -407,7 +413,7 @@ Ordinary prisoners cannot be returned this way. Faction leader prisoners cannot
|
||||
|
||||
<b>Vigor cost:</b> None.
|
||||
|
||||
<b>Available when:</b> you are traveling and the province has enough gold or food for at least one trade.
|
||||
<b>Available when:</b> the ruling hero is visiting town and the province has enough gold or food for at least one trade.
|
||||
|
||||
<b>Effect:</b> Buy or sell food using the acting province's gold and food stores. Food buy and sell prices are based on the province price index. Buying spends gold and adds food; selling removes food and adds gold. The price index may change after the trade.
|
||||
|
||||
@@ -425,7 +431,7 @@ These commands appear only when the game is resolving a special situation, respo
|
||||
|
||||
<b>Available when:</b> an attack decision is pending.
|
||||
|
||||
<b>Effect:</b> Choose how to respond to a pending attack opportunity or battle decision. The command shows involved armies, acting units, and the available attack decision types. See [Attack and Defense Decisions](/concepts/eagle-to-shardok/#attack-and-defense-decisions) for how this fits into battle creation.
|
||||
<b>Effect:</b> Choose how to respond to a pending attack opportunity or battle decision. The command shows involved armies, acting units, and the available attack decision types. **Advance** commits the army to the attack. **Withdraw** sends it back without fighting. **Demand Tribute** lets you request a specific amount of the province's gold and food, up to the displayed maximum; the demand cannot be zero. The defender later chooses whether to pay the full demand or refuse it. See [Attack and Defense Decisions](/concepts/eagle-to-shardok/#attack-and-defense-decisions) and [Resolving a tribute demand](/concepts/march-timeline/#resolving-a-tribute-demand).
|
||||
|
||||
<b>Selection:</b> choose one available attack decision.
|
||||
|
||||
@@ -567,7 +573,9 @@ Gift success is probabilistic. The chance is based on the province leader's Char
|
||||
|
||||
<b>Available when:</b> another faction tries to break an alliance.
|
||||
|
||||
<b>Effect:</b> Respond to a faction attempting to break an alliance.
|
||||
<b>Effect:</b> Respond to a faction attempting to break an alliance. Accepting the break replaces the alliance with a 12-month truce; it does not make the factions immediately hostile. The truce remains active through its reset month and changes to **Hostile** when a later **New Round** advances the date past that month. For example, a break accepted in January has a January reset date the following year and becomes hostile when February begins.
|
||||
|
||||
Imprisoning the envoy ends the alliance without a truce and makes both relationships **Hostile**. See [Diplomacy Outcomes](/factions/#diplomacy-outcomes) for the other imprisonment consequences.
|
||||
|
||||
<b>Selection:</b> choose the originating faction and the resolution.
|
||||
|
||||
@@ -615,7 +623,7 @@ Gift success is probabilistic. The chance is based on the province leader's Char
|
||||
|
||||
<b>Available when:</b> another faction demands tribute.
|
||||
|
||||
<b>Effect:</b> Decide whether to pay demanded tribute. The command shows the demanding factions, requested tribute, their rough strength, and the acting province's available food and gold.
|
||||
<b>Effect:</b> Decide whether to pay demanded tribute. The command shows the demanding factions, requested tribute, their rough strength, and the acting province's available food and gold. A demand must be accepted or refused in full; partial payment and counteroffers are not available. Paying requires the province to have the full requested amount, creates a 12-month truce between the factions, and turns back the demanding faction's hostile armies against the payer's provinces. Refusing transfers no resources and returns that faction's army to **Attacking**, after which normal defense selection and battle resolution continue. Neither response changes prestige directly, although a battle caused by refusal can later affect prestige. See [Resolving a tribute demand](/concepts/march-timeline/#resolving-a-tribute-demand).
|
||||
|
||||
<b>Selection:</b> choose the demanding faction and whether tribute is paid.
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ Moving armies arrive at their destinations, supplies move, and incoming hostile
|
||||
|
||||
### Hero Departures
|
||||
|
||||
Heroes who are ready to leave unaffiliated states or temporary situations may depart.
|
||||
The game checks affiliated heroes for [low-loyalty departure](/heroes/#loyalty-departure). Any hero with loyalty below 50 may leave at any time, and lower loyalty means a greater risk. A departing hero becomes an unaffiliated traveler in the same province and does not take the province's supplies.
|
||||
|
||||
### Unaffiliated Hero Actions
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ March is a delayed strategic order. Selected heroes, battalions, gold, and food
|
||||
| Province Move Resolution, round N+1 | Arrival checked | An army already friendly to the destination enters it. A hostile arrival waits outside rather than entering. |
|
||||
| After Player Commands, round N+1 | Hostile Army Setup | Hostile arrivals become attacking groups, one per faction, preserving their approach direction and carried supplies. |
|
||||
| Uncontested Conquest | No tactical defense needed | An unruled destination can be claimed without Shardok. Competing groups are compared by size, power, and a deterministic tie-breaker. |
|
||||
| Attack and Defense Decisions | A ruled province is contested | Attackers decide whether to press the attack; defenders may choose an army and fallback destination. Truces can turn armies back. |
|
||||
| Attack and Defense Decisions | A ruled province is contested | Attackers decide whether to press the attack or demand tribute; defenders answer tribute demands and may choose an army and fallback destination. Truces can turn armies back. |
|
||||
| Battle Request and Resolution | Shardok battle | Eagle sends armies, map, limited battle food, and victory conditions to Shardok, then waits for a result. |
|
||||
| Battle Aftermath | Consequences applied | Eagle resolves ownership, survivors, prisoners, supplies, devastation, prestige, and any allied-claimant decisions. |
|
||||
|
||||
@@ -27,6 +27,16 @@ A marching army does not occupy a neutral or enemy province during movement reso
|
||||
|
||||
An unruled province may be conquered without a tactical battle. A ruled and defended province can require attack and defense decisions followed by Shardok.
|
||||
|
||||
## Resolving a tribute demand
|
||||
|
||||
A tribute demand pauses that attacking faction during Defense Decision. The defender resolves each demand by either paying it in full or refusing it; there is no partial-payment or counteroffer option.
|
||||
|
||||
If the defender pays, the full requested gold and food are deducted from the province. The two factions enter a 12-month truce, and the demanding faction's hostile armies against any province ruled by the payer turn back. The returning armies arrive at their origin provinces in the next round.
|
||||
|
||||
If the defender refuses, no food or gold is transferred and the demanding army returns to **Attacking** status. The province remains in Defense Decision, so an eligible defender can then choose a defending army and fallback destination. Once the remaining attack and defense decisions are resolved, the ordinary battle-request rules apply: opposed armies proceed to battle, while an undefended attack may instead become an uncontested conquest.
|
||||
|
||||
Paying or refusing tribute has no direct prestige effect. If refusal leads to battle, the eventual battle result can change prestige normally.
|
||||
|
||||
## Food and gold
|
||||
|
||||
Carried supplies travel with the army. Moving armies consume their own food while incoming. If food is insufficient, their battalions can suffer starvation losses.
|
||||
|
||||
@@ -17,7 +17,7 @@ Movement cost depends on terrain and battalion type. Water, mountains, bridges,
|
||||
|
||||
Melee and ranged attacks require valid targets and battlefield geometry. Line of fire can be blocked. Damage and resistance depend on the units, heroes, damage profiles, and terrain involved.
|
||||
|
||||
Many major actions require a minimum AP and then consume all remaining AP. The command reference states the exact rule. Unless an entry says otherwise, a morale-using unit needs at least 10 morale, and a unit with a hero needs at least 10 hero vigor.
|
||||
Many major actions require a minimum AP and then consume all remaining AP. The command reference states the exact rule. Unless an entry says otherwise, a morale-using unit needs at least 10 morale, and a unit with a hero needs at least 10 [hero vigor](/heroes/#vigor). Current vigor also contributes to maximum AP. Reaching zero vigor resolves the entire hero-led unit as captured, even if troops remain.
|
||||
|
||||
## Heroes and battalions
|
||||
|
||||
|
||||
@@ -60,6 +60,22 @@ The common states are:
|
||||
|
||||
Some free heroes are effectively unavailable until circumstances change. For example, a province without one of your faction leaders cannot recruit from the Free Hero panel, and a hero may refuse forever if they have a permanent reason not to join your faction.
|
||||
|
||||
### What `WOULD_JOIN` Means
|
||||
|
||||
`WOULD_JOIN` means the hero meets the normal recruitment threshold for the faction that rules their current province. It is faction-specific: another faction may not qualify to recruit the same hero. The calculation uses the ruling faction's prestige, its faction head's charisma, the free hero's power and ambition, the hero's current state, and any personal bias toward or against that faction. A hero who is permanently unwilling to join that faction does not qualify.
|
||||
|
||||
The status means **willing, not recruited**. To recruit the hero with [**Recruit Heroes**](/commands/#recruit-heroes):
|
||||
|
||||
1. The province must be ruled by one of your faction leaders.
|
||||
2. That ruling faction leader must personally use [**Visit Town**](/commands/#visit-town).
|
||||
3. Use **Recruit Heroes** while the leader is visiting town and select the hero.
|
||||
|
||||
The faction head does not have to visit personally. Any of your faction leaders can recruit in a province they rule, although the willingness calculation still uses the faction head's charisma.
|
||||
|
||||
Vassals may visit town for their own purposes, but they cannot use **Recruit Heroes**, so they do not automatically recruit a hero marked `WOULD_JOIN`. However, an eligible free hero in a vassal-ruled province with enough support to provide taxes may independently ask to join during the **Please Recruit Me** phase. That is a separate faction-level decision: a human player accepts or declines the request rather than the vassal deciding. AI-controlled factions accept such requests automatically.
|
||||
|
||||
`WOULD_JOIN` is not the only status that can lead to recruitment. Travelers continue to display `TRAVELER` even when they would accept, because their willingness remains unknown until a faction leader visits town. Once the leader visits, the authoritative **Recruit Heroes** list shows which free heroes that faction can recruit under the current circumstances.
|
||||
|
||||
## Relationships
|
||||
|
||||
Faction relationships have a visible diplomatic state and an internal trust value.
|
||||
@@ -82,6 +98,20 @@ Trust is a hidden number used by AI diplomacy. Higher trust makes a faction more
|
||||
|
||||
Incoming offers are unresolved diplomatic proposals from other factions. They become [special and conditional commands](/commands/#special-and-conditional-commands) such as resolving truce offers, alliance offers, invitations, ransom offers, or alliance breaks.
|
||||
|
||||
## Diplomacy Outcomes
|
||||
|
||||
An envoy sent with a truce, invitation, alliance, or break-alliance proposal leaves the acting province during **Player Commands**. The hero is unavailable for the rest of that round, including any battles, and normally returns during **Diplomacy Resolution** after the battles are complete. If the origin province is still controlled, the envoy returns there; otherwise the game tries another faction province and then a moving faction army. If none exists, the envoy becomes an outlaw in the origin province.
|
||||
|
||||
Accepting another faction's alliance break replaces the alliance with a truce. Its reset date is 12 months after acceptance. The truce remains in force through that reset month, then becomes **Hostile** when **New Round** advances the date beyond it. Accepting a break therefore provides a protected transition instead of permitting immediate war.
|
||||
|
||||
Imprisoning a messenger has concrete consequences:
|
||||
|
||||
- The messenger does not return. They leave their original faction and become a prisoner held in the imprisoning faction's senior faction leader province.
|
||||
- The messenger receives a -100 faction bias toward the imprisoning faction, making later recruitment much harder.
|
||||
- The messenger's original faction loses 150 trust toward the imprisoning faction.
|
||||
- Every third-party faction loses 40 trust toward the imprisoning faction.
|
||||
- The incoming offer is removed. If the message was an alliance break, the alliance ends immediately, both sides become **Hostile**, and no transition truce is created.
|
||||
|
||||
## Focus Province
|
||||
|
||||
A faction can have a focus province. The focus province helps direct [province orders](/provinces/#province-orders) and AI attention toward a particular province.
|
||||
|
||||
@@ -28,7 +28,37 @@ These values help drive hero flavor, AI decisions, and story generation.
|
||||
|
||||
## Vigor
|
||||
|
||||
Vigor is a hero's current energy. Many commands require an acting hero and spend vigor. Resting restores vigor up to the hero's constitution.
|
||||
Vigor is a hero's current energy reserve. **Constitution is the hero's normal strategic vigor cap**; vigor and Constitution are related, but they are not the same stat.
|
||||
|
||||
### Strategic vigor
|
||||
|
||||
Many Eagle commands require an acting hero and spend vigor. The command reference lists each cost. A hero with too little vigor is not offered as an actor for that command.
|
||||
|
||||
Vigor recovers over time and through deliberate recovery:
|
||||
|
||||
- Each new month restores 5 vigor.
|
||||
- [Rest](/commands/#rest) restores up to 15 vigor to each faction hero in the province.
|
||||
- [Feast](/commands/#feast) restores up to 10 vigor to each faction hero in the province.
|
||||
- Recovery cannot raise strategic vigor above Constitution.
|
||||
|
||||
Strategic actions that spend vigor also grant Constitution XP. Constitution can rise through normal stat advancement, increasing the hero's future strategic vigor cap.
|
||||
|
||||
### Vigor in a Shardok battle
|
||||
|
||||
A hero enters battle with the vigor they currently have after strategic costs such as marching. That value becomes the hero's **battle-start vigor** and the recovery ceiling for that battle. A tired hero can therefore enter with a battle recovery ceiling below their Constitution. The ceiling does not fall as the battle proceeds; current vigor falls and rises beneath it.
|
||||
|
||||
Tactical vigor affects what a hero-led unit can do:
|
||||
|
||||
- Higher current vigor contributes to the unit's maximum Action Points. A normal unit's maximum is 5 plus rounded `vigor / 25` plus rounded `morale / 25`.
|
||||
- Spending Action Points normally spends 0.5 vigor per AP, and some profession abilities have additional vigor costs.
|
||||
- Combat damage and battlefield effects can reduce vigor.
|
||||
- Ending an activation with unused AP, [Rest](/shardok-commands/#stop-or-rest), and effects such as [Holy Wave](/shardok-commands/#holy-wave) can restore vigor, but not above battle-start vigor.
|
||||
- Below 10 vigor, most ordinary tactical commands are unavailable. A few cleanup or required commands are exceptions.
|
||||
- At zero vigor, the hero-led unit is resolved as captured. Its surviving battalion does not remain on the map as an independently acting unit.
|
||||
|
||||
While the hero remains active, their stats affect the battalion's combat swing, they add personal damage, and their profession can unlock special commands. Low vigor does not directly scale those contributions down point by point; it reduces AP and command access, with the decisive removal occurring at zero.
|
||||
|
||||
When a tactical display shows `current / battle start`, the first number is current vigor and the second is the battle-start recovery ceiling—not Constitution. Enemy details may be hidden by battlefield knowledge. An absent or hidden enemy value must not be interpreted as an actual zero.
|
||||
|
||||
## Loyalty
|
||||
|
||||
@@ -36,12 +66,48 @@ Loyalty measures a hero's attachment to their faction. Feasts, events, captures,
|
||||
|
||||
Loyalty is not faction bias. Loyalty describes an affiliated hero's current attachment; bias helps determine whether an unaffiliated or imprisoned hero will join a particular faction.
|
||||
|
||||
### Loyalty Departure
|
||||
|
||||
Any hero with low loyalty (below 50) may leave at any time. The game checks for departures automatically during the **Hero Departures** phase of each strategic round, before players and vassals issue commands. Lower loyalty means a greater chance of departure.
|
||||
|
||||
When a hero leaves, they stop belonging to the faction and become an unaffiliated traveler in the same province. They do not take gold or food with them. If other faction heroes remain, the province and its supplies remain with the faction. If every affiliated hero leaves, the province becomes unowned and its stored gold and food are lost.
|
||||
|
||||
## Professions
|
||||
|
||||
Professions give heroes special command access, bonuses, and tactical identity. Some effects are Eagle strategic effects, while others apply only when the hero is attached to a Shardok battle unit.
|
||||
|
||||
Several Shardok profession commands also depend on the attached battalion type. For example, many magic commands require a battalion type that allows casting, and stealth commands require a battalion type that allows stealth.
|
||||
|
||||
### Profession Effects At A Glance
|
||||
|
||||
**Active** means a command only that profession can use. **Semi-active** means a normal command or phase action is available without the profession, but the profession improves or changes it. **Passive** means the effect happens automatically, without the hero selecting a profession-specific command. A dash means there is no profession-specific effect in that category.
|
||||
|
||||
#### Eagle Effects
|
||||
|
||||
| Profession | Eagle Active | Eagle Semi-Active | Eagle Passive |
|
||||
| --- | --- | --- | --- |
|
||||
| [Mage](#mage) | [Control Weather](/commands/#control-weather): start or end a blizzard or drought in the acting province or a neighboring province. | — | — |
|
||||
| [Necromancer](#necromancer) | [Start Epidemic](/commands/#start-epidemic): start an epidemic in the acting province or an eligible neighboring province. | — | — |
|
||||
| [Engineer](#engineer) | — | [Improve](/commands/#improve): add 2 to the improvement or devastation repair produced by the command. | — |
|
||||
| [Paladin](#paladin) | — | [Alms](/commands/#alms): multiply the support gained from donated food by 4. | — |
|
||||
| [Ranger](#ranger) | [Recon](/commands/#recon): gather intelligence about an eligible province. | — | — |
|
||||
| [Champion](#champion) | — | [Train](/commands/#train): add 4 training to every trainable battalion affected by the command. | — |
|
||||
| [Warden](#warden) | — | — | Each new round, add 10 faction bias toward the ruling faction for prisoners held in the Warden's province, improving their future recruitment odds. |
|
||||
|
||||
Every profession other than **NoProfession** also adds 200 to Eagle's generic hero-power calculation and 50 to the hero-power calculation used when cracking down on a riot. Because those shared passive effects do not differ by profession, they are not repeated in every row.
|
||||
|
||||
#### Shardok Effects
|
||||
|
||||
| Profession | Shardok Active | Shardok Semi-Active | Shardok Passive |
|
||||
| --- | --- | --- | --- |
|
||||
| [Mage](#mage) | [Lightning Bolt](/shardok-commands/#lightning-bolt): cast a ranged lightning attack.<br />[Meteor](/shardok-commands/#meteor): cast a delayed area attack that also damages structures and terrain modifiers.<br />[Freeze Water](/shardok-commands/#freeze-water): turn valid water into traversable frozen terrain. | [Start Fire](/shardok-commands/#start-fire): always qualifies as capable and receives a +20 odds bonus.<br />[Melee](/shardok-commands/#melee) and [Charge](/shardok-commands/#charge): the Mage's personal damage uses Wisdom and magical damage types instead of Strength and physical damage types. | — |
|
||||
| [Necromancer](#necromancer) | [Raise Dead](/shardok-commands/#raise-dead): summon a controlled Undead battalion, enabling the linked Control, Dismiss, and Release commands.<br />[Fear](/shardok-commands/#fear): try to reduce an enemy's morale and stun it. | — | — |
|
||||
| [Engineer](#engineer) | [Build Bridge](/shardok-commands/#build-bridge) and [Blow Bridge](/shardok-commands/#blow-bridge): create or destroy crossings.<br />[Repair](/shardok-commands/#repair): restore bridge or castle integrity.<br />[Reduce](/shardok-commands/#reduce): bombard units or structures from a castle or fortified position.<br />[Fortify](/shardok-commands/#fortify): become fortified and take 10% less damage. | — | — |
|
||||
| [Paladin](#paladin) | [Holy Wave](/shardok-commands/#holy-wave): improve nearby allies' morale and vigor, remove stun, and devastate adjacent Undead. | — | — |
|
||||
| [Ranger](#ranger) | [Scout](/shardok-commands/#scout): reveal battlefield information.<br />[Hide](/shardok-commands/#hide): enter a hidden position.<br />[Hidden setup placement](/shardok-commands/#placement): a defending Ranger on a stealth-capable unit can deploy hidden. | [Brave Water](/shardok-commands/#brave-water): receive a +20 odds bonus.<br />[Move](/shardok-commands/#move): reveal adjacent hidden enemies while moving. | An adjacent enemy Ranger prevents a unit from hiding or remaining hidden. |
|
||||
| [Champion](#champion) | [Challenge Duel](/shardok-commands/#challenge-duel): challenge an adjacent enemy hero to personal combat. | — | — |
|
||||
| [Warden](#warden) | [Evacuate Prisoners](/shardok-commands/#evacuate-prisoners): automatically evacuate the Warden and secure every hostile hero already captured in the battle. | — | — |
|
||||
|
||||
### Mage
|
||||
|
||||
Eagle effects:
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
# Gitea offsite backups
|
||||
|
||||
Eagle0's Gitea mirror keeps Git and Git LFS data on `zog` for fast local
|
||||
access. The `Gitea Offsite Backup` GitHub Actions workflow copies the
|
||||
recoverable state to an encrypted Restic repository in the private
|
||||
`eagle0-gitea-backups` DigitalOcean Space.
|
||||
|
||||
## Backup contents
|
||||
|
||||
Each snapshot contains:
|
||||
|
||||
- a transactionally consistent SQLite backup of the Gitea database;
|
||||
- Gitea configuration and persistent application data;
|
||||
- every locally stored Git LFS object;
|
||||
- a fresh mirror clone of `admin/eagle0`;
|
||||
- the Gitea version and captured `main` commit in `manifest.txt`.
|
||||
|
||||
Regenerable indexes, sessions, queues, repository archives, logs, and the
|
||||
OAuth2 JWT signing key are excluded. Gitea creates a new JWT signing key when
|
||||
none exists, which invalidates pre-restoration OAuth2 tokens. Container-managed
|
||||
SSH host keys are not readable by the backup account and must also be
|
||||
regenerated after a complete NAS loss.
|
||||
|
||||
## Schedule and retention
|
||||
|
||||
The backup runs daily at 10:23 UTC on a `halfdan` self-hosted runner. Each
|
||||
run validates the SQLite snapshot, runs `git fsck`, uploads an encrypted
|
||||
snapshot, and reads a rotating 5% sample of the Restic repository.
|
||||
|
||||
Weekly maintenance runs Sunday at 11:53 UTC. It retains 14 daily, 8 weekly,
|
||||
12 monthly, and 3 yearly snapshots, prunes unreferenced Restic packs, and
|
||||
reads a rotating 10% sample.
|
||||
|
||||
Use the workflow's `full_restore_verify` option after storage or credential
|
||||
changes. It downloads the latest snapshot, validates the restored SQLite
|
||||
database, and runs `git fsck` against the restored repository.
|
||||
|
||||
## Credentials
|
||||
|
||||
GitHub Actions stores:
|
||||
|
||||
- `GITEA_DO_SPACES_ACCESS_KEY`
|
||||
- `GITEA_DO_SPACES_SECRET_KEY`
|
||||
- `DO_SPACES_ENDPOINT`
|
||||
- `GITEA_BACKUP_SSH_KEY`
|
||||
- `GITEA_RESTIC_PASSWORD`
|
||||
|
||||
`GITEA_BACKUP_SSH_KEY` is a dedicated key accepted by the NAS account used for
|
||||
backup reads. It is not a copy of a developer's personal SSH key.
|
||||
|
||||
The Restic recovery password also has an offline recovery copy in the
|
||||
workstation's macOS Keychain under service `eagle0-gitea-restic`. Never put
|
||||
that password in the repository, workflow logs, or a support ticket.
|
||||
|
||||
## Restore
|
||||
|
||||
Install `restic`, then set the DigitalOcean credentials, Restic repository,
|
||||
and Restic password in the shell performing the restore. Restore into an
|
||||
empty directory:
|
||||
|
||||
```bash
|
||||
restic snapshots --host zog --tag gitea
|
||||
restic restore latest --host zog --tag gitea --target /path/to/restore
|
||||
```
|
||||
|
||||
The snapshot preserves the absolute staging path below the restore target.
|
||||
Locate `manifest.txt`; its containing directory is the restored backup root.
|
||||
|
||||
Before changing Gitea:
|
||||
|
||||
```bash
|
||||
sqlite3 /path/to/restored/root/gitea/gitea.db "PRAGMA integrity_check;"
|
||||
git --git-dir=/path/to/restored/root/repositories/admin/eagle0.git fsck --full
|
||||
```
|
||||
|
||||
To recover a complete installation:
|
||||
|
||||
1. Recreate the same Gitea version shown in `manifest.txt`.
|
||||
2. Stop Gitea.
|
||||
3. Restore `config`, `gitea`, `lfs`, and the bare repository into the
|
||||
matching paths under `/volume2/docker/gitea`.
|
||||
4. Preserve the restored file ownership expected by the container.
|
||||
5. Allow Gitea to regenerate indexes, sessions, queues, archives, and SSH
|
||||
host keys.
|
||||
6. Start Gitea and verify a historical LFS checkout before enabling writes.
|
||||
|
||||
Do not overwrite a running installation during a restore.
|
||||
@@ -5,15 +5,15 @@ go 1.25.0
|
||||
toolchain go1.26.5
|
||||
|
||||
require (
|
||||
github.com/aws/aws-sdk-go-v2 v1.42.1
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.30
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.29
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.105.1
|
||||
github.com/aws/aws-sdk-go-v2 v1.43.0
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.31
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.30
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.106.0
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6
|
||||
golang.org/x/sys v0.47.0
|
||||
google.golang.org/grpc v1.82.0
|
||||
google.golang.org/grpc v1.82.1
|
||||
google.golang.org/protobuf v1.36.11
|
||||
)
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ github.com/aws/aws-sdk-go-v2 v1.42.0 h1:XvXMJTkFQtpBKIWZnmr9ZEOc2InWM2yldjXEJ/by
|
||||
github.com/aws/aws-sdk-go-v2 v1.42.0/go.mod h1:27+ACypSLljLAEKsCYOmrjKh83vuTRkuAe9Uv/3A4bg=
|
||||
github.com/aws/aws-sdk-go-v2 v1.42.1 h1:9eOTgu1z/dVtYpNZ3/8/XbbaX0x/BqE3HUzAzs6K0ek=
|
||||
github.com/aws/aws-sdk-go-v2 v1.42.1/go.mod h1:5pKeft2eJj+gElQ38Jqg4ibCqh+/AK33/0X3hip7IjM=
|
||||
github.com/aws/aws-sdk-go-v2 v1.43.0 h1:fharf/WhbRAVZ1du0QL7roNFxZ6T/sWr+4Ni617bwSI=
|
||||
github.com/aws/aws-sdk-go-v2 v1.43.0/go.mod h1:5pKeft2eJj+gElQ38Jqg4ibCqh+/AK33/0X3hip7IjM=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 h1:lL7IfaFzngfx0ZwUGOZdsFFnQ5uLvR0hWqqhyE7Q9M8=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7/go.mod h1:QraP0UcVlQJsmHfioCrveWOC1nbiWUl3ej08h4mXWoc=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13 h1:p1BBrg/Hhp6uK7zpejeI8QFXHJeC/mynzi04Sl03k9g=
|
||||
@@ -26,6 +28,8 @@ github.com/aws/aws-sdk-go-v2/config v1.32.29 h1:BcMHHnpiWKogf+gGfpj3K1w+Sktz29XD
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.29/go.mod h1:+Kbhn8Es4kPUph3F/0W7avykytc+Jh2Ld9/msv9ljV4=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.30 h1:XwsEzpTJfQYJbFicz/QMLwAZdyeNVVoOEkbF7R3gPJk=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.30/go.mod h1:Ud32SuMc+/9BGxfpSVld7HrE2o05JwKmXY4M3jOQNZU=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.31 h1:n4nY9O3QKoHIkL85EX+V8RcMFtOhlpTFhGArg915PXk=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.31/go.mod h1:PN0NYDCCoOpGGsZ2+elDUidmHfQBPyYzN2GCgl8HEBs=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.51 h1:F/9Sm6Y6k4LqDesZDPJCLxQGXNNHd/ZtJiWd0lCZKRk=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.51/go.mod h1:TKbzCHm43AoPyA+iLGGcruXd4AFhF8tOmLex2R9jWNQ=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.24 h1:2hQqYCV9yqyePQ9o6dCrZc/zO8U3TwPr9mIKlZnPu/I=
|
||||
@@ -34,24 +38,32 @@ github.com/aws/aws-sdk-go-v2/credentials v1.19.28 h1:zTXJSsNcoO91/mTXsZoYf0AK8dv
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.28/go.mod h1:Kd9E0JzDBW/q1xbsHFrev/GnbAf5J0Ng8xoyc7HZ91Q=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.29 h1:WHZGssHH887cO0ox07SIQZsFx3MKD4ps6w0xUEmnKYQ=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.29/go.mod h1:Mhl0xR6zjguiuj00XRx2wMx22sAltk7oya39sT7fdg8=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.30 h1:TTCvvzFU6gXa4iJecNG/0F/B0oYTiazoRECr2XyLHrY=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.30/go.mod h1:jKxAp2AEncnliinzpgOSZDFv6+VjvWhjw/AtbfsWT9U=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.23 h1:IBAoD/1d8A8/1aA8g4MBVtTRHhXRiNAgwdbo/xRM2DI=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.23/go.mod h1:vfENuCM7dofkgKpYzuzf1VT1UKkA/YL3qanfBn7HCaA=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29 h1:r6qZHbT+wxgWO/e9vYNUEtg7lv5+UN3pRqKhLXvnArg=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29/go.mod h1:QRnaRcTVGKPGRy8w78HMQtKUGRYcnMZAANATkeVA6Mo=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.30 h1:/hi1JADLEW9YYryEz1w4GQu0EtP23pP553Cf9KgsDV4=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.30/go.mod h1:/3AOgy4K17Dm4ucMZVC/MJkzy5kmfKUcINRHZyo0koQ=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.31 h1:kfVL5wAunCJycL6MOQ6aNh6PlAYEymflcjuKmrWUA0o=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.31/go.mod h1:nWfRNDAppujCQgOUd43lKT4yeLv9z3nJ3bw1G3BgQKo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.27 h1:jSJjSBzw8VDIbWv+mmvBSP8ezsztMYJGH+eKqi9AmNs=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.27/go.mod h1:/DAhLbFRgwhmvJdOfSm+WwikZrCuUJiA4WgJG0fTNSw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 h1:f3vKqSo13fhTYb+JEcXwXefZQE26I1FB5eTSniU67ko=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29/go.mod h1:MzoLFUArKGpGD+ukmPiTPG1X5x4o6M2kq4v2dr1FiEc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.30 h1:xM/Is9cKMHa8Jj8zkvWhvrFkZsXJV9E+BB4g0HW0duQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.30/go.mod h1:WueJeNDZvK1fMYEWJIkcivBfEzUkTpBhzlrUKKY8EuA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.31 h1:Z8F3hfCY33IGpJjFAnv0wvtv1FIKj1GHmRDEYqy64tw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.31/go.mod h1:aVyUoytEyOViR6jhq6jula0xkc5NfBE2hgeF6BvOrao=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.27 h1:l+X4K77Dui85pIj5foXDhPlnqcNRG2QUyvca300lXh8=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.27/go.mod h1:KvZXSFEXm6x84yE8qffKvT3x8J5clWnVFXphpohhzJ8=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 h1:RdwIf/CuUsvJX3RgJagbOyotl/cxoLY4xviKuE7p2GY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29/go.mod h1:71wt8W2EgswdZy9Mf9KNnzxZ3TiZlv4caKghPktDOkA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.30 h1:jn46zC9LdsVR/ZpMIJqMqb8hHv31BlLx3ulVqNspUOk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.30/go.mod h1:1hTMsAgbdS/AtUi4bw8+gUuh1pceo+eXRLfpSuSQj3M=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.31 h1:hyOxUyXdh3AyjE93gBgsfziJag9ACwcs+ZpDBLzi8mw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.31/go.mod h1:OERqI9k0draSLB8O8woxY3q25ZWTELRK4RRoLMuMZFo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw=
|
||||
@@ -62,6 +74,8 @@ github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30 h1:VTGy885W5DKBxWRUJbym9hytNaY
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30/go.mod h1:AS0HycUvJRFvTt613AYDOgO2jzw+00cVSMny8XB3yMY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.31 h1:3GUprIsfmGcC5SACIyB0e7E0BM1O1b3Erl5CePYIAeQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.31/go.mod h1:7PuV1yl5e2xnUbm+RqvVg5i2iBM8EyijZNoI9wsOoOc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.32 h1:0MrUL35H/Y4kdFfItoR5jCgtDQ4Z/8LudAoIHRfA4hE=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.32/go.mod h1:2tNZkuWz54arj8mHVf+8Y7cKkcD8Wr/fBpENgEXpjLc=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 h1:iXtILhvDxB6kPvEXgsDhGaZCSC6LQET5ZHSdJozeI0Y=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12 h1:ZD2+BSw9vFsNlKYIasSNt3uDbjqqXIBcM13UJv/Lx2k=
|
||||
@@ -74,18 +88,24 @@ github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.22 h1:V51LGlOq/1VsDs
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.22/go.mod h1:4Pzhyz8hJOm2bepgl+NjvRx8vlUFAIIvJnZ/MkcNPpU=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.23 h1:9Fjh6fi/U5JEStVZijmaMpUwE/gvBJj7x2B/PjbO9To=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.23/go.mod h1:iMoT2f1tClxrWAAnKCXjZQ6LOmfLrMG14wmnWpM+F14=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.24 h1:mdPwDQPqxlw9Sc62Nt15yjEcARaDbPXkjRYtXsUripo=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.24/go.mod h1:ls5ytnwLTcQaUu32fMYXFI3MjpKuTwL840PAm9iqyEg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.8 h1:cWno7lefSH6Pp+mSznagKCgfDGeZRin66UvYUqAkyeA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.8/go.mod h1:tPD+VjU3ABTBoEJ3nctu5Nyg4P4yjqSH5bJGGkY4+XE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29 h1:DRebniUGZ2MqiiIVmQJ04vIXr918hubdHMnarSLEWyU=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29/go.mod h1:LfRkPCD8YHDM2E5eTkos2UpwYeZnBcVarTa8L59bJHA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.30 h1:/Z5jmNrKsSD7EmDjzAPsm/3L9IuOkzaynklJZ1qX7S4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.30/go.mod h1:lEzEZnOosE7zi8Z6royW1cFJTD9fpab4Ul1SBrllewk=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.31 h1:w2SIhW92DZPFrSL4ksVCr8IYff5OZwIcxg8+95tzvAI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.31/go.mod h1:wAhpCQbkov+IcvjozJbd2xRCoZybUEHNkcFunssNACg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.8 h1:/Mn7gTedG86nbpjT4QEKsN1D/fThiYe1qvq7WsBGNHg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.8/go.mod h1:Ae3va9LPmvjj231ukHB6UeT8nS7wTPfC3tMZSZMwNYg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.29 h1:hiME6pBzC7OTl9LMtlyTWBuEl1f4QBcUmFDKC7MLXtc=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.29/go.mod h1:G7RP+uhagpKtKhd1BM9N6JQqjCcGEU47K5lBVZQyRQw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.31 h1:uao4A3QZ5UmB326V6KF+qRpv9Tjz7IlnlnTbbANntlU=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.31/go.mod h1:I/1+z0VwL1GhQyLgkoHDlygpUZ+iTAwOQ/NsftiUL2I=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.32 h1:jWXtZdCnhXa9sGFixRaU2AxT4DIVse9HS4E2f+/KwV0=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.32/go.mod h1:9JS1UpfVvyD/ZPX8GsKb/Pq8scEM+7GP5fqh9SwH7po=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.72.2 h1:a7aQ3RW+ug4IbhoQp29NZdc7vqrzKZZfWZSaQAXOZvQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.72.2/go.mod h1:xMekrnhmJ5aqmyxtmALs7mlvXw5xRh+eYjOjvrIIFJ4=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.103.3 h1:JRseEu/vIDMaWis4bSw0QbXL+cvIGc1XnX076H5ZXLE=
|
||||
@@ -94,12 +114,18 @@ github.com/aws/aws-sdk-go-v2/service/s3 v1.105.0 h1:XptwLL+UHXgafYMIHTy59IRovLbh
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.105.0/go.mod h1:zdmCoFO/dSI7GlrwsPqFJI+WlFnSU4Tc8TJnlXrM1Do=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.105.1 h1:LkBKxAOE5WXjlFuFZqPG1rREnl6I6QCMElcXFDEidos=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.105.1/go.mod h1:zdmCoFO/dSI7GlrwsPqFJI+WlFnSU4Tc8TJnlXrM1Do=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.105.2 h1:5C00eQYpTrgQXnp6V3P6P7zPElna3AXvlukbANE6nJI=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.105.2/go.mod h1:zdmCoFO/dSI7GlrwsPqFJI+WlFnSU4Tc8TJnlXrM1Do=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.106.0 h1:7QZWVJZWzHivHWIa+5TELLaBBkbuoj0GPwQtMlJ0sqk=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.106.0/go.mod h1:fcvq5L7dK+5cQFicEJwpI6e6Wn8NY2i6yT5wRLYVc7s=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.2.0 h1:3nXpRcFwRCW8n7HgO2QGy0Dc20eQNfBuUemGQhpF8m8=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.2.0/go.mod h1:LxYujSTLPRlp2vTtcUO/+1ilrew8ytt6SvQyOgejzFQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.4.0 h1:sLzmJGCMv+C8KqiJgEqDLB6vxaJGmobRh4rr//ZpA3w=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.4.0/go.mod h1:mxC0nT/C8wMMS97DemZPzvUZxvIt+2Iq+eS3JdFZGgg=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.4.1 h1:V7ZZ300WPXGjvkyore5DGe0ljVPOxCXie/thWdtSBXE=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.4.1/go.mod h1:mxC0nT/C8wMMS97DemZPzvUZxvIt+2Iq+eS3JdFZGgg=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.5.0 h1:OHH5iTQvVGmfHjX/5Q+vFuA/Rf2x6/95aJ/75QCQSm4=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.5.0/go.mod h1:mCF3AK9PpL49oOrhniUXWAfhVBVQ/XbytoE5eccZUIs=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.24.9 h1:YqtxripbjWb2QLyzRK9pByfEDvgg95gpC2AyDq4hFE8=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.24.9/go.mod h1:lV8iQpg6OLOfBnqbGMBKYjilBlf633qwHnBEiMSPoHY=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.31.3 h1:ey1XLTYXb9PcLt4535632o5kCGXNXEhNb620Dqwuylo=
|
||||
@@ -108,6 +134,8 @@ github.com/aws/aws-sdk-go-v2/service/sso v1.32.0 h1:qjMmry/cBDee1E/2gyvel0uRYCi3
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.32.0/go.mod h1:u8af9Nqkmqnr96f7v9nHqzZT9XBwbXEkTiqT4ROuJSE=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.32.1 h1:gYFYh4iLLcAOJRLNPY2aD2g9DIhKn4eof8UkIrr1rTk=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.32.1/go.mod h1:u8af9Nqkmqnr96f7v9nHqzZT9XBwbXEkTiqT4ROuJSE=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.33.0 h1:CaJyYhxBE0M/HJX/YvSaSmQlsI91VHB0lKU8LtLxL3A=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.33.0/go.mod h1:+e6BMRMPjBQoCw/WovYR9GLy2IU0z4Q77smOB1DraSg=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.8 h1:6dBT1Lz8fK11m22R+AqfRsFn8320K0T5DTGxxOQBSMw=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.8/go.mod h1:/kiBvRQXBc6xeJTYzhSdGvJ5vm1tjaDEjH+MSeRJnlY=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6 h1:yLr03zQE/5Eu5l3QU0Si+xMbLMbSDF2YXsigqXngs6g=
|
||||
@@ -116,6 +144,8 @@ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.37.0 h1:fpOlDPI55HdszaxapEGk6HsG
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.37.0/go.mod h1:DMPWJBjYs6+3+f/qhBFEFPPlQ6NlhWjai3dJNvipJ84=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.37.1 h1:arjT9Cm3/WYbGmD5TUZHk4UQn4Lle1fUNZs5FC6CtF0=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.37.1/go.mod h1:DMPWJBjYs6+3+f/qhBFEFPPlQ6NlhWjai3dJNvipJ84=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.0 h1:tC323YV77QdafeBr6LUhLDTsboyuyHLNRwAyCP44kGU=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.0/go.mod h1:SfLK1sgviHmbI+MozR9iDwDjL4cdCVZtahsjoR+z7wg=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.6 h1:VwhTrsTuVn52an4mXx29PqRzs2Dvu921NpGk7y43tAM=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.6/go.mod h1:+8h7PZb3yY5ftmVLD7ocEoE98hdc8PoKS0H3wfx1dlc=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.43.3 h1:VrIhKRCSK1umelSgB9RghvA9RTUYeQffyAS5ApXehNI=
|
||||
@@ -124,6 +154,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.44.0 h1:bLZ0PolJ8J+HkJHztcXORUpHXBye
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.44.0/go.mod h1:9gdl4RrflIdpDb2TlXshWgR1F9TeCkvqDx77Vpr4Z/Q=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.44.1 h1:RvfHDg+xvAeZ+5741vUEjpOVtYSIm93W2zhx10Xtydw=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.44.1/go.mod h1:9gdl4RrflIdpDb2TlXshWgR1F9TeCkvqDx77Vpr4Z/Q=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.45.0 h1:Pd6PNlp4t8PTXxqzstICl52Wsy78vpjFZ7PRUj44mJc=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.45.0/go.mod h1:rmQ0TnHzuLPmabgjPcsywhsSOmaBDgzR4zvDxSPsGdg=
|
||||
github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro=
|
||||
github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
|
||||
github.com/aws/smithy-go v1.27.1 h1:4T340VFndXtADGF52gYa1POyL7s9E4Z1OeZ1hCscIw8=
|
||||
@@ -232,6 +264,8 @@ google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
|
||||
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
|
||||
google.golang.org/grpc v1.82.0 h1:vguDnZUPjE26w09A63VoxZPnvPjB5Riyc0mkXPFmAIU=
|
||||
google.golang.org/grpc v1.82.0/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
|
||||
google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE=
|
||||
google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
|
||||
google.golang.org/protobuf v1.26.0-rc.1 h1:7QnIQpGRHE5RnLKnESfDoxm2dTapTZua5a0kS0A+VXQ=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
|
||||
|
||||
+66
-79
@@ -47,7 +47,7 @@
|
||||
"io.opencensus:opencensus-api": 2042364617,
|
||||
"io.opencensus:opencensus-contrib-grpc-metrics": 317094357,
|
||||
"io.perfmark:perfmark-api": 1019157794,
|
||||
"io.sentry:sentry": -511720268,
|
||||
"io.sentry:sentry": -699383028,
|
||||
"javax.xml.bind:jaxb-api": -1735500680,
|
||||
"junit:junit": -744267592,
|
||||
"org.checkerframework:checker-qual": -819031361,
|
||||
@@ -59,11 +59,11 @@
|
||||
"org.postgresql:postgresql": -1283293677,
|
||||
"org.reactivestreams:reactive-streams": 509083534,
|
||||
"org.scala-lang:scala3-staging_3": -360398695,
|
||||
"org.scalameta:scalafmt-cli_2.13": 923201552,
|
||||
"org.scalameta:scalafmt-cli_2.13": 1603259922,
|
||||
"org.scalamock:scalamock_3": -1047317697,
|
||||
"org.slf4j:slf4j-api": -2005163028,
|
||||
"org.slf4j:slf4j-simple": 1999565066,
|
||||
"org.xerial:sqlite-jdbc": 1165654128,
|
||||
"org.xerial:sqlite-jdbc": -641800335,
|
||||
"repositories": 514854214,
|
||||
"software.amazon.awssdk:aws-core": 578934204,
|
||||
"software.amazon.awssdk:http-client-spi": 1408046502,
|
||||
@@ -165,7 +165,7 @@
|
||||
"io.opencensus:opencensus-contrib-grpc-metrics": -1797408624,
|
||||
"io.opencensus:opencensus-contrib-http-util": -541858674,
|
||||
"io.perfmark:perfmark-api": -752483303,
|
||||
"io.sentry:sentry": -1910119764,
|
||||
"io.sentry:sentry": 1646897141,
|
||||
"javax.activation:javax.activation-api": -217704356,
|
||||
"javax.annotation:javax.annotation-api": 193132517,
|
||||
"javax.inject:javax.inject": -1284481446,
|
||||
@@ -216,27 +216,27 @@
|
||||
"org.scala-lang:scala-reflect": 2030778053,
|
||||
"org.scala-lang:scala3-library_3": -1714704820,
|
||||
"org.scala-lang:scala3-staging_3": 1767551804,
|
||||
"org.scalameta:common2_2.13": 1775009973,
|
||||
"org.scalameta:common_2.13": -1607493593,
|
||||
"org.scalameta:io_2.13": 1529358668,
|
||||
"org.scalameta:mdoc-parser_2.13": 1998129208,
|
||||
"org.scalameta:metaconfig-core_2.13": 1560822216,
|
||||
"org.scalameta:metaconfig-pprint_2.13": -13676725,
|
||||
"org.scalameta:metaconfig-typesafe-config_2.13": 389481839,
|
||||
"org.scalameta:common2_2.13": 1796949606,
|
||||
"org.scalameta:common_2.13": 1892326143,
|
||||
"org.scalameta:io_2.13": -1056988288,
|
||||
"org.scalameta:mdoc-parser_2.13": -377476829,
|
||||
"org.scalameta:metaconfig-core_2.13": -557456764,
|
||||
"org.scalameta:metaconfig-pprint_2.13": 268613438,
|
||||
"org.scalameta:metaconfig-typesafe-config_2.13": 618711541,
|
||||
"org.scalameta:munit-diff_2.13": 667211464,
|
||||
"org.scalameta:parsers_2.13": -732548017,
|
||||
"org.scalameta:scalafmt-cli_2.13": -872730967,
|
||||
"org.scalameta:scalafmt-config_2.13": -1220516276,
|
||||
"org.scalameta:scalafmt-core_2.13": -518465330,
|
||||
"org.scalameta:scalafmt-dynamic-core_2.13": 1088145997,
|
||||
"org.scalameta:scalafmt-dynamic_2.13": -193332653,
|
||||
"org.scalameta:scalafmt-interfaces": 2029532209,
|
||||
"org.scalameta:scalafmt-macros_2.13": 1019057318,
|
||||
"org.scalameta:scalafmt-sysops_2.13": -919350628,
|
||||
"org.scalameta:scalameta_2.13": 494629884,
|
||||
"org.scalameta:parsers_2.13": 970620234,
|
||||
"org.scalameta:scalafmt-cli_2.13": 1904050715,
|
||||
"org.scalameta:scalafmt-config_2.13": 1077807261,
|
||||
"org.scalameta:scalafmt-core_2.13": -542782848,
|
||||
"org.scalameta:scalafmt-dynamic-core_2.13": -565068427,
|
||||
"org.scalameta:scalafmt-dynamic_2.13": -133643165,
|
||||
"org.scalameta:scalafmt-interfaces": -1699622215,
|
||||
"org.scalameta:scalafmt-macros_2.13": -1322813645,
|
||||
"org.scalameta:scalafmt-sysops_2.13": -1994112303,
|
||||
"org.scalameta:scalameta_2.13": 625605450,
|
||||
"org.scalameta:svm-subs": -181782616,
|
||||
"org.scalameta:trees2_2.13": 1085082114,
|
||||
"org.scalameta:trees_2.13": -1981634150,
|
||||
"org.scalameta:trees2_2.13": 1553624450,
|
||||
"org.scalameta:trees_2.13": 1539326839,
|
||||
"org.scalamock:scalamock_3": 23905729,
|
||||
"org.slf4j:slf4j-api": -1249720338,
|
||||
"org.slf4j:slf4j-simple": -1378006603,
|
||||
@@ -244,7 +244,7 @@
|
||||
"org.typelevel:paiges-core_2.13": 1005692255,
|
||||
"org.virtuslab.scala-cli:config_2.13": -113493958,
|
||||
"org.virtuslab.scala-cli:specification-level_2.13": -176416420,
|
||||
"org.xerial:sqlite-jdbc": 532995836,
|
||||
"org.xerial:sqlite-jdbc": -2058325677,
|
||||
"software.amazon.awssdk:annotations": 998747622,
|
||||
"software.amazon.awssdk:apache5-client": -1074889369,
|
||||
"software.amazon.awssdk:arns": -343721679,
|
||||
@@ -809,9 +809,9 @@
|
||||
},
|
||||
"io.sentry:sentry": {
|
||||
"shasums": {
|
||||
"jar": "a5477fab0018a68c335565c034302b12cff3b0cb56e4a0ffa5f8f47840746d83"
|
||||
"jar": "0bac4f4330e94ca19eb91640618d22a89b47ce45ffb79aa3d55e41f9b4f33517"
|
||||
},
|
||||
"version": "8.48.0"
|
||||
"version": "8.50.1"
|
||||
},
|
||||
"javax.activation:javax.activation-api": {
|
||||
"shasums": {
|
||||
@@ -1115,45 +1115,45 @@
|
||||
},
|
||||
"org.scalameta:common2_2.13": {
|
||||
"shasums": {
|
||||
"jar": "dc17dfd2a4923f49c85bd69be85f820ba5d57f50a4425b27a8635b592a0c1471"
|
||||
"jar": "d16050f3557e472ac3489501edd4d941d975bc0a6f72a54363b6598e29a972d0"
|
||||
},
|
||||
"version": "4.17.2"
|
||||
"version": "4.17.3"
|
||||
},
|
||||
"org.scalameta:common_2.13": {
|
||||
"shasums": {
|
||||
"jar": "70fd266cd4e6ab3909d329ef051264865fbadeeb86b598672a65439e51cdadd6"
|
||||
"jar": "e15f1eca7e1d855daf0fd62874b944272b84d2bbee96f3adfaef0cdd34ec91e4"
|
||||
},
|
||||
"version": "4.17.2"
|
||||
"version": "4.17.3"
|
||||
},
|
||||
"org.scalameta:io_2.13": {
|
||||
"shasums": {
|
||||
"jar": "5e6edc6b5aa96a595382ffb9e4cd55a6cb302064d388db7fa2168334fd1cb90e"
|
||||
"jar": "c9aa7493cf90a1acafcc96dad84670409aeb3b5e280a38ce40332eb6a21cf49f"
|
||||
},
|
||||
"version": "4.17.2"
|
||||
"version": "4.17.3"
|
||||
},
|
||||
"org.scalameta:mdoc-parser_2.13": {
|
||||
"shasums": {
|
||||
"jar": "37442a255f0e6a31c9786d7d8af4ef1e29ded76c4f44af29895b4b36d904a068"
|
||||
"jar": "4ce63efb015b6ea92bd0bcf107c8153b3e2857495821a9a59dc41424b4015831"
|
||||
},
|
||||
"version": "2.9.0"
|
||||
"version": "2.9.1"
|
||||
},
|
||||
"org.scalameta:metaconfig-core_2.13": {
|
||||
"shasums": {
|
||||
"jar": "f8ed537b95758409a0dd0a9533fdd6da703914f5901f42d6f96ba07c77f58aaf"
|
||||
"jar": "9493d98e6a9b8b43bdbd7a00e06fa1dcf295180e0bf0d94d705f18801c75ec80"
|
||||
},
|
||||
"version": "0.18.6"
|
||||
"version": "0.18.7"
|
||||
},
|
||||
"org.scalameta:metaconfig-pprint_2.13": {
|
||||
"shasums": {
|
||||
"jar": "cbe1531b67e0c080294e3eaeea35952524c91af0c100f517ba7004607f44be46"
|
||||
"jar": "9f727dfdf0883c4066509e82f9ab58142ce0fb25371599d4a1b15e35d52467af"
|
||||
},
|
||||
"version": "0.18.6"
|
||||
"version": "0.18.7"
|
||||
},
|
||||
"org.scalameta:metaconfig-typesafe-config_2.13": {
|
||||
"shasums": {
|
||||
"jar": "1ddbdfcc9f425ca385fa0e0079b8f6fc12571b2e03c9a3ae2bc0c832ca288bca"
|
||||
"jar": "85085a7a082eead66bf0dccd08d402e5a4bad1f3fc73673284fac8e8f9e6c4ec"
|
||||
},
|
||||
"version": "0.18.6"
|
||||
"version": "0.18.7"
|
||||
},
|
||||
"org.scalameta:munit-diff_2.13": {
|
||||
"shasums": {
|
||||
@@ -1163,63 +1163,63 @@
|
||||
},
|
||||
"org.scalameta:parsers_2.13": {
|
||||
"shasums": {
|
||||
"jar": "c789f3cb81b93ddc8d46cf8b36dfd40715d52fe3c1ef2cb4f14bd4b1fc8543be"
|
||||
"jar": "21931f60d741a95f6fc364f5514672b587b24a2316df0a80fc67059a9989828d"
|
||||
},
|
||||
"version": "4.17.2"
|
||||
"version": "4.17.3"
|
||||
},
|
||||
"org.scalameta:scalafmt-cli_2.13": {
|
||||
"shasums": {
|
||||
"jar": "da5379c790d39a52e4c059eba04fdb0e97402d6eb9b5318b9370c7b2f9c08318"
|
||||
"jar": "86ab6706dcefa4e0efd8bd9fe6ab5d3223dfa3efcf2cd74cd0fddc2dc09864ee"
|
||||
},
|
||||
"version": "3.11.3"
|
||||
"version": "3.11.5"
|
||||
},
|
||||
"org.scalameta:scalafmt-config_2.13": {
|
||||
"shasums": {
|
||||
"jar": "4cb7fd5ac2768203e5ea09e7670d3a5012f5176a573ccf6a92114fcb79f55ab5"
|
||||
"jar": "0e000fe9c11c991f9a012331b829a7295b10995c1f5421701755e1cce6373149"
|
||||
},
|
||||
"version": "3.11.3"
|
||||
"version": "3.11.5"
|
||||
},
|
||||
"org.scalameta:scalafmt-core_2.13": {
|
||||
"shasums": {
|
||||
"jar": "dd176a03c2716223977e8c0cd29cef4b7b7dc0a4e3102a8a9bc70bae6e917751"
|
||||
"jar": "301fffd68a5723390d62ce62b075ed2e9c84003a0994634951638c4df6eb77f8"
|
||||
},
|
||||
"version": "3.11.3"
|
||||
"version": "3.11.5"
|
||||
},
|
||||
"org.scalameta:scalafmt-dynamic-core_2.13": {
|
||||
"shasums": {
|
||||
"jar": "3c38c49a717f1be67757d1192707ec38442fc84014408010bde746db94bca92a"
|
||||
"jar": "f55a6614c5815f4435451eaf24b0b0a771fc26a2b3603cc8f4428ccfc6ae96c0"
|
||||
},
|
||||
"version": "3.11.3"
|
||||
"version": "3.11.5"
|
||||
},
|
||||
"org.scalameta:scalafmt-dynamic_2.13": {
|
||||
"shasums": {
|
||||
"jar": "f78b5eb890132266ae0ab0ff05f57caaf07d53d206b6528e9a5a868d14917591"
|
||||
"jar": "fcaaf533c46fd0c297e33a4363587202e973d27f37dc37538e17195d964d0f3a"
|
||||
},
|
||||
"version": "3.11.3"
|
||||
"version": "3.11.5"
|
||||
},
|
||||
"org.scalameta:scalafmt-interfaces": {
|
||||
"shasums": {
|
||||
"jar": "e217de8cbc3324f5e2dea7015a25c91f1b155f665a2a8c3e9c981a0a0f8735b0"
|
||||
"jar": "eb8c7d5fd1728b1ea1905817d18d6e5859e5614ee4bf417b20c8f60c439f6f4c"
|
||||
},
|
||||
"version": "3.11.3"
|
||||
"version": "3.11.5"
|
||||
},
|
||||
"org.scalameta:scalafmt-macros_2.13": {
|
||||
"shasums": {
|
||||
"jar": "90f69e31e837e393b252f07504f240c142449876f1444e01c727625f1b63acdd"
|
||||
"jar": "7edb248e9d74ad95ce8cabf9fd903dc20419f405a3e86eedbeefdf02e291cbe5"
|
||||
},
|
||||
"version": "3.11.3"
|
||||
"version": "3.11.5"
|
||||
},
|
||||
"org.scalameta:scalafmt-sysops_2.13": {
|
||||
"shasums": {
|
||||
"jar": "8bdf5ef43760eaba9e7ef5023db7a9714af2e136899e19cb8cf1ecf0355fef0c"
|
||||
"jar": "314582e8086be283b9df1c22b41269cf22eb6fed5af37be0a6049ba5ee08594e"
|
||||
},
|
||||
"version": "3.11.3"
|
||||
"version": "3.11.5"
|
||||
},
|
||||
"org.scalameta:scalameta_2.13": {
|
||||
"shasums": {
|
||||
"jar": "38123610b842b10010ab3a62cff36f9c1f197a8ace3d84903258580aca4acd55"
|
||||
"jar": "5d7b942d074c764f10da068903f7a8667838d68c6e7a737ad0d0061cada0d182"
|
||||
},
|
||||
"version": "4.17.2"
|
||||
"version": "4.17.3"
|
||||
},
|
||||
"org.scalameta:svm-subs": {
|
||||
"shasums": {
|
||||
@@ -1229,15 +1229,15 @@
|
||||
},
|
||||
"org.scalameta:trees2_2.13": {
|
||||
"shasums": {
|
||||
"jar": "0e75bfede5171c07f8a0d8f0ef581a7af27c9b5f6953182783553aa67fe0fb83"
|
||||
"jar": "3f3729adf6faa48802f8b7c77ac67a6077cb99073596dc3eaf48d6ca9371fe55"
|
||||
},
|
||||
"version": "4.17.2"
|
||||
"version": "4.17.3"
|
||||
},
|
||||
"org.scalameta:trees_2.13": {
|
||||
"shasums": {
|
||||
"jar": "638b5500b8815094a25936b57188fc616358add563a9337f525522eac16ddf24"
|
||||
"jar": "38047a5a7b0f66faad77e18177a6152413de848b23353b615b1cecab2c0212f3"
|
||||
},
|
||||
"version": "4.17.2"
|
||||
"version": "4.17.3"
|
||||
},
|
||||
"org.scalamock:scalamock_3": {
|
||||
"shasums": {
|
||||
@@ -1283,9 +1283,9 @@
|
||||
},
|
||||
"org.xerial:sqlite-jdbc": {
|
||||
"shasums": {
|
||||
"jar": "dc320e4102884c135ccc30c3c6fc3fb190b750e1586a100e3aba3be783cf33a9"
|
||||
"jar": "f55e405ed96d5ffe629e05b7b51b059e1c7d64527c0cc90a972fbac06730ccc1"
|
||||
},
|
||||
"version": "3.53.2.0"
|
||||
"version": "3.53.2.1"
|
||||
},
|
||||
"software.amazon.awssdk:annotations": {
|
||||
"shasums": {
|
||||
@@ -2358,20 +2358,12 @@
|
||||
"autovalue.shaded.com.google.auto.service",
|
||||
"autovalue.shaded.com.google.common.annotations",
|
||||
"autovalue.shaded.com.google.common.base",
|
||||
"autovalue.shaded.com.google.common.cache",
|
||||
"autovalue.shaded.com.google.common.collect",
|
||||
"autovalue.shaded.com.google.common.escape",
|
||||
"autovalue.shaded.com.google.common.eventbus",
|
||||
"autovalue.shaded.com.google.common.graph",
|
||||
"autovalue.shaded.com.google.common.hash",
|
||||
"autovalue.shaded.com.google.common.html",
|
||||
"autovalue.shaded.com.google.common.io",
|
||||
"autovalue.shaded.com.google.common.math",
|
||||
"autovalue.shaded.com.google.common.net",
|
||||
"autovalue.shaded.com.google.common.primitives",
|
||||
"autovalue.shaded.com.google.common.reflect",
|
||||
"autovalue.shaded.com.google.common.util.concurrent",
|
||||
"autovalue.shaded.com.google.common.xml",
|
||||
"autovalue.shaded.com.google.errorprone.annotations",
|
||||
"autovalue.shaded.com.google.errorprone.annotations.concurrent",
|
||||
"autovalue.shaded.com.google.escapevelocity",
|
||||
@@ -2792,8 +2784,6 @@
|
||||
"io.netty.util.concurrent",
|
||||
"io.netty.util.internal",
|
||||
"io.netty.util.internal.logging",
|
||||
"io.netty.util.internal.shaded.org.jctools.counters",
|
||||
"io.netty.util.internal.shaded.org.jctools.maps",
|
||||
"io.netty.util.internal.shaded.org.jctools.queues",
|
||||
"io.netty.util.internal.shaded.org.jctools.queues.atomic",
|
||||
"io.netty.util.internal.shaded.org.jctools.queues.atomic.unpadded",
|
||||
@@ -3059,7 +3049,6 @@
|
||||
"org.apache.commons.compress.compressors.xz",
|
||||
"org.apache.commons.compress.compressors.z",
|
||||
"org.apache.commons.compress.compressors.zstandard",
|
||||
"org.apache.commons.compress.harmony",
|
||||
"org.apache.commons.compress.harmony.archive.internal.nls",
|
||||
"org.apache.commons.compress.harmony.pack200",
|
||||
"org.apache.commons.compress.harmony.unpack200",
|
||||
@@ -4200,8 +4189,6 @@
|
||||
"software.amazon.awssdk.thirdparty.jackson.core.filter",
|
||||
"software.amazon.awssdk.thirdparty.jackson.core.format",
|
||||
"software.amazon.awssdk.thirdparty.jackson.core.internal.shaded.fdp.v2_21_3",
|
||||
"software.amazon.awssdk.thirdparty.jackson.core.internal.shaded.fdp.v2_21_3.bte",
|
||||
"software.amazon.awssdk.thirdparty.jackson.core.internal.shaded.fdp.v2_21_3.chr",
|
||||
"software.amazon.awssdk.thirdparty.jackson.core.io",
|
||||
"software.amazon.awssdk.thirdparty.jackson.core.io.schubfach",
|
||||
"software.amazon.awssdk.thirdparty.jackson.core.json",
|
||||
|
||||
+22
-1
@@ -11,7 +11,8 @@
|
||||
"custom.regex",
|
||||
"github-actions",
|
||||
"gomod",
|
||||
"swift"
|
||||
"swift",
|
||||
"unity3d"
|
||||
],
|
||||
"labels": [
|
||||
"dependencies"
|
||||
@@ -32,6 +33,26 @@
|
||||
"gomod"
|
||||
],
|
||||
"skipArtifactsUpdate": true
|
||||
},
|
||||
{
|
||||
"description": "Propose stable Unity editor patch updates for manual review",
|
||||
"matchManagers": [
|
||||
"unity3d"
|
||||
],
|
||||
"automerge": false,
|
||||
"dependencyDashboardApproval": true,
|
||||
"minimumReleaseAge": "7 days"
|
||||
},
|
||||
{
|
||||
"description": "Keep Unity editor upgrades on the current release line",
|
||||
"matchManagers": [
|
||||
"unity3d"
|
||||
],
|
||||
"matchUpdateTypes": [
|
||||
"major",
|
||||
"minor"
|
||||
],
|
||||
"enabled": false
|
||||
}
|
||||
],
|
||||
"customManagers": [
|
||||
|
||||
@@ -11,6 +11,12 @@ if [ -n "${BAZEL_OUTPUT_BASE:-}" ]; then
|
||||
fi
|
||||
|
||||
"${bazel_cmd[@]}" build \
|
||||
-c opt \
|
||||
--strip=never \
|
||||
--copt=-gline-tables-only \
|
||||
--copt=-fno-omit-frame-pointer \
|
||||
--copt=-mno-omit-leaf-frame-pointer \
|
||||
--linkopt=-Wl,--build-id=sha1 \
|
||||
--platforms=//:linux_arm64 \
|
||||
--extra_toolchains=@llvm_toolchain_linux_arm64//:cc-toolchain-aarch64-linux \
|
||||
"$@" \
|
||||
|
||||
+102
-25
@@ -4,41 +4,118 @@
|
||||
# Reads the active instance from /opt/eagle0/.active-instance (set by deploy-blue-green.sh).
|
||||
#
|
||||
# Usage:
|
||||
# eagle-logs # Tail logs (follow mode)
|
||||
# eagle-logs -n 100 # Show last 100 lines and follow
|
||||
# eagle-logs --no-follow -n 50 # Show last 50 lines without following
|
||||
# eagle-logs # Show existing logs and keep following
|
||||
# eagle-logs -n 100 # Show the last 100 lines and keep following
|
||||
# eagle-logs --no-follow -n 50 # Show the last 50 lines and exit
|
||||
#
|
||||
# To create an alias, add to ~/.bashrc:
|
||||
# alias eagle-logs='/opt/eagle0/scripts/eagle-logs.sh'
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
set -uo pipefail
|
||||
|
||||
APP_DIR="${APP_DIR:-/opt/eagle0}"
|
||||
ACTIVE_FILE="${APP_DIR}/.active-instance"
|
||||
RETRY_INTERVAL_SECONDS="${EAGLE_LOGS_RETRY_INTERVAL_SECONDS:-2}"
|
||||
|
||||
# Read active instance from file, with fallback
|
||||
if [ -f "$ACTIVE_FILE" ]; then
|
||||
ACTIVE=$(cat "$ACTIVE_FILE")
|
||||
else
|
||||
# Fallback: check which container is actually running
|
||||
if docker inspect --format='{{.State.Running}}' eagle-blue 2>/dev/null | grep -q true; then
|
||||
ACTIVE="eagle-blue"
|
||||
elif docker inspect --format='{{.State.Running}}' eagle-green 2>/dev/null | grep -q true; then
|
||||
ACTIVE="eagle-green"
|
||||
else
|
||||
ACTIVE=""
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
|
||||
FOLLOW=true
|
||||
DOCKER_LOG_ARGS=()
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--no-follow)
|
||||
FOLLOW=false
|
||||
;;
|
||||
-f | --follow)
|
||||
FOLLOW=true
|
||||
;;
|
||||
*)
|
||||
DOCKER_LOG_ARGS+=("$arg")
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
container_is_running() {
|
||||
[ "$(docker inspect --format='{{.State.Running}}' "$1" 2>/dev/null)" = "true" ]
|
||||
}
|
||||
|
||||
resolve_active_instance() {
|
||||
local recorded_active=""
|
||||
|
||||
if [ -f "$ACTIVE_FILE" ]; then
|
||||
IFS= read -r recorded_active < "$ACTIVE_FILE" || true
|
||||
fi
|
||||
|
||||
if [ -n "$recorded_active" ] && container_is_running "$recorded_active"; then
|
||||
printf '%s\n' "$recorded_active"
|
||||
elif container_is_running eagle-blue; then
|
||||
printf '%s\n' "eagle-blue"
|
||||
elif container_is_running eagle-green; then
|
||||
printf '%s\n' "eagle-green"
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
print_disconnect_marker() {
|
||||
local stopped_instance=${1:-unknown}
|
||||
|
||||
printf '\n' >&2
|
||||
printf '%s\n' '================================================================================' >&2
|
||||
printf '%s\n' '==================== EAGLE LOG STREAM DISCONNECTED ============================' >&2
|
||||
printf ' Server unavailable at %s (last instance: %s)\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$stopped_instance" >&2
|
||||
printf '%s\n' ' Waiting for the deployment or server restart; log following will resume.' >&2
|
||||
printf '%s\n' '================================================================================' >&2
|
||||
}
|
||||
|
||||
print_resume_marker() {
|
||||
local active_instance=$1
|
||||
|
||||
printf '\n' >&2
|
||||
printf '%s\n' '================================================================================' >&2
|
||||
printf '%s\n' '======================= EAGLE LOG STREAM RESUMED ==============================' >&2
|
||||
printf ' Attached to %s at %s\n' "$active_instance" "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" >&2
|
||||
printf '%s\n' '================================================================================' >&2
|
||||
}
|
||||
|
||||
if [ "$FOLLOW" = false ]; then
|
||||
if ! ACTIVE=$(resolve_active_instance); then
|
||||
echo "Error: No active Eagle instance found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exec docker logs "${DOCKER_LOG_ARGS[@]}" "$ACTIVE"
|
||||
fi
|
||||
|
||||
if [ -z "$ACTIVE" ]; then
|
||||
echo "Error: No active Eagle instance found" >&2
|
||||
exit 1
|
||||
fi
|
||||
ACTIVE=""
|
||||
DISCONNECTED=false
|
||||
RECONNECT_SINCE=""
|
||||
|
||||
# Default to follow mode if no args provided
|
||||
if [ $# -eq 0 ]; then
|
||||
exec docker logs -f "$ACTIVE"
|
||||
else
|
||||
exec docker logs "$@" "$ACTIVE"
|
||||
fi
|
||||
while true; do
|
||||
if NEXT_ACTIVE=$(resolve_active_instance); then
|
||||
if [ "$DISCONNECTED" = true ]; then
|
||||
print_resume_marker "$NEXT_ACTIVE"
|
||||
fi
|
||||
|
||||
ACTIVE="$NEXT_ACTIVE"
|
||||
DISCONNECTED=false
|
||||
|
||||
if [ -n "$RECONNECT_SINCE" ]; then
|
||||
docker logs --follow "${DOCKER_LOG_ARGS[@]}" --since "$RECONNECT_SINCE" "$ACTIVE" || true
|
||||
else
|
||||
docker logs --follow "${DOCKER_LOG_ARGS[@]}" "$ACTIVE" || true
|
||||
fi
|
||||
|
||||
RECONNECT_SINCE=$(date +%s)
|
||||
fi
|
||||
|
||||
if [ "$DISCONNECTED" = false ]; then
|
||||
print_disconnect_marker "$ACTIVE"
|
||||
DISCONNECTED=true
|
||||
fi
|
||||
|
||||
sleep "$RETRY_INTERVAL_SECONDS"
|
||||
done
|
||||
|
||||
Executable
+85
@@ -0,0 +1,85 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
TEST_DIR=$(mktemp -d)
|
||||
trap 'rm -rf "$TEST_DIR"' EXIT
|
||||
|
||||
FAKE_BIN="${TEST_DIR}/bin"
|
||||
APP_DIR="${TEST_DIR}/app"
|
||||
STATE_FILE="${TEST_DIR}/state"
|
||||
CALLS_FILE="${TEST_DIR}/calls"
|
||||
OUTPUT_FILE="${TEST_DIR}/output"
|
||||
mkdir -p "$FAKE_BIN" "$APP_DIR"
|
||||
|
||||
cat > "${FAKE_BIN}/docker" <<'EOF'
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
case "$1" in
|
||||
inspect)
|
||||
container=${@: -1}
|
||||
state=$(cat "$EAGLE_LOGS_TEST_STATE_FILE")
|
||||
if { [ "$state" = "blue" ] && [ "$container" = "eagle-blue" ]; } ||
|
||||
{ [ "$state" = "green" ] && [ "$container" = "eagle-green" ]; }; then
|
||||
echo true
|
||||
else
|
||||
echo false
|
||||
fi
|
||||
;;
|
||||
logs)
|
||||
container=${@: -1}
|
||||
printf '%s\n' "$*" >> "$EAGLE_LOGS_TEST_CALLS_FILE"
|
||||
if [ "$container" = "eagle-blue" ]; then
|
||||
echo "last blue log"
|
||||
echo "eagle-green" > "$EAGLE_LOGS_TEST_ACTIVE_FILE"
|
||||
echo "green" > "$EAGLE_LOGS_TEST_STATE_FILE"
|
||||
else
|
||||
echo "first green log"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "Unexpected docker command: $*" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
EOF
|
||||
chmod +x "${FAKE_BIN}/docker"
|
||||
|
||||
export EAGLE_LOGS_TEST_STATE_FILE="$STATE_FILE"
|
||||
export EAGLE_LOGS_TEST_CALLS_FILE="$CALLS_FILE"
|
||||
export EAGLE_LOGS_TEST_ACTIVE_FILE="${APP_DIR}/.active-instance"
|
||||
|
||||
echo "blue" > "$STATE_FILE"
|
||||
echo "eagle-blue" > "${APP_DIR}/.active-instance"
|
||||
|
||||
PATH="${FAKE_BIN}:$PATH" APP_DIR="$APP_DIR" "${SCRIPT_DIR}/eagle-logs.sh" --no-follow -n 25
|
||||
grep -q '^logs -n 25 eagle-blue$' "$CALLS_FILE"
|
||||
|
||||
: > "$CALLS_FILE"
|
||||
echo "blue" > "$STATE_FILE"
|
||||
echo "eagle-blue" > "${APP_DIR}/.active-instance"
|
||||
PATH="${FAKE_BIN}:$PATH" APP_DIR="$APP_DIR" EAGLE_LOGS_RETRY_INTERVAL_SECONDS=0.01 \
|
||||
"${SCRIPT_DIR}/eagle-logs.sh" -n 10 > "$OUTPUT_FILE" 2>&1 &
|
||||
FOLLOW_PID=$!
|
||||
|
||||
for _ in $(seq 1 100); do
|
||||
if grep -q 'first green log' "$OUTPUT_FILE"; then
|
||||
break
|
||||
fi
|
||||
sleep 0.01
|
||||
done
|
||||
|
||||
kill "$FOLLOW_PID" 2>/dev/null || true
|
||||
wait "$FOLLOW_PID" 2>/dev/null || true
|
||||
|
||||
grep -q 'EAGLE LOG STREAM DISCONNECTED' "$OUTPUT_FILE"
|
||||
grep -q 'EAGLE LOG STREAM RESUMED' "$OUTPUT_FILE"
|
||||
grep -q 'last blue log' "$OUTPUT_FILE"
|
||||
grep -q 'first green log' "$OUTPUT_FILE"
|
||||
grep -q '^logs --follow -n 10 eagle-blue$' "$CALLS_FILE"
|
||||
grep -Eq '^logs --follow -n 10 --since [0-9]+ eagle-green$' "$CALLS_FILE"
|
||||
|
||||
echo "eagle-logs tests passed"
|
||||
@@ -105,6 +105,7 @@ public:
|
||||
|
||||
class Iter {
|
||||
public:
|
||||
using iterator_concept = std::forward_iterator_tag;
|
||||
using iterator_category = std::forward_iterator_tag;
|
||||
using difference_type = std::ptrdiff_t;
|
||||
using value_type = size_t;
|
||||
@@ -112,14 +113,14 @@ public:
|
||||
using reference = size_t;
|
||||
|
||||
private:
|
||||
size_t storeIndex;
|
||||
size_t bitIndex;
|
||||
size_t storeIndex = STORE_COUNT;
|
||||
size_t bitIndex = 0;
|
||||
|
||||
const IterableBitset& bs;
|
||||
const IterableBitset* bs = nullptr;
|
||||
|
||||
void ToNext() {
|
||||
do {
|
||||
const STORE_TYPE current = bs.store[storeIndex] >> bitIndex;
|
||||
const STORE_TYPE current = bs->store[storeIndex] >> bitIndex;
|
||||
if (current != 0) {
|
||||
// Skip ahead by the number of trailing zeroes
|
||||
const auto ctz = std::countr_zero(current);
|
||||
@@ -132,10 +133,12 @@ public:
|
||||
}
|
||||
|
||||
public:
|
||||
Iter() = default;
|
||||
|
||||
Iter(const IterableBitset& bs, const int startStore, const int startBit)
|
||||
: storeIndex(startStore),
|
||||
bitIndex(startBit),
|
||||
bs(bs) {
|
||||
bs(&bs) {
|
||||
ToNext();
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,8 @@ auto AttackLocations::LocationsWithEnemyInRange(const Unit *unit) const -> Coord
|
||||
|
||||
AttackLocationsCache::AttackLocationsCache(const HexMap *hexMap, const SettingsGetter &settings)
|
||||
: rowCount(hexMap->row_count()),
|
||||
columnCount(hexMap->column_count()) {
|
||||
columnCount(hexMap->column_count()),
|
||||
landRegions(hexMap) {
|
||||
const int mageRange =
|
||||
std::max(settings.Backing().meteor_range(), settings.Backing().lightning_range());
|
||||
const auto fearRange = settings.Backing().fear_range();
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/Coordinates.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/LandRegions.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/unit.hpp"
|
||||
|
||||
@@ -151,6 +152,8 @@ struct alignas(128) AttackLocationsCache {
|
||||
private:
|
||||
const MapIndex rowCount;
|
||||
const MapIndex columnCount;
|
||||
// Immutable static-map analysis shared by every speculative state evaluated by this AI.
|
||||
const LandRegions landRegions;
|
||||
|
||||
// For each of these vectors, the index is the map index of the tile *that can be attacked*,
|
||||
// and the entry is the CoordsSet of the tiles that can attack that location.
|
||||
@@ -161,6 +164,7 @@ private:
|
||||
public:
|
||||
AttackLocationsCache(const HexMap* hexMap, const SettingsGetter& settings);
|
||||
|
||||
[[nodiscard]] auto GetLandRegions() const -> const LandRegions& { return landRegions; }
|
||||
auto CachedLocations(const Coords& singleTarget, bool lateGame) -> const AttackLocations&;
|
||||
auto CachedLocations(const CoordsSet& targets, bool lateGame) -> AttackLocations;
|
||||
auto CachedLocations(const std::vector<const Unit*>& enemies, bool lateGame) -> AttackLocations;
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
|
||||
namespace shardok {
|
||||
|
||||
constexpr bool kMultithread = true;
|
||||
constexpr bool kLogging = false;
|
||||
|
||||
[[nodiscard]] static auto AverageGenerator() -> std::shared_ptr<SequenceRandomGenerator> {
|
||||
@@ -120,10 +119,12 @@ static auto CommandSorter(
|
||||
AICommandEvaluator::AICommandEvaluator(
|
||||
const AIScoreCalculator& scorer,
|
||||
const APDCache& apdCache,
|
||||
BattalionTypeGetter battalionTypeGetter)
|
||||
BattalionTypeGetter battalionTypeGetter,
|
||||
AIThreadPool& threadPool)
|
||||
: scorer_(scorer),
|
||||
apdCache_(apdCache),
|
||||
battalionTypeGetter_(std::move(battalionTypeGetter)) {}
|
||||
battalionTypeGetter_(std::move(battalionTypeGetter)),
|
||||
threadPool_(threadPool) {}
|
||||
|
||||
auto AICommandEvaluator::PerformLookahead(
|
||||
const PlayerId pid,
|
||||
@@ -295,7 +296,7 @@ auto AICommandEvaluator::EvaluateWithRandomness(
|
||||
returnValue.lookaheadScore = p.get_future();
|
||||
p.set_value(EvaluationResult{.score = innerUtility, .completed = true});
|
||||
} else {
|
||||
auto lookaheadLambda = [this,
|
||||
auto lookaheadLambda = [evaluator = *this,
|
||||
pid,
|
||||
isDefender,
|
||||
remainingLookahead,
|
||||
@@ -307,8 +308,11 @@ auto AICommandEvaluator::EvaluateWithRandomness(
|
||||
guessedCommands,
|
||||
allCastleCoords,
|
||||
deadline]() -> EvaluationResult {
|
||||
if (std::chrono::steady_clock::now() > deadline) {
|
||||
return EvaluationResult{.score = 0.0, .completed = false};
|
||||
}
|
||||
try {
|
||||
auto lookaheadFuture = PerformLookahead(
|
||||
auto lookaheadFuture = evaluator.PerformLookahead(
|
||||
pid,
|
||||
isDefender,
|
||||
remainingLookahead,
|
||||
@@ -329,15 +333,14 @@ auto AICommandEvaluator::EvaluateWithRandomness(
|
||||
}
|
||||
};
|
||||
|
||||
if constexpr (kMultithread) {
|
||||
auto launchPolicy =
|
||||
remainingLookahead == 1 ? std::launch::async : std::launch::deferred;
|
||||
returnValue.lookaheadScore = std::async(launchPolicy, lookaheadLambda);
|
||||
// Only the depth-one frontier is parallel. A pool worker therefore reaches depth zero,
|
||||
// never another queued frontier. AIThreadPool also executes same-pool reentrant
|
||||
// submissions inline as a hard backstop against worker-waits-on-worker deadlocks.
|
||||
if (remainingLookahead == 1) {
|
||||
returnValue.lookaheadScore = threadPool_.Submit(std::move(lookaheadLambda));
|
||||
} else {
|
||||
std::promise<EvaluationResult> p;
|
||||
returnValue.lookaheadScore = p.get_future();
|
||||
auto lambdaResult = lookaheadLambda();
|
||||
p.set_value(lambdaResult);
|
||||
returnValue.lookaheadScore =
|
||||
std::async(std::launch::deferred, std::move(lookaheadLambda));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <future>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIStrategy.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIThreadPool.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCTypes.h"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistancesCache.hpp"
|
||||
@@ -35,7 +36,8 @@ public:
|
||||
AICommandEvaluator(
|
||||
const AIScoreCalculator& scorer,
|
||||
const APDCache& apdCache,
|
||||
BattalionTypeGetter battalionTypeGetter); // Pass by value
|
||||
BattalionTypeGetter battalionTypeGetter,
|
||||
AIThreadPool& threadPool = SharedAIThreadPool()); // Pass getter by value
|
||||
|
||||
/// Evaluates the score for a particular command index with lookahead.
|
||||
struct EvaluationResult {
|
||||
@@ -79,6 +81,7 @@ private:
|
||||
const AIScoreCalculator& scorer_;
|
||||
const APDCache& apdCache_;
|
||||
BattalionTypeGetter battalionTypeGetter_;
|
||||
AIThreadPool& threadPool_;
|
||||
|
||||
struct ImmediateAndLookaheadScore {
|
||||
ScoreValue immediateScore{};
|
||||
|
||||
@@ -4,9 +4,6 @@
|
||||
|
||||
#include "AIDefenderStrategySelector.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <ranges>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIAttackGroups.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIScoreUtilities.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIWaterCrossingCalculator.hpp"
|
||||
@@ -23,31 +20,14 @@ auto AIDefenderStrategySelector::BestDefenderStrategy(
|
||||
const CoordsSet& criticalTileCoords,
|
||||
int maxRounds,
|
||||
const APDCache& apdCache,
|
||||
const LandRegions& landRegions,
|
||||
const BattalionTypeGetter& battalionTypeGetter) -> AIStrategy {
|
||||
uint32_t attackerNonUndeadUnitCount = 0;
|
||||
uint32_t attackerNonUndeadUnitNotRequiringWaterCrossingCount = 0;
|
||||
int attackerTroops = 0;
|
||||
int defenderTroops = 0;
|
||||
|
||||
bool canFlee = false;
|
||||
|
||||
vector<UnitId> attackerUnitIdsRequiringWaterCrossing{};
|
||||
attackerUnitIdsRequiringWaterCrossing.reserve(gameState->units()->size());
|
||||
for (const auto& player : *gameState->player_infos()) {
|
||||
if (!player->is_defender()) {
|
||||
const auto& unitIdsRequiringWaterCrossing = UnitIdsRequiringWaterCrossing(
|
||||
gameState,
|
||||
player->player_id(),
|
||||
criticalTileCoords,
|
||||
apdCache,
|
||||
battalionTypeGetter);
|
||||
attackerUnitIdsRequiringWaterCrossing.insert(
|
||||
attackerUnitIdsRequiringWaterCrossing.end(),
|
||||
unitIdsRequiringWaterCrossing.begin(),
|
||||
unitIdsRequiringWaterCrossing.end());
|
||||
}
|
||||
}
|
||||
|
||||
for (const Unit* unit : *gameState->units()) {
|
||||
if (unit->status() != net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
|
||||
unit->status() != net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT)
|
||||
@@ -63,12 +43,6 @@ auto AIDefenderStrategySelector::BestDefenderStrategy(
|
||||
if (unit->battalion().type() !=
|
||||
net::eagle0::shardok::storage::fb::BattalionTypeId_UNDEAD) {
|
||||
++attackerNonUndeadUnitCount;
|
||||
|
||||
if (!std::ranges::contains(
|
||||
attackerUnitIdsRequiringWaterCrossing,
|
||||
unit->unit_id())) {
|
||||
++attackerNonUndeadUnitNotRequiringWaterCrossingCount;
|
||||
}
|
||||
}
|
||||
attackerTroops += unit->battalion().size();
|
||||
}
|
||||
@@ -86,11 +60,21 @@ auto AIDefenderStrategySelector::BestDefenderStrategy(
|
||||
// other considerations we should have but do not presently:
|
||||
// * the attacker is close to the defender / castles
|
||||
|
||||
if (canFlee && roundsRemaining > 2 &&
|
||||
defenderTroops < MAXIMUM_RATIO_FOR_DEFENDER_TO_FLEE * attackerTroops &&
|
||||
attackerNonUndeadUnitNotRequiringWaterCrossingCount >= criticalTileCoords.size()) {
|
||||
chosenStrategy = FleeStrategy;
|
||||
} else if (attackerNonUndeadUnitCount >= criticalTileCoords.size()) {
|
||||
const bool waterCrossingCouldAffectStrategy =
|
||||
canFlee && roundsRemaining > 2 &&
|
||||
defenderTroops < MAXIMUM_RATIO_FOR_DEFENDER_TO_FLEE * attackerTroops &&
|
||||
attackerNonUndeadUnitCount >= criticalTileCoords.size();
|
||||
if (waterCrossingCouldAffectStrategy && HasEnoughNonUndeadAttackersWithoutWaterCrossing(
|
||||
gameState,
|
||||
criticalTileCoords,
|
||||
landRegions,
|
||||
apdCache,
|
||||
battalionTypeGetter,
|
||||
criticalTileCoords.size())) {
|
||||
return FleeStrategy;
|
||||
}
|
||||
|
||||
if (attackerNonUndeadUnitCount >= criticalTileCoords.size()) {
|
||||
chosenStrategy = HoldCastlesStrategy;
|
||||
} else if (defenderTroops > MINIMUM_RATIO_FOR_DEFENDER_TO_HOLD * attackerTroops) {
|
||||
chosenStrategy = HoldCastlesStrategy;
|
||||
|
||||
@@ -22,6 +22,7 @@ public:
|
||||
const CoordsSet& criticalTileCoords,
|
||||
int maxRounds,
|
||||
const APDCache& apdCache,
|
||||
const LandRegions& landRegions,
|
||||
const BattalionTypeGetter& battalionTypeGetter) -> AIStrategy;
|
||||
};
|
||||
} // namespace shardok
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
#include "AIThreadPool.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace shardok {
|
||||
|
||||
namespace {
|
||||
|
||||
// Keep the process responsive to gRPC and command handling while still giving lookahead enough
|
||||
// parallelism to saturate typical production hosts.
|
||||
constexpr size_t kMaximumAIWorkerCount = 12;
|
||||
|
||||
} // namespace
|
||||
|
||||
thread_local const AIThreadPool* AIThreadPool::currentPool_ = nullptr;
|
||||
|
||||
AIThreadPool::AIThreadPool(const size_t workerCount) {
|
||||
const size_t normalizedWorkerCount = std::max<size_t>(1, workerCount);
|
||||
workers_.reserve(normalizedWorkerCount);
|
||||
try {
|
||||
for (size_t index = 0; index < normalizedWorkerCount; ++index) {
|
||||
workers_.emplace_back([this] { RunWorker(); });
|
||||
}
|
||||
} catch (...) {
|
||||
StopAndJoin();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
AIThreadPool::~AIThreadPool() { StopAndJoin(); }
|
||||
|
||||
void AIThreadPool::RunWorker() {
|
||||
currentPool_ = this;
|
||||
while (true) {
|
||||
std::function<void()> task;
|
||||
{
|
||||
std::unique_lock lock(mutex_);
|
||||
condition_.wait(lock, [this] { return stopping_ || !tasks_.empty(); });
|
||||
if (stopping_ && tasks_.empty()) { break; }
|
||||
task = std::move(tasks_.front());
|
||||
tasks_.pop();
|
||||
}
|
||||
task();
|
||||
}
|
||||
currentPool_ = nullptr;
|
||||
}
|
||||
|
||||
void AIThreadPool::StopAndJoin() {
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
stopping_ = true;
|
||||
}
|
||||
condition_.notify_all();
|
||||
for (auto& worker : workers_) {
|
||||
if (worker.joinable()) { worker.join(); }
|
||||
}
|
||||
}
|
||||
|
||||
auto AIWorkerCountForHardwareConcurrency(const unsigned int hardwareConcurrency) -> size_t {
|
||||
if (hardwareConcurrency <= 1) { return 1; }
|
||||
return std::min(kMaximumAIWorkerCount, static_cast<size_t>(hardwareConcurrency - 1));
|
||||
}
|
||||
|
||||
auto SharedAIThreadPool() -> AIThreadPool& {
|
||||
static AIThreadPool pool(
|
||||
AIWorkerCountForHardwareConcurrency(std::thread::hardware_concurrency()));
|
||||
return pool;
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
@@ -0,0 +1,79 @@
|
||||
//
|
||||
// Fixed-size worker pool for iterative-deepening lookahead.
|
||||
//
|
||||
|
||||
#ifndef EAGLE0_SHARDOK_AI_AI_THREAD_POOL_HPP
|
||||
#define EAGLE0_SHARDOK_AI_AI_THREAD_POOL_HPP
|
||||
|
||||
#include <condition_variable>
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <queue>
|
||||
#include <stdexcept>
|
||||
#include <thread>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace shardok {
|
||||
|
||||
class AIThreadPool {
|
||||
public:
|
||||
explicit AIThreadPool(size_t workerCount);
|
||||
~AIThreadPool();
|
||||
|
||||
AIThreadPool(const AIThreadPool&) = delete;
|
||||
auto operator=(const AIThreadPool&) -> AIThreadPool& = delete;
|
||||
AIThreadPool(AIThreadPool&&) = delete;
|
||||
auto operator=(AIThreadPool&&) -> AIThreadPool& = delete;
|
||||
|
||||
template<typename Function>
|
||||
[[nodiscard]] auto Submit(Function&& function)
|
||||
-> std::future<std::invoke_result_t<std::decay_t<Function>&>> {
|
||||
using Result = std::invoke_result_t<std::decay_t<Function>&>;
|
||||
|
||||
auto task =
|
||||
std::make_shared<std::packaged_task<Result()>>(std::forward<Function>(function));
|
||||
auto result = task->get_future();
|
||||
|
||||
// A worker must never enqueue work and then wait for the same bounded pool. Running
|
||||
// reentrant submissions inline makes that safe even if recursive search scheduling
|
||||
// changes in the future.
|
||||
if (currentPool_ == this) {
|
||||
(*task)();
|
||||
return result;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
if (stopping_) { throw std::runtime_error("Submit on stopped AIThreadPool"); }
|
||||
tasks_.emplace([task] { (*task)(); });
|
||||
}
|
||||
condition_.notify_one();
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto WorkerCount() const -> size_t { return workers_.size(); }
|
||||
|
||||
private:
|
||||
void RunWorker();
|
||||
void StopAndJoin();
|
||||
|
||||
static thread_local const AIThreadPool* currentPool_;
|
||||
|
||||
std::vector<std::thread> workers_;
|
||||
std::queue<std::function<void()>> tasks_;
|
||||
std::mutex mutex_;
|
||||
std::condition_variable condition_;
|
||||
bool stopping_ = false;
|
||||
};
|
||||
|
||||
[[nodiscard]] auto AIWorkerCountForHardwareConcurrency(unsigned int hardwareConcurrency) -> size_t;
|
||||
[[nodiscard]] auto SharedAIThreadPool() -> AIThreadPool&;
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
#endif // EAGLE0_SHARDOK_AI_AI_THREAD_POOL_HPP
|
||||
@@ -5,10 +5,13 @@
|
||||
#include "AIWaterCrossingCalculator.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <optional>
|
||||
|
||||
#include "AIMinimumDistanceAndTarget.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/HexMapHelpers.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/HexMapHasher.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/LandRegions.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"
|
||||
|
||||
namespace shardok {
|
||||
@@ -22,6 +25,112 @@ auto RecomputeModifierHash(HexMap *hexMap) -> MapId {
|
||||
return ActionPointDistancesCache::GetMapId(hexMap);
|
||||
}
|
||||
|
||||
auto HasNonWaterFire(const HexMap *hexMap) -> bool {
|
||||
return std::ranges::any_of(*hexMap->terrain(), [](const auto *terrain) {
|
||||
return !IsWater(terrain->type()) && terrain->modifier().fire().present();
|
||||
});
|
||||
}
|
||||
|
||||
auto StaticRegionsAreConclusiveFor(const BattalionType &battalionType) -> bool {
|
||||
return battalionType.costToEnterWater.type == ActionCost::impossible &&
|
||||
battalionType.costToEnterMountain.type == ActionCost::impossible;
|
||||
}
|
||||
|
||||
auto CanReachWithDistances(
|
||||
const Coords &origin,
|
||||
const Coords &destination,
|
||||
const ActionPointDistances *distances) -> bool {
|
||||
return distances->Distance(origin, destination) != ActionPointDistances::IMPOSSIBLE;
|
||||
}
|
||||
|
||||
auto CanReachAllWithDistances(
|
||||
const Coords &origin,
|
||||
const CoordsSet &destinations,
|
||||
const ActionPointDistances *distances) -> bool {
|
||||
return std::ranges::all_of(destinations, [&](const Coords &destination) {
|
||||
return CanReachWithDistances(origin, destination, distances);
|
||||
});
|
||||
}
|
||||
|
||||
class EffectiveDestinations {
|
||||
private:
|
||||
const EffectiveLandRegions &effectiveLandRegions;
|
||||
std::optional<Coords> representative;
|
||||
bool shareRegion;
|
||||
|
||||
public:
|
||||
EffectiveDestinations(
|
||||
const EffectiveLandRegions &effectiveLandRegions,
|
||||
const CoordsSet &destinations)
|
||||
: effectiveLandRegions(effectiveLandRegions),
|
||||
shareRegion(true) {
|
||||
for (const Coords &destination : destinations) {
|
||||
if (!representative.has_value()) {
|
||||
representative = destination;
|
||||
} else if (!effectiveLandRegions.DefinitelyConnectedWithoutCreatingCrossing(
|
||||
*representative,
|
||||
destination)) {
|
||||
shareRegion = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] auto DefinitelyReachableFrom(const Coords &origin) const -> bool {
|
||||
return !representative.has_value() ||
|
||||
(shareRegion && effectiveLandRegions.DefinitelyConnectedWithoutCreatingCrossing(
|
||||
origin,
|
||||
*representative));
|
||||
}
|
||||
};
|
||||
|
||||
auto DefinitelyReachesAllWithoutCreatingCrossing(
|
||||
const HexMap *map,
|
||||
const net::eagle0::shardok::storage::fb::Unit *unit,
|
||||
const EffectiveDestinations &effectiveDestinations) -> bool {
|
||||
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) {
|
||||
return effectiveDestinations.DefinitelyReachableFrom(unit->location());
|
||||
}
|
||||
|
||||
const auto *startingPositions =
|
||||
map->attacker_starting_positions()->Get(unit->starting_position_index())->positions();
|
||||
return std::ranges::all_of(*startingPositions, [&](const Coords *position) {
|
||||
return effectiveDestinations.DefinitelyReachableFrom(*position);
|
||||
});
|
||||
}
|
||||
|
||||
auto CanReachAllWithoutWater(
|
||||
const HexMap *map,
|
||||
const net::eagle0::shardok::storage::fb::Unit *unit,
|
||||
const CoordsSet &destinations,
|
||||
const ActionPointDistances *distances) -> bool {
|
||||
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) {
|
||||
return CanReachAllWithDistances(unit->location(), destinations, distances);
|
||||
}
|
||||
|
||||
const auto *startingPositions =
|
||||
map->attacker_starting_positions()->Get(unit->starting_position_index())->positions();
|
||||
return std::ranges::all_of(*startingPositions, [&](const Coords *position) {
|
||||
return CanReachAllWithDistances(*position, destinations, distances);
|
||||
});
|
||||
}
|
||||
|
||||
auto IsActiveNonUndeadAttacker(
|
||||
const GameState *gameState,
|
||||
const net::eagle0::shardok::storage::fb::Unit *unit) -> bool {
|
||||
if (unit->status() != net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
|
||||
unit->status() != net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT) {
|
||||
return false;
|
||||
}
|
||||
if (unit->battalion().type() == net::eagle0::shardok::storage::fb::BattalionTypeId_UNDEAD) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return std::ranges::any_of(*gameState->player_infos(), [&](const auto *playerInfo) {
|
||||
return playerInfo->player_id() == unit->player_id() && !playerInfo->is_defender();
|
||||
});
|
||||
}
|
||||
|
||||
auto CanCreateWaterCrossing(
|
||||
const net::eagle0::shardok::storage::fb::Unit *unit,
|
||||
const BattalionTypeGetter &battalionTypeGetter) -> bool {
|
||||
@@ -41,34 +150,105 @@ auto UnitIdsRequiringWaterCrossing(
|
||||
const GameStateW &gameState,
|
||||
const PlayerId pid,
|
||||
const CoordsSet &destinations,
|
||||
const LandRegions &landRegions,
|
||||
const APDCache &apdCache,
|
||||
const BattalionTypeGetter &battalionTypeGetter) -> vector<UnitId> {
|
||||
// Put out all the fires, except on bridges
|
||||
fb::HexMapW mapCopy = fb::CopyHexMap(gameState->hex_map());
|
||||
for (uint32_t index = 0; index < mapCopy->terrain()->size(); ++index) {
|
||||
if (IsWater(mapCopy->terrain()->Get(index)->type())) continue;
|
||||
// const_cast is safe because we own the mutable buffer (mapCopy)
|
||||
const_cast<net::eagle0::shardok::storage::fb::Terrain *>(
|
||||
mapCopy->mutable_terrain()->GetMutableObject(index))
|
||||
->mutable_modifier()
|
||||
.mutable_fire()
|
||||
.mutate_present(false);
|
||||
}
|
||||
const GameState *state = gameState.Get();
|
||||
const auto *units = state->units();
|
||||
const HexMap *map = state->hex_map();
|
||||
const EffectiveLandRegions effectiveLandRegions(landRegions, map);
|
||||
const EffectiveDestinations effectiveDestinations(effectiveLandRegions, destinations);
|
||||
|
||||
const auto mapId = ActionPointDistancesCache::GetMapId(mapCopy);
|
||||
|
||||
vector<UnitId> unitIdsToReturn{};
|
||||
|
||||
// Set this to -1 for unchecked, 0 for does not need crossing, 1 for does need crossing
|
||||
std::vector<int> startingPositionIndices(9, -1);
|
||||
for (const auto *unit : *gameState->units()) {
|
||||
std::vector<const net::eagle0::shardok::storage::fb::Unit *> unitsToCheck{};
|
||||
const auto queueForExactCheck = [&](const auto *unit) {
|
||||
if (unitsToCheck.empty()) { unitsToCheck.reserve(units->size()); }
|
||||
unitsToCheck.push_back(unit);
|
||||
};
|
||||
std::array<int8_t, 9> startingPositionIndices{};
|
||||
startingPositionIndices.fill(-1);
|
||||
for (const auto *unit : *units) {
|
||||
if (unit->player_id() != pid) continue;
|
||||
|
||||
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) {
|
||||
if (!effectiveDestinations.DefinitelyReachableFrom(unit->location())) {
|
||||
queueForExactCheck(unit);
|
||||
}
|
||||
} else if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT) {
|
||||
if (startingPositionIndices[unit->starting_position_index()] == -1) {
|
||||
startingPositionIndices[unit->starting_position_index()] = 0;
|
||||
const auto *startingPositions = map->attacker_starting_positions()
|
||||
->Get(unit->starting_position_index())
|
||||
->positions();
|
||||
for (const Coords *position : *startingPositions) {
|
||||
if (!effectiveDestinations.DefinitelyReachableFrom(*position)) {
|
||||
startingPositionIndices[unit->starting_position_index()] = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (startingPositionIndices[unit->starting_position_index()] == 1) {
|
||||
queueForExactCheck(unit);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (unitsToCheck.empty()) return {};
|
||||
|
||||
MapId mapId = ActionPointDistancesCache::GetMapId(map);
|
||||
|
||||
fb::HexMapW fireClearedMap;
|
||||
bool mapPreparedForDistances = false;
|
||||
std::array<const ActionPointDistances *, BattalionTypeId::BattalionTypeId_MAX + 1>
|
||||
distancesByBattalionType{};
|
||||
const auto distancesFor = [&](const BattalionTypeSPtr &battalionType) {
|
||||
if (!mapPreparedForDistances) {
|
||||
mapPreparedForDistances = true;
|
||||
|
||||
// Most states have no relevant fire, so avoid copying the entire map unless exact
|
||||
// distances need it.
|
||||
if (HasNonWaterFire(map)) {
|
||||
fireClearedMap = fb::CopyHexMap(map);
|
||||
for (uint32_t index = 0; index < fireClearedMap->terrain()->size(); ++index) {
|
||||
if (IsWater(fireClearedMap->terrain()->Get(index)->type())) continue;
|
||||
// const_cast is safe because we own the mutable buffer (fireClearedMap)
|
||||
const_cast<net::eagle0::shardok::storage::fb::Terrain *>(
|
||||
fireClearedMap->mutable_terrain()->GetMutableObject(index))
|
||||
->mutable_modifier()
|
||||
.mutable_fire()
|
||||
.mutate_present(false);
|
||||
}
|
||||
|
||||
map = fireClearedMap.Get();
|
||||
mapId = RecomputeModifierHash(fireClearedMap.Get());
|
||||
}
|
||||
}
|
||||
|
||||
auto &distances = distancesByBattalionType[battalionType->typeId];
|
||||
if (distances == nullptr) {
|
||||
distances = apdCache->GetRaw(map, mapId, battalionType, false);
|
||||
}
|
||||
return distances;
|
||||
};
|
||||
|
||||
vector<UnitId> unitIdsToReturn{};
|
||||
unitIdsToReturn.reserve(unitsToCheck.size());
|
||||
|
||||
// The effective-region pass above removed every unit that can reach every destination through
|
||||
// static land and current bridges. Resolve full APDs only for battalions that can traverse raw
|
||||
// water or mountains.
|
||||
startingPositionIndices.fill(-1);
|
||||
for (const auto *unit : unitsToCheck) {
|
||||
const auto &battType = battalionTypeGetter(unit->battalion().type());
|
||||
|
||||
if (StaticRegionsAreConclusiveFor(*battType)) {
|
||||
unitIdsToReturn.push_back(unit->unit_id());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) {
|
||||
const auto *distances = distancesFor(battType);
|
||||
for (const Coords &destination : destinations) {
|
||||
if (!CanReach(unit->location(), destination, mapCopy, mapId, apdCache, battType)) {
|
||||
if (!CanReachWithDistances(unit->location(), destination, distances)) {
|
||||
unitIdsToReturn.push_back(unit->unit_id());
|
||||
break;
|
||||
}
|
||||
@@ -76,14 +256,14 @@ auto UnitIdsRequiringWaterCrossing(
|
||||
} else if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT) {
|
||||
if (startingPositionIndices[unit->starting_position_index()] == -1) {
|
||||
startingPositionIndices[unit->starting_position_index()] = 0;
|
||||
const auto *startingPositions = gameState->hex_map()
|
||||
->attacker_starting_positions()
|
||||
const auto *distances = distancesFor(battType);
|
||||
const auto *startingPositions = map->attacker_starting_positions()
|
||||
->Get(unit->starting_position_index())
|
||||
->positions();
|
||||
|
||||
for (const Coords *pos : *startingPositions) {
|
||||
for (const Coords &destination : destinations) {
|
||||
if (!CanReach(*pos, destination, mapCopy, mapId, apdCache, battType)) {
|
||||
if (!CanReachWithDistances(*pos, destination, distances)) {
|
||||
startingPositionIndices[unit->starting_position_index()] = 1;
|
||||
break;
|
||||
}
|
||||
@@ -101,6 +281,87 @@ auto UnitIdsRequiringWaterCrossing(
|
||||
return unitIdsToReturn;
|
||||
}
|
||||
|
||||
auto HasEnoughNonUndeadAttackersWithoutWaterCrossing(
|
||||
const GameStateW &gameState,
|
||||
const CoordsSet &destinations,
|
||||
const LandRegions &landRegions,
|
||||
const APDCache &apdCache,
|
||||
const BattalionTypeGetter &battalionTypeGetter,
|
||||
const std::size_t requiredUnitCount) -> bool {
|
||||
if (requiredUnitCount == 0) return true;
|
||||
|
||||
const GameState *state = gameState.Get();
|
||||
const auto *units = state->units();
|
||||
const HexMap *baseMap = state->hex_map();
|
||||
const EffectiveLandRegions effectiveLandRegions(landRegions, baseMap);
|
||||
const EffectiveDestinations effectiveDestinations(effectiveLandRegions, destinations);
|
||||
|
||||
std::size_t eligibleUnitCount = 0;
|
||||
std::size_t reachableUnitCount = 0;
|
||||
for (const auto *unit : *units) {
|
||||
if (!IsActiveNonUndeadAttacker(state, unit)) continue;
|
||||
|
||||
++eligibleUnitCount;
|
||||
if (DefinitelyReachesAllWithoutCreatingCrossing(baseMap, unit, effectiveDestinations) &&
|
||||
++reachableUnitCount >= requiredUnitCount) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (eligibleUnitCount < requiredUnitCount) return false;
|
||||
|
||||
const HexMap *map = baseMap;
|
||||
MapId mapId = ActionPointDistancesCache::GetMapId(baseMap);
|
||||
fb::HexMapW fireClearedMap;
|
||||
bool mapPreparedForDistances = false;
|
||||
std::array<const ActionPointDistances *, BattalionTypeId::BattalionTypeId_MAX + 1>
|
||||
distancesByBattalionType{};
|
||||
const auto distancesFor = [&](const BattalionTypeSPtr &battalionType) {
|
||||
if (!mapPreparedForDistances) {
|
||||
mapPreparedForDistances = true;
|
||||
|
||||
if (HasNonWaterFire(map)) {
|
||||
fireClearedMap = fb::CopyHexMap(map);
|
||||
for (uint32_t index = 0; index < fireClearedMap->terrain()->size(); ++index) {
|
||||
if (IsWater(fireClearedMap->terrain()->Get(index)->type())) continue;
|
||||
// const_cast is safe because we own the mutable buffer (fireClearedMap)
|
||||
const_cast<net::eagle0::shardok::storage::fb::Terrain *>(
|
||||
fireClearedMap->mutable_terrain()->GetMutableObject(index))
|
||||
->mutable_modifier()
|
||||
.mutable_fire()
|
||||
.mutate_present(false);
|
||||
}
|
||||
|
||||
map = fireClearedMap.Get();
|
||||
mapId = RecomputeModifierHash(fireClearedMap.Get());
|
||||
}
|
||||
}
|
||||
|
||||
auto &distances = distancesByBattalionType[battalionType->typeId];
|
||||
if (distances == nullptr) {
|
||||
distances = apdCache->GetRaw(map, mapId, battalionType, false);
|
||||
}
|
||||
return distances;
|
||||
};
|
||||
|
||||
for (const auto *unit : *units) {
|
||||
if (!IsActiveNonUndeadAttacker(state, unit)) continue;
|
||||
if (DefinitelyReachesAllWithoutCreatingCrossing(baseMap, unit, effectiveDestinations)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto &battalionType = battalionTypeGetter(unit->battalion().type());
|
||||
if (StaticRegionsAreConclusiveFor(*battalionType)) continue;
|
||||
|
||||
if (CanReachAllWithoutWater(map, unit, destinations, distancesFor(battalionType)) &&
|
||||
++reachableUnitCount >= requiredUnitCount) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
auto UnitIdsToCreateWaterCrossing(
|
||||
const GameStateW &gameState,
|
||||
const PlayerId pid,
|
||||
@@ -128,10 +389,10 @@ auto CanReach(
|
||||
const MapId &mapId,
|
||||
const APDCache &apdCache,
|
||||
const BattalionTypeSPtr &battalionType) -> bool {
|
||||
const DIST_T startingDistance =
|
||||
apdCache->GetRaw(hexMap, mapId, battalionType, false)->Distance(origin, destination);
|
||||
|
||||
return startingDistance != ActionPointDistances::IMPOSSIBLE;
|
||||
return CanReachWithDistances(
|
||||
origin,
|
||||
destination,
|
||||
apdCache->GetRaw(hexMap, mapId, battalionType, false));
|
||||
}
|
||||
|
||||
// Returns the *non*-water tiles from which you could bridge/freeze water to allow a unit to cross
|
||||
@@ -193,7 +454,7 @@ auto WaterCrossingTiles(
|
||||
terr->mutable_modifier().mutable_bridge().mutate_integrity(100.0);
|
||||
terr->mutable_modifier().mutable_fire().mutate_present(false);
|
||||
|
||||
auto hash = ActionPointDistancesCache::GetMapId(mapCopy);
|
||||
const MapId hash = RecomputeModifierHash(mapCopy.Get());
|
||||
|
||||
if (const auto *distances = apdCache->GetRaw(mapCopy, hash, battalionType, false);
|
||||
distances->Distance(origin, destination) != ActionPointDistances::IMPOSSIBLE) {
|
||||
@@ -286,6 +547,7 @@ auto WaterCrossingScore(
|
||||
const GameStateW &gameState,
|
||||
const CoordsSet &castleCoords,
|
||||
const CoordsSet &startCrossingFrom,
|
||||
const LandRegions &landRegions,
|
||||
const APDCache &apdCache) -> double {
|
||||
uint32_t castleClaimCount = 0;
|
||||
for (const auto *unit : *gameState->units()) {
|
||||
@@ -315,6 +577,7 @@ auto WaterCrossingScore(
|
||||
gameState,
|
||||
playerId,
|
||||
castleCoords,
|
||||
landRegions,
|
||||
apdCache,
|
||||
battalionTypeGetter);
|
||||
if (unitIdsRequiringCrossing.empty()) return kNoRequiredCrossingScore;
|
||||
|
||||
@@ -5,10 +5,13 @@
|
||||
#ifndef EAGLE0_SHARDOK_AI_AI_WATER_CROSSING_CALCULATOR_HPP
|
||||
#define EAGLE0_SHARDOK_AI_AI_WATER_CROSSING_CALCULATOR_HPP
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AICommonTypes.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistancesCache.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/LandRegions.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
|
||||
|
||||
@@ -35,9 +38,20 @@ static inline void AssertValid(const Coords& c, const HexMap* hexMap) {
|
||||
const GameStateW& gameState,
|
||||
PlayerId pid,
|
||||
const CoordsSet& destinations,
|
||||
const LandRegions& landRegions,
|
||||
const APDCache& apdCache,
|
||||
const BattalionTypeGetter& battalionTypeGetter) -> vector<UnitId>;
|
||||
|
||||
// Whether at least requiredUnitCount active, non-undead attacker units can reach every destination
|
||||
// without crossing water.
|
||||
[[nodiscard]] auto HasEnoughNonUndeadAttackersWithoutWaterCrossing(
|
||||
const GameStateW& gameState,
|
||||
const CoordsSet& destinations,
|
||||
const LandRegions& landRegions,
|
||||
const APDCache& apdCache,
|
||||
const BattalionTypeGetter& battalionTypeGetter,
|
||||
std::size_t requiredUnitCount) -> bool;
|
||||
|
||||
// Units belonging to the player that are capable of creating water crossings
|
||||
[[nodiscard]] auto UnitIdsToCreateWaterCrossing(
|
||||
const GameStateW& gameState,
|
||||
@@ -92,6 +106,7 @@ static inline void AssertValid(const Coords& c, const HexMap* hexMap) {
|
||||
const GameStateW& gameState,
|
||||
const CoordsSet& castleCoords,
|
||||
const CoordsSet& startCrossingFrom,
|
||||
const LandRegions& landRegions,
|
||||
const APDCache& apdCache) -> double;
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
@@ -44,6 +44,7 @@ auto AIWaterCrossingCommandChooser::StartCrossingFrom(
|
||||
gameState,
|
||||
playerId,
|
||||
castleCoords,
|
||||
landRegions,
|
||||
apdCache,
|
||||
battalionTypeGetter);
|
||||
if (unitIdsRequiringCrossing.empty()) return startCrossingFrom;
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistancesCache.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/LandRegions.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
|
||||
|
||||
@@ -23,11 +24,16 @@ class AIWaterCrossingCommandChooser {
|
||||
private:
|
||||
const PlayerId playerId;
|
||||
const APDCache apdCache;
|
||||
const LandRegions &landRegions;
|
||||
|
||||
public:
|
||||
AIWaterCrossingCommandChooser(const PlayerId pid, APDCache apdCache)
|
||||
AIWaterCrossingCommandChooser(
|
||||
const PlayerId pid,
|
||||
APDCache apdCache,
|
||||
const LandRegions &landRegions)
|
||||
: playerId(pid),
|
||||
apdCache(std::move(apdCache)) {}
|
||||
apdCache(std::move(apdCache)),
|
||||
landRegions(landRegions) {}
|
||||
|
||||
[[nodiscard]] auto StartCrossingFrom(
|
||||
const BattalionTypeGetter &battalionTypeGetter,
|
||||
|
||||
@@ -73,6 +73,7 @@ cc_library(
|
||||
],
|
||||
deps = [
|
||||
":ai_score_utilities",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/map:land_regions",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/map:terrain",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/settings:game_settings",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/util:hex_map_utils",
|
||||
@@ -194,6 +195,16 @@ cc_library(
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "ai_thread_pool",
|
||||
srcs = ["AIThreadPool.cpp"],
|
||||
hdrs = ["AIThreadPool.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = [
|
||||
"//src/test/cpp/net/eagle0/shardok/ai:__pkg__",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "ai_command_evaluator",
|
||||
srcs = ["AICommandEvaluator.cpp"],
|
||||
@@ -207,6 +218,7 @@ cc_library(
|
||||
":ai_command_filter",
|
||||
":ai_experiment_config",
|
||||
":ai_strategy",
|
||||
":ai_thread_pool",
|
||||
":transposition_table",
|
||||
"//src/main/cpp/net/eagle0/common:sequence_random_generator",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai/score:ai_score_calculator_interface",
|
||||
@@ -306,6 +318,7 @@ cc_library(
|
||||
"//src/main/cpp/net/eagle0/shardok/library/action_point_distances",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/action_point_distances:action_point_distances_cache",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/fb_helpers:hex_map_helpers",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/map:land_regions",
|
||||
"//src/main/flatbuffer/net/eagle0/shardok/storage:game_state_cc_fbs",
|
||||
"//src/main/flatbuffer/net/eagle0/shardok/storage:hex_map_cc_fbs",
|
||||
],
|
||||
@@ -323,6 +336,7 @@ cc_library(
|
||||
"//src/main/cpp/net/eagle0/shardok/library:game_state_w",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/action_point_distances",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/action_point_distances:action_point_distances_cache",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/map:land_regions",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -292,8 +292,6 @@ auto IterativeDeepeningAI::IterativeSearch(
|
||||
<< " < 100%\n";
|
||||
}
|
||||
|
||||
// Print TranspositionTable statistics
|
||||
g_transpositionTable.printStats();
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ ShardokAIClient::ShardokAIClient(
|
||||
aiAlgorithmType(aiAlgorithmType),
|
||||
scoringCalculatorType(scoringCalculatorType),
|
||||
alCache(std::make_unique<AttackLocationsCache>(hexMap, settings)),
|
||||
waterCrossingCommandChooser(playerId, apdCache),
|
||||
waterCrossingCommandChooser(playerId, apdCache, alCache->GetLandRegions()),
|
||||
mctsConfig(mctsConfig) {
|
||||
// Pre-generate the most common cache entries for better performance
|
||||
const auto mapId = ActionPointDistancesCache::GetMapId(hexMap);
|
||||
@@ -302,6 +302,7 @@ auto ShardokAIClient::StandardChooseCommandIndex(
|
||||
castleCoords,
|
||||
maxRounds,
|
||||
apdCache,
|
||||
alCache->GetLandRegions(),
|
||||
battalionTypeGetter)
|
||||
: AIAttackerStrategySelector::BestAttackerStrategy(
|
||||
playerId,
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
|
||||
#include "AbstractAIScoreCalculator.hpp"
|
||||
|
||||
#include <array>
|
||||
|
||||
#include "AIScoreCalculatorSharedUtilities.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIScoreUtilities.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIStrategy.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIUnitScoreCalculator.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIWaterCrossingCalculator.hpp"
|
||||
@@ -41,16 +42,44 @@ auto AbstractAIScoreCalculator::CalculateUnitsScoreComponents(
|
||||
|
||||
bool isLateGame = cachedCurrentRound > 18; // Inline IsLateGame for efficiency
|
||||
|
||||
// APDCache now has built-in thread-local caching - no need for PreCachedAPDs
|
||||
ActionPoints braveWaterCost = GetBraveWaterCost();
|
||||
|
||||
// Memoization cache for EffectiveDistance calls
|
||||
EffectiveDistanceCache distanceCache;
|
||||
|
||||
// Early return for empty game states
|
||||
const size_t estimatedUnitCount = cachedUnits->size();
|
||||
if (estimatedUnitCount == 0) { return UnitsScoreComponents{0.0, 0.0}; }
|
||||
|
||||
ActionPoints braveWaterCost = GetBraveWaterCost();
|
||||
|
||||
struct ResolvedActionPointDistances {
|
||||
const ActionPointDistances *notBraving = nullptr;
|
||||
const ActionPointDistances *braving = nullptr;
|
||||
};
|
||||
std::array<ResolvedActionPointDistances, BattalionTypeId::BattalionTypeId_MAX + 1>
|
||||
resolvedActionPointDistances{};
|
||||
|
||||
// A scoring call repeatedly asks for the same map/type APDs. Resolve each pointer once while
|
||||
// leaving ownership and cross-call caching with ActionPointDistancesCache.
|
||||
const auto notBravingApdFor = [&](int battalionTypeId, const BattalionTypeSPtr &battalionType) {
|
||||
auto &resolved = resolvedActionPointDistances[battalionTypeId];
|
||||
if (resolved.notBraving == nullptr) {
|
||||
resolved.notBraving = GetApdCache()->GetRaw(cachedHexMap, mapId, battalionType, false);
|
||||
}
|
||||
return resolved.notBraving;
|
||||
};
|
||||
const auto bravingApdFor = [&](int battalionTypeId, const BattalionTypeSPtr &battalionType) {
|
||||
if (!battalionType->allowsBraveWater) {
|
||||
return static_cast<const ActionPointDistances *>(nullptr);
|
||||
}
|
||||
|
||||
auto &resolved = resolvedActionPointDistances[battalionTypeId];
|
||||
if (resolved.braving == nullptr) {
|
||||
resolved.braving =
|
||||
GetApdCache()->GetRaw(cachedHexMap, mapId, battalionType, true, braveWaterCost);
|
||||
}
|
||||
return resolved.braving;
|
||||
};
|
||||
|
||||
// Memoization cache for EffectiveDistance calls
|
||||
EffectiveDistanceCache distanceCache;
|
||||
|
||||
std::vector<const Unit *> attackerUnits{};
|
||||
std::vector<const Unit *> defenderUnits{};
|
||||
// Pre-allocate vectors based on estimated unit ratios to avoid reallocations
|
||||
@@ -62,13 +91,24 @@ auto AbstractAIScoreCalculator::CalculateUnitsScoreComponents(
|
||||
|
||||
auto occupants = Occupants(*cachedUnits, cachedRowCount, cachedColumnCount);
|
||||
|
||||
// StartGame assigns real player IDs densely from zero, matching their player_infos indexes.
|
||||
// Use that invariant instead of scanning PlayerInfo for every unit or initializing a separate
|
||||
// lookup table for every evaluated state.
|
||||
const auto *playerInfos = gameStateRawPtr->player_infos();
|
||||
if (playerInfos == nullptr) { return UnitsScoreComponents{0.0, 0.0}; }
|
||||
|
||||
for (const Unit *unit : *cachedUnits) {
|
||||
const auto *pi = PlayerInfoForPid(gameState, unit->player_id());
|
||||
if (pi == nullptr) { continue; }
|
||||
const PlayerId playerId = unit->player_id();
|
||||
if (playerId < 0 || static_cast<flatbuffers::uoffset_t>(playerId) >= playerInfos->size()) {
|
||||
continue;
|
||||
}
|
||||
const auto *playerInfo = playerInfos->Get(static_cast<flatbuffers::uoffset_t>(playerId));
|
||||
if (playerInfo->player_id() != playerId) { continue; }
|
||||
const bool isDefender = playerInfo->is_defender();
|
||||
|
||||
switch (unit->status()) {
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT: {
|
||||
if (pi->is_defender()) {
|
||||
if (isDefender) {
|
||||
defenderUnits.push_back(unit);
|
||||
} else {
|
||||
attackerUnits.push_back(unit);
|
||||
@@ -79,7 +119,7 @@ auto AbstractAIScoreCalculator::CalculateUnitsScoreComponents(
|
||||
double thisScore = unit->has_attached_hero() && unit->attached_hero().is_vip()
|
||||
? CAPTURED_VIP_SCORE
|
||||
: CAPTURED_UNIT_SCORE;
|
||||
if (pi->is_defender()) {
|
||||
if (isDefender) {
|
||||
defenderUnitsValue += thisScore;
|
||||
} else {
|
||||
attackerUnitsValue += thisScore;
|
||||
@@ -115,16 +155,8 @@ auto AbstractAIScoreCalculator::CalculateUnitsScoreComponents(
|
||||
// Cache battalion type reference to avoid shared_ptr atomic operations
|
||||
const auto &battalionType = GetBattalionType(static_cast<BattalionTypeId>(battTypeId));
|
||||
|
||||
// Cache APD lookups - same battalion type is used multiple times below
|
||||
const auto *notBravingApd =
|
||||
GetApdCache()->GetRaw(cachedHexMap, mapId, battalionType, false);
|
||||
const auto *bravingApd = battalionType->allowsBraveWater ? GetApdCache()->GetRaw(
|
||||
cachedHexMap,
|
||||
mapId,
|
||||
battalionType,
|
||||
true,
|
||||
braveWaterCost)
|
||||
: nullptr;
|
||||
const auto *notBravingApd = notBravingApdFor(battTypeId, battalionType);
|
||||
const auto *bravingApd = bravingApdFor(battTypeId, battalionType);
|
||||
|
||||
const auto &priorityList = std::ranges::find_if(
|
||||
attackerTargetPriorities,
|
||||
@@ -174,9 +206,7 @@ auto AbstractAIScoreCalculator::CalculateUnitsScoreComponents(
|
||||
// Cache battalion type reference to avoid shared_ptr atomic operations
|
||||
const auto &battalionType = GetBattalionType(static_cast<BattalionTypeId>(battTypeId));
|
||||
|
||||
// Cache APD lookups for this defender unit
|
||||
const auto *defenderNotBravingApd =
|
||||
GetApdCache()->GetRaw(cachedHexMap, mapId, battalionType, false);
|
||||
const auto *defenderNotBravingApd = notBravingApdFor(battTypeId, battalionType);
|
||||
|
||||
auto dv = UnitValue(
|
||||
unit,
|
||||
@@ -211,14 +241,8 @@ auto AbstractAIScoreCalculator::CalculateUnitsScoreComponents(
|
||||
const DIST_T thisDistance = distanceCache.GetOrCompute(
|
||||
attackerUnit,
|
||||
unit->location(),
|
||||
GetApdCache()->GetRaw(cachedHexMap, mapId, attackerBattalionType, false),
|
||||
attackerBattalionType->allowsBraveWater ? GetApdCache()->GetRaw(
|
||||
cachedHexMap,
|
||||
mapId,
|
||||
attackerBattalionType,
|
||||
true,
|
||||
braveWaterCost)
|
||||
: nullptr,
|
||||
notBravingApdFor(attackerBattTypeId, attackerBattalionType),
|
||||
bravingApdFor(attackerBattTypeId, attackerBattalionType),
|
||||
cachedHexMap);
|
||||
if (thisDistance < closestDistanceToEnemy) {
|
||||
closestDistanceToEnemy = thisDistance;
|
||||
@@ -241,19 +265,8 @@ auto AbstractAIScoreCalculator::CalculateUnitsScoreComponents(
|
||||
const DIST_T thisDistance = distanceCache.GetOrCompute(
|
||||
defenderUnit,
|
||||
unit->location(),
|
||||
GetApdCache()->GetRaw(
|
||||
cachedHexMap,
|
||||
mapId,
|
||||
defenderBattalionType,
|
||||
false),
|
||||
defenderBattalionType->allowsBraveWater
|
||||
? GetApdCache()->GetRaw(
|
||||
cachedHexMap,
|
||||
mapId,
|
||||
defenderBattalionType,
|
||||
true,
|
||||
braveWaterCost)
|
||||
: nullptr,
|
||||
notBravingApdFor(defenderBattTypeId, defenderBattalionType),
|
||||
bravingApdFor(defenderBattTypeId, defenderBattalionType),
|
||||
cachedHexMap);
|
||||
if (thisDistance < closestDistanceToFriendly) {
|
||||
closestDistanceToFriendly = thisDistance;
|
||||
@@ -306,6 +319,7 @@ auto AbstractAIScoreCalculator::CalculateAttackerVictoryConditionScore(
|
||||
gameState,
|
||||
castleCoords,
|
||||
attackerStrategy.targetLocations,
|
||||
GetAlCache()->GetLandRegions(),
|
||||
GetApdCache());
|
||||
break;
|
||||
|
||||
|
||||
@@ -25,6 +25,32 @@
|
||||
#include "src/main/protobuf/net/eagle0/shardok/api/game_state_view.pb.h"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
auto PlayerBattleProgressFrom(const net::eagle0::shardok::storage::fb::GameState &gameState)
|
||||
-> std::vector<PlayerBattleProgress> {
|
||||
std::vector<PlayerBattleProgress> progress;
|
||||
progress.reserve(gameState.player_infos()->size());
|
||||
for (const auto *player : *gameState.player_infos()) {
|
||||
int32_t troopCount = 0;
|
||||
bool isRetreating = false;
|
||||
for (const auto *unit : *gameState.units()) {
|
||||
if (unit->player_id() != player->player_id()) continue;
|
||||
switch (unit->status()) {
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_NEVER_ENTERED_UNIT:
|
||||
troopCount += unit->battalion().size();
|
||||
break;
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_FLED_UNIT:
|
||||
isRetreating = true;
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
progress.push_back(PlayerBattleProgress{player->player_id(), troopCount, isRetreating});
|
||||
}
|
||||
return progress;
|
||||
}
|
||||
using net::eagle0::common::ScopedShardokLatencyTrace;
|
||||
|
||||
using net::eagle0::shardok::common::GameStatus;
|
||||
@@ -588,22 +614,7 @@ auto ShardokGameController::GetUpdates(const int64_t startingActionId) -> AllUpd
|
||||
// Extract battle progress data from the FlatBuffer game state
|
||||
const auto *gs = engine->GetCurrentGameState().Get();
|
||||
updates.currentRound = gs->current_round();
|
||||
updates.playerTroopCounts.reserve(gs->player_infos()->size());
|
||||
for (const auto *player : *gs->player_infos()) {
|
||||
int32_t troopCount = 0;
|
||||
for (const auto *unit : *gs->units()) {
|
||||
if (unit->player_id() != player->player_id()) continue;
|
||||
switch (unit->status()) {
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_NEVER_ENTERED_UNIT:
|
||||
troopCount += unit->battalion().size();
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
updates.playerTroopCounts.emplace_back(player->player_id(), troopCount);
|
||||
}
|
||||
updates.playerBattleProgress = PlayerBattleProgressFrom(*gs);
|
||||
}
|
||||
|
||||
if (recommendationEngine) {
|
||||
@@ -724,7 +735,7 @@ auto ShardokGameController::WaitForUpdatesAndPush(
|
||||
updates.newUnfilteredCount,
|
||||
updates.currentGameState,
|
||||
updates.currentRound,
|
||||
updates.playerTroopCounts);
|
||||
updates.playerBattleProgress);
|
||||
}
|
||||
|
||||
// Tutorial battle reset: defender lost, signal reset instead of game over
|
||||
|
||||
@@ -59,6 +59,16 @@ struct OnePlayerUpdates {
|
||||
availableCommands(acs) {}
|
||||
};
|
||||
|
||||
struct PlayerBattleProgress {
|
||||
int32_t playerId;
|
||||
int32_t troopCount;
|
||||
bool isRetreating;
|
||||
};
|
||||
|
||||
[[nodiscard]] auto PlayerBattleProgressFrom(
|
||||
const net::eagle0::shardok::storage::fb::GameState& gameState)
|
||||
-> std::vector<PlayerBattleProgress>;
|
||||
|
||||
/// Result of WaitForUpdatesAndPush indicating how the wait ended
|
||||
enum class WaitResult {
|
||||
GAME_OVER, // Game ended normally
|
||||
@@ -78,7 +88,7 @@ public:
|
||||
int32_t newUnfilteredCount,
|
||||
const byte_vector& currentGameState,
|
||||
int32_t currentRound,
|
||||
const std::vector<std::pair<int32_t, int32_t>>& playerTroopCounts) = 0;
|
||||
const std::vector<PlayerBattleProgress>& playerBattleProgress) = 0;
|
||||
|
||||
/// Called when the game ends
|
||||
virtual void OnGameOver(const GameOverInfo& info) = 0;
|
||||
@@ -189,7 +199,7 @@ public:
|
||||
int32_t newUnfilteredCount;
|
||||
byte_vector currentGameState;
|
||||
int32_t currentRound = 0;
|
||||
std::vector<std::pair<int32_t, int32_t>> playerTroopCounts; // (playerId, troopCount)
|
||||
std::vector<PlayerBattleProgress> playerBattleProgress;
|
||||
};
|
||||
auto GetUpdates(int64_t startingActionId) -> AllUpdates;
|
||||
auto GetCurrentGameStateBytes() -> byte_vector;
|
||||
|
||||
@@ -52,11 +52,22 @@ private:
|
||||
const std::vector<std::shared_ptr<const CommandFactory>> commandFactories;
|
||||
const PlayerSetupCommandFactory playerSetupCommandFactory;
|
||||
|
||||
struct PlayerCommandContext {
|
||||
std::vector<PlayerId> allyPids;
|
||||
PossibleChargees possibleChargees;
|
||||
bool isAttacker;
|
||||
bool cannotBecomeOutlaw;
|
||||
};
|
||||
|
||||
static auto MakePlayerCommandContext(const GameStateW &gameState, PlayerId playerId)
|
||||
-> PlayerCommandContext;
|
||||
|
||||
// AvailableCommands helpers for a single unit
|
||||
void AddAvailableCommandsForOneUnit(
|
||||
const GameStateW &gameState,
|
||||
CommandList &commands,
|
||||
const Unit *unit,
|
||||
const PlayerCommandContext &playerContext,
|
||||
bool onlyFollowUps) const;
|
||||
|
||||
public:
|
||||
@@ -95,30 +106,39 @@ auto AvailableCommandsFactoryImpl::GetPlayerSetupCommands(
|
||||
gameState,
|
||||
reinforcementPositions);
|
||||
|
||||
return std::make_shared<CommandList>(previewCommands);
|
||||
return std::make_shared<CommandList>(std::move(previewCommands));
|
||||
}
|
||||
|
||||
auto AvailableCommandsFactoryImpl::MakePlayerCommandContext(
|
||||
const GameStateW &gameState,
|
||||
const PlayerId playerId) -> PlayerCommandContext {
|
||||
PossibleChargees possibleChargees{};
|
||||
possibleChargees.reserve(gameState->possible_chargee_ids()->size());
|
||||
for (const UnitId chargeeId : *gameState->possible_chargee_ids()) {
|
||||
if (chargeeId != -1) { possibleChargees.push_back(chargeeId); }
|
||||
}
|
||||
|
||||
return PlayerCommandContext{
|
||||
.allyPids = AlliedPids(gameState, playerId),
|
||||
.possibleChargees = std::move(possibleChargees),
|
||||
.isAttacker = IsAttacker(gameState, playerId),
|
||||
.cannotBecomeOutlaw = CannotBecomeOutlaw(gameState, playerId)};
|
||||
}
|
||||
|
||||
void AvailableCommandsFactoryImpl::AddAvailableCommandsForOneUnit(
|
||||
const GameStateW &gameState,
|
||||
CommandList &commands,
|
||||
const Unit *unit,
|
||||
const PlayerCommandContext &playerContext,
|
||||
const bool onlyFollowUps) const {
|
||||
const auto &battType = settings.GetBattalionType(unit->battalion().type());
|
||||
const bool hasHero = unit->has_attached_hero();
|
||||
const auto allyPids = AlliedPids(gameState, unit->player_id());
|
||||
const bool isAttacker = IsAttacker(gameState, unit->player_id());
|
||||
const bool cannotBecomeOutlaw = CannotBecomeOutlaw(gameState, unit->player_id());
|
||||
|
||||
const bool unitMovedIntoZoc = unit->has_moved_in_zoc();
|
||||
const ActionPoints remainingActionPoints = unit->remaining_action_points();
|
||||
const Coords ¤tCoords = unit->location();
|
||||
|
||||
const bool isEligibleCharger = unit->unit_id() == gameState->eligible_charger_id();
|
||||
std::vector<UnitId> possibleChargees{};
|
||||
possibleChargees.reserve(gameState->possible_chargee_ids()->size());
|
||||
for (const UnitId cid : *gameState->possible_chargee_ids()) {
|
||||
if (cid != -1) { possibleChargees.push_back(cid); }
|
||||
}
|
||||
|
||||
// Undead units cannot act until the commanding hero has controlled them
|
||||
if (unit->commanding_unit_id() != -1) {
|
||||
@@ -140,12 +160,12 @@ void AvailableCommandsFactoryImpl::AddAvailableCommandsForOneUnit(
|
||||
.position = currentCoords,
|
||||
.weatherFb = gameState->weather(),
|
||||
.isEligibleCharger = isEligibleCharger,
|
||||
.possibleChargees = possibleChargees,
|
||||
.possibleChargees = playerContext.possibleChargees,
|
||||
.hexMap = gameState->hex_map(),
|
||||
.units = gameState->units(),
|
||||
.allyPids = allyPids,
|
||||
.isAttacker = isAttacker,
|
||||
.cannotBecomeOutlaw = cannotBecomeOutlaw,
|
||||
.allyPids = playerContext.allyPids,
|
||||
.isAttacker = playerContext.isAttacker,
|
||||
.cannotBecomeOutlaw = playerContext.cannotBecomeOutlaw,
|
||||
.unitMovedIntoZoc = unitMovedIntoZoc};
|
||||
|
||||
for (const auto &commandFactory : commandFactories) {
|
||||
@@ -184,7 +204,10 @@ void AvailableCommandsFactoryImpl::AddAvailableCommandsForOneUnit(
|
||||
});
|
||||
}
|
||||
|
||||
commands.insert(commands.end(), oneUnitCommands.begin(), oneUnitCommands.end());
|
||||
commands.insert(
|
||||
commands.end(),
|
||||
std::make_move_iterator(oneUnitCommands.begin()),
|
||||
std::make_move_iterator(oneUnitCommands.end()));
|
||||
}
|
||||
|
||||
auto AvailableCommandsFactoryImpl::GetAvailableCommands(
|
||||
@@ -196,7 +219,9 @@ auto AvailableCommandsFactoryImpl::GetAvailableCommands(
|
||||
|
||||
playerSetupCommandFactory
|
||||
.AddAvailablePlayerSetupCommands(commands, playerId, gameState, reinforcementPositions);
|
||||
if (!commands.empty()) return std::make_shared<CommandList>(commands);
|
||||
if (!commands.empty()) return std::make_shared<CommandList>(std::move(commands));
|
||||
|
||||
const auto playerContext = MakePlayerCommandContext(gameState, playerId);
|
||||
|
||||
for (const Unit *unit : *gameState->units()) {
|
||||
if (unit->status() != net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) continue;
|
||||
@@ -205,6 +230,7 @@ auto AvailableCommandsFactoryImpl::GetAvailableCommands(
|
||||
gameState,
|
||||
commands,
|
||||
unit,
|
||||
playerContext,
|
||||
/* onlyFollowUps=*/false);
|
||||
}
|
||||
|
||||
@@ -229,10 +255,13 @@ auto AvailableCommandsFactoryImpl::GetAvailableCommands(
|
||||
auto after = ApplyResults(copy, results, settings);
|
||||
|
||||
CommandList followUpCommands{};
|
||||
const auto followUpPlayerContext =
|
||||
MakePlayerCommandContext(after, after->units()->Get(actor)->player_id());
|
||||
AddAvailableCommandsForOneUnit(
|
||||
after,
|
||||
followUpCommands,
|
||||
after->units()->Get(actor),
|
||||
followUpPlayerContext,
|
||||
/* onlyFollowUps=*/true);
|
||||
|
||||
std::unordered_set<CommandType> followUpTypes{};
|
||||
@@ -249,7 +278,7 @@ auto AvailableCommandsFactoryImpl::GetAvailableCommands(
|
||||
}
|
||||
|
||||
if (commands.empty()) return nullptr;
|
||||
return std::make_shared<CommandList>(commands);
|
||||
return std::make_shared<CommandList>(std::move(commands));
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
@@ -57,7 +57,7 @@ auto shardok::MoveCommand::InternalExecute(
|
||||
const auto* destinationTerrain = GetTerrain(map, destination);
|
||||
bool startedInEnemyZoc = enemyZocCoords.Contains(origin);
|
||||
vector<const Unit*> knownAdjacentEnemies =
|
||||
KnownAdjacentEnemies(player, allUnits, map, allyPids, origin);
|
||||
KnownAdjacentEnemies(player, currentState, allyPids, origin);
|
||||
|
||||
MutatingSpendActionPoints(
|
||||
&mover,
|
||||
@@ -132,7 +132,7 @@ auto shardok::MoveCommand::InternalExecute(
|
||||
|
||||
if (!startedInEnemyZoc) {
|
||||
vector<const Unit*> newAdjacentEnemies =
|
||||
KnownAdjacentEnemies(player, allUnits, map, allyPids, destination);
|
||||
KnownAdjacentEnemies(player, currentState, allyPids, destination);
|
||||
|
||||
for (const Unit* possibleChargee : newAdjacentEnemies) {
|
||||
if (std::ranges::find_if(
|
||||
|
||||
@@ -59,6 +59,20 @@ cc_library(
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "land_regions",
|
||||
srcs = ["LandRegions.cpp"],
|
||||
hdrs = ["LandRegions.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
":coordinates",
|
||||
":terrain",
|
||||
"//src/main/flatbuffer/net/eagle0/shardok/storage:hex_map_cc_fbs",
|
||||
"//src/main/flatbuffer/net/eagle0/shardok/storage:terrain_cc_fbs",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "terrain",
|
||||
srcs = ["Terrain.cpp"],
|
||||
|
||||
@@ -190,6 +190,7 @@ public:
|
||||
|
||||
class Iter {
|
||||
public:
|
||||
using iterator_concept = std::forward_iterator_tag;
|
||||
using iterator_category = std::forward_iterator_tag;
|
||||
using value_type = Coords;
|
||||
using difference_type = std::ptrdiff_t;
|
||||
@@ -197,12 +198,14 @@ public:
|
||||
using reference = Coords;
|
||||
|
||||
private:
|
||||
BackingBitset::Iter iterator;
|
||||
BackingBitset::Iter iterator{};
|
||||
|
||||
const CoordsSet& cs;
|
||||
const CoordsSet* cs = nullptr;
|
||||
|
||||
public:
|
||||
Iter(const CoordsSet& cs, BackingBitset::Iter iter) : iterator(iter), cs(cs) {}
|
||||
Iter() = default;
|
||||
|
||||
Iter(const CoordsSet& cs, BackingBitset::Iter iter) : iterator(iter), cs(&cs) {}
|
||||
|
||||
[[nodiscard]] auto operator==(const Iter& rhs) const -> bool {
|
||||
return iterator == rhs.iterator;
|
||||
@@ -224,8 +227,8 @@ public:
|
||||
[[nodiscard]] auto operator*() const -> Coords {
|
||||
const size_t index = *iterator;
|
||||
|
||||
const int row = static_cast<int>(index / static_cast<size_t>(cs.columnCount));
|
||||
const int column = static_cast<int>(index % static_cast<size_t>(cs.columnCount));
|
||||
const int row = static_cast<int>(index / static_cast<size_t>(cs->columnCount));
|
||||
const int column = static_cast<int>(index % static_cast<size_t>(cs->columnCount));
|
||||
return Coords(row, column);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
//
|
||||
// Copyright 2026 Dan Crosby
|
||||
//
|
||||
|
||||
#include "LandRegions.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <limits>
|
||||
#include <stdexcept>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/Terrain.hpp"
|
||||
|
||||
namespace shardok {
|
||||
namespace {
|
||||
|
||||
using TerrainType = net::eagle0::shardok::storage::fb::Terrain_::Type;
|
||||
|
||||
constexpr std::array<std::array<int, 2>, 6> kEvenRowNeighborOffsets = {
|
||||
std::array{-1, 0},
|
||||
std::array{0, 1},
|
||||
std::array{1, 0},
|
||||
std::array{1, -1},
|
||||
std::array{0, -1},
|
||||
std::array{-1, -1}};
|
||||
constexpr std::array<std::array<int, 2>, 6> kOddRowNeighborOffsets = {
|
||||
std::array{-1, 1},
|
||||
std::array{0, 1},
|
||||
std::array{1, 1},
|
||||
std::array{1, 0},
|
||||
std::array{0, -1},
|
||||
std::array{-1, 0}};
|
||||
|
||||
auto IsGuaranteedLand(const TerrainType type) -> bool {
|
||||
return !IsWater(type) && type != TerrainType::Type_MOUNTAIN;
|
||||
}
|
||||
|
||||
auto ToIndex(const int row, const int column, const int columnCount) -> int {
|
||||
return row * columnCount + column;
|
||||
}
|
||||
|
||||
auto CountRegions(const std::span<const LandRegionId> regions) -> int {
|
||||
int count = 0;
|
||||
for (const LandRegionId region : regions) {
|
||||
if (region != kNoLandRegion) { count = std::max(count, static_cast<int>(region) + 1); }
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
auto HasUsableBridge(const net::eagle0::shardok::storage::fb::Terrain& terrain) -> bool {
|
||||
const auto& bridge = terrain.modifier().bridge();
|
||||
return bridge.present() && bridge.integrity() > 0.0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto ComputeLandRegions(
|
||||
const int rowCount,
|
||||
const int columnCount,
|
||||
const std::span<const net::eagle0::shardok::storage::fb::Terrain> terrain)
|
||||
-> std::vector<LandRegionId> {
|
||||
const int tileCount = rowCount * columnCount;
|
||||
if (tileCount < 0 || terrain.size() != static_cast<size_t>(tileCount)) {
|
||||
throw std::invalid_argument("Land region terrain size does not match map dimensions");
|
||||
}
|
||||
|
||||
std::vector<LandRegionId> regions(static_cast<size_t>(tileCount), kNoLandRegion);
|
||||
std::vector<int> pending;
|
||||
pending.reserve(static_cast<size_t>(tileCount));
|
||||
|
||||
int nextRegion = 0;
|
||||
for (int startIndex = 0; startIndex < tileCount; ++startIndex) {
|
||||
if (regions[startIndex] != kNoLandRegion || !IsGuaranteedLand(terrain[startIndex].type())) {
|
||||
continue;
|
||||
}
|
||||
if (nextRegion > std::numeric_limits<LandRegionId>::max()) {
|
||||
throw std::overflow_error("Map has too many distinct land regions");
|
||||
}
|
||||
|
||||
regions[startIndex] = static_cast<LandRegionId>(nextRegion);
|
||||
pending.push_back(startIndex);
|
||||
while (!pending.empty()) {
|
||||
const int index = pending.back();
|
||||
pending.pop_back();
|
||||
|
||||
const int row = index / columnCount;
|
||||
const int column = index % columnCount;
|
||||
const auto& offsets = row % 2 == 0 ? kEvenRowNeighborOffsets : kOddRowNeighborOffsets;
|
||||
for (const auto& [rowOffset, columnOffset] : offsets) {
|
||||
const int neighborRow = row + rowOffset;
|
||||
const int neighborColumn = column + columnOffset;
|
||||
if (neighborRow < 0 || neighborRow >= rowCount || neighborColumn < 0 ||
|
||||
neighborColumn >= columnCount) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const int neighborIndex = ToIndex(neighborRow, neighborColumn, columnCount);
|
||||
if (regions[neighborIndex] != kNoLandRegion ||
|
||||
!IsGuaranteedLand(terrain[neighborIndex].type())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
regions[neighborIndex] = static_cast<LandRegionId>(nextRegion);
|
||||
pending.push_back(neighborIndex);
|
||||
}
|
||||
}
|
||||
|
||||
++nextRegion;
|
||||
}
|
||||
|
||||
return regions;
|
||||
}
|
||||
|
||||
LandRegions::LandRegions(const net::eagle0::shardok::storage::fb::HexMap* map)
|
||||
: rowCount(map->row_count()),
|
||||
columnCount(map->column_count()),
|
||||
regionCount(0) {
|
||||
std::vector<net::eagle0::shardok::storage::fb::Terrain> terrain;
|
||||
terrain.reserve(map->terrain()->size());
|
||||
for (const auto* tile : *map->terrain()) { terrain.push_back(*tile); }
|
||||
regions = ComputeLandRegions(rowCount, columnCount, terrain);
|
||||
regionCount = CountRegions(regions);
|
||||
}
|
||||
|
||||
LandRegions::LandRegions(
|
||||
const int rowCount,
|
||||
const int columnCount,
|
||||
const std::span<const net::eagle0::shardok::storage::fb::Terrain> terrain)
|
||||
: rowCount(rowCount),
|
||||
columnCount(columnCount),
|
||||
regionCount(0),
|
||||
regions(ComputeLandRegions(rowCount, columnCount, terrain)) {
|
||||
regionCount = CountRegions(regions);
|
||||
}
|
||||
|
||||
auto LandRegions::RegionAt(const Coords& coords) const -> LandRegionId {
|
||||
if (coords.row() < 0 || coords.column() < 0 || coords.row() >= rowCount ||
|
||||
coords.column() >= columnCount) {
|
||||
return kNoLandRegion;
|
||||
}
|
||||
return regions[ToIndex(coords.row(), coords.column(), columnCount)];
|
||||
}
|
||||
|
||||
auto LandRegions::DefinitelyConnectedWithoutWater(const Coords& origin, const Coords& destination)
|
||||
const -> bool {
|
||||
const auto originRegion = RegionAt(origin);
|
||||
const auto destinationRegion = RegionAt(destination);
|
||||
return originRegion != kNoLandRegion && originRegion == destinationRegion;
|
||||
}
|
||||
|
||||
template<typename TerrainAt>
|
||||
void EffectiveLandRegions::Initialize(
|
||||
const int rowCount,
|
||||
const int columnCount,
|
||||
TerrainAt&& terrainAt) {
|
||||
const int tileCount = rowCount * columnCount;
|
||||
if (rowCount != landRegions.rowCount || columnCount != landRegions.columnCount ||
|
||||
tileCount < 0 || tileCount > static_cast<int>(kMaximumLandRegionTileCount)) {
|
||||
throw std::invalid_argument("Effective land region map dimensions do not match");
|
||||
}
|
||||
|
||||
nodeCount = landRegions.regionCount + tileCount;
|
||||
for (int node = 0; node < nodeCount; ++node) {
|
||||
parents[static_cast<size_t>(node)] = static_cast<int16_t>(node);
|
||||
}
|
||||
|
||||
for (int index = 0; index < tileCount; ++index) {
|
||||
bridgeTiles[static_cast<size_t>(index)] =
|
||||
landRegions.regions[static_cast<size_t>(index)] == kNoLandRegion &&
|
||||
HasUsableBridge(terrainAt(index));
|
||||
}
|
||||
|
||||
for (int index = 0; index < tileCount; ++index) {
|
||||
if (!bridgeTiles[static_cast<size_t>(index)]) continue;
|
||||
|
||||
const int row = index / columnCount;
|
||||
const int column = index % columnCount;
|
||||
const auto& offsets = row % 2 == 0 ? kEvenRowNeighborOffsets : kOddRowNeighborOffsets;
|
||||
const int bridgeNode = landRegions.regionCount + index;
|
||||
for (const auto& [rowOffset, columnOffset] : offsets) {
|
||||
const int neighborRow = row + rowOffset;
|
||||
const int neighborColumn = column + columnOffset;
|
||||
if (neighborRow < 0 || neighborRow >= rowCount || neighborColumn < 0 ||
|
||||
neighborColumn >= columnCount) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const int neighborIndex = ToIndex(neighborRow, neighborColumn, columnCount);
|
||||
const LandRegionId neighborRegion =
|
||||
landRegions.regions[static_cast<size_t>(neighborIndex)];
|
||||
if (neighborRegion != kNoLandRegion) {
|
||||
Union(bridgeNode, neighborRegion);
|
||||
} else if (bridgeTiles[static_cast<size_t>(neighborIndex)]) {
|
||||
Union(bridgeNode, landRegions.regionCount + neighborIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int node = 0; node < nodeCount; ++node) {
|
||||
parents[static_cast<size_t>(node)] = static_cast<int16_t>(Root(node));
|
||||
}
|
||||
}
|
||||
|
||||
EffectiveLandRegions::EffectiveLandRegions(
|
||||
const LandRegions& landRegions,
|
||||
const net::eagle0::shardok::storage::fb::HexMap* map)
|
||||
: landRegions(landRegions),
|
||||
nodeCount(0) {
|
||||
if (map == nullptr) { throw std::invalid_argument("Effective land region map is null"); }
|
||||
Initialize(
|
||||
map->row_count(),
|
||||
map->column_count(),
|
||||
[&](const int index) -> const auto& { return *map->terrain()->Get(index); });
|
||||
}
|
||||
|
||||
EffectiveLandRegions::EffectiveLandRegions(
|
||||
const LandRegions& landRegions,
|
||||
const int rowCount,
|
||||
const int columnCount,
|
||||
const std::span<const net::eagle0::shardok::storage::fb::Terrain> terrain)
|
||||
: landRegions(landRegions),
|
||||
nodeCount(0) {
|
||||
if (terrain.size() != static_cast<size_t>(rowCount * columnCount)) {
|
||||
throw std::invalid_argument("Effective land region terrain size does not match");
|
||||
}
|
||||
Initialize(
|
||||
rowCount,
|
||||
columnCount,
|
||||
[&](const int index) -> const auto& { return terrain[static_cast<size_t>(index)]; });
|
||||
}
|
||||
|
||||
auto EffectiveLandRegions::Root(const int node) const -> int {
|
||||
int current = node;
|
||||
while (parents[static_cast<size_t>(current)] != current) {
|
||||
current = parents[static_cast<size_t>(current)];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
auto EffectiveLandRegions::Root(const int node) -> int {
|
||||
int root = node;
|
||||
while (parents[static_cast<size_t>(root)] != root) {
|
||||
root = parents[static_cast<size_t>(root)];
|
||||
}
|
||||
|
||||
int current = node;
|
||||
while (parents[static_cast<size_t>(current)] != current) {
|
||||
const int next = parents[static_cast<size_t>(current)];
|
||||
parents[static_cast<size_t>(current)] = static_cast<int16_t>(root);
|
||||
current = next;
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
void EffectiveLandRegions::Union(const int first, const int second) {
|
||||
const int firstRoot = Root(first);
|
||||
const int secondRoot = Root(second);
|
||||
if (firstRoot != secondRoot) {
|
||||
parents[static_cast<size_t>(secondRoot)] = static_cast<int16_t>(firstRoot);
|
||||
}
|
||||
}
|
||||
|
||||
auto EffectiveLandRegions::NodeAt(const Coords& coords) const -> int {
|
||||
if (coords.row() < 0 || coords.column() < 0 || coords.row() >= landRegions.rowCount ||
|
||||
coords.column() >= landRegions.columnCount) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const int index = ToIndex(coords.row(), coords.column(), landRegions.columnCount);
|
||||
const LandRegionId region = landRegions.regions[static_cast<size_t>(index)];
|
||||
if (region != kNoLandRegion) return region;
|
||||
if (bridgeTiles[static_cast<size_t>(index)]) return landRegions.regionCount + index;
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto EffectiveLandRegions::DefinitelyConnectedWithoutCreatingCrossing(
|
||||
const Coords& origin,
|
||||
const Coords& destination) const -> bool {
|
||||
const int originNode = NodeAt(origin);
|
||||
const int destinationNode = NodeAt(destination);
|
||||
return originNode >= 0 && destinationNode >= 0 && Root(originNode) == Root(destinationNode);
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
@@ -0,0 +1,97 @@
|
||||
//
|
||||
// Copyright 2026 Dan Crosby
|
||||
//
|
||||
|
||||
#ifndef EAGLE0_SHARDOK_LIBRARY_MAP_LAND_REGIONS_HPP
|
||||
#define EAGLE0_SHARDOK_LIBRARY_MAP_LAND_REGIONS_HPP
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/Coordinates.hpp"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/hex_map.hpp"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/terrain.hpp"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
using LandRegionId = int8_t;
|
||||
|
||||
constexpr LandRegionId kNoLandRegion = -1;
|
||||
constexpr size_t kMaximumLandRegionTileCount = size_t{12} * 14;
|
||||
|
||||
[[nodiscard]] auto ComputeLandRegions(
|
||||
int rowCount,
|
||||
int columnCount,
|
||||
std::span<const net::eagle0::shardok::storage::fb::Terrain> terrain)
|
||||
-> std::vector<LandRegionId>;
|
||||
|
||||
class LandRegions {
|
||||
private:
|
||||
int rowCount;
|
||||
int columnCount;
|
||||
int regionCount;
|
||||
std::vector<LandRegionId> regions;
|
||||
|
||||
friend class EffectiveLandRegions;
|
||||
|
||||
public:
|
||||
explicit LandRegions(const net::eagle0::shardok::storage::fb::HexMap* map);
|
||||
|
||||
LandRegions(
|
||||
int rowCount,
|
||||
int columnCount,
|
||||
std::span<const net::eagle0::shardok::storage::fb::Terrain> terrain);
|
||||
|
||||
// A true result is conclusive: the two locations have a static path that uses neither water nor
|
||||
// mountains. False is inconclusive because dynamic bridges, ice, or unit capabilities may
|
||||
// connect different static regions.
|
||||
[[nodiscard]] auto DefinitelyConnectedWithoutWater(
|
||||
const Coords& origin,
|
||||
const Coords& destination) const -> bool;
|
||||
|
||||
[[nodiscard]] auto RegionAt(const Coords& coords) const -> LandRegionId;
|
||||
[[nodiscard]] auto size() const -> size_t { return regions.size(); }
|
||||
};
|
||||
|
||||
// Dynamic connectivity derived from immutable land regions plus the usable bridges in one
|
||||
// speculative map state. Ice is intentionally excluded because the AI treats frozen crossings as
|
||||
// crossing-strategy targets rather than ordinary land paths.
|
||||
class EffectiveLandRegions {
|
||||
private:
|
||||
static constexpr size_t kMaximumNodeCount = kMaximumLandRegionTileCount * 2;
|
||||
|
||||
const LandRegions& landRegions;
|
||||
int nodeCount;
|
||||
std::array<int16_t, kMaximumNodeCount> parents{};
|
||||
std::array<bool, kMaximumLandRegionTileCount> bridgeTiles{};
|
||||
|
||||
template<typename TerrainAt>
|
||||
void Initialize(int rowCount, int columnCount, TerrainAt&& terrainAt);
|
||||
|
||||
[[nodiscard]] auto Root(int node) const -> int;
|
||||
auto Root(int node) -> int;
|
||||
void Union(int first, int second);
|
||||
[[nodiscard]] auto NodeAt(const Coords& coords) const -> int;
|
||||
|
||||
public:
|
||||
EffectiveLandRegions(
|
||||
const LandRegions& landRegions,
|
||||
const net::eagle0::shardok::storage::fb::HexMap* map);
|
||||
|
||||
EffectiveLandRegions(
|
||||
const LandRegions& landRegions,
|
||||
int rowCount,
|
||||
int columnCount,
|
||||
std::span<const net::eagle0::shardok::storage::fb::Terrain> terrain);
|
||||
|
||||
[[nodiscard]] auto DefinitelyConnectedWithoutCreatingCrossing(
|
||||
const Coords& origin,
|
||||
const Coords& destination) const -> bool;
|
||||
};
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
#endif // EAGLE0_SHARDOK_LIBRARY_MAP_LAND_REGIONS_HPP
|
||||
@@ -42,6 +42,7 @@ cc_library(
|
||||
deps = [
|
||||
":hex_cube_utils",
|
||||
"//src/main/cpp/net/eagle0/common:filesystem_utils",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:game_state_w",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/map:coordinates",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/map:coords_set",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/map:hex_map_direction",
|
||||
|
||||
@@ -78,7 +78,8 @@ auto FixupCube(
|
||||
// Always rounds half up, instead of away from zero.
|
||||
auto RoundHalfUp(const double d) -> double { return std::floor(d + 0.5); }
|
||||
|
||||
auto CubeLerp(const Cube &c1, const Cube &c2, const double t) -> std::vector<Cube> {
|
||||
auto AppendCubeLerp(std::vector<Cube> &cubes, const Cube &c1, const Cube &c2, const double t)
|
||||
-> void {
|
||||
const auto floatX = std::lerp(c1.x, c2.x, t);
|
||||
const auto newX = RoundHalfUp(floatX);
|
||||
const auto xDiff = abs(newX - floatX);
|
||||
@@ -91,8 +92,6 @@ auto CubeLerp(const Cube &c1, const Cube &c2, const double t) -> std::vector<Cub
|
||||
const auto newZ = RoundHalfUp(floatZ);
|
||||
const auto zDiff = abs(newZ - floatZ);
|
||||
|
||||
std::vector<Cube> cubes{};
|
||||
cubes.reserve(2);
|
||||
cubes.push_back(FixupCube(newX, xDiff, newY, yDiff, newZ, zDiff));
|
||||
|
||||
// The 0.5 case represents a situation where the line passes exactly between two hexes. In that
|
||||
@@ -103,8 +102,6 @@ auto CubeLerp(const Cube &c1, const Cube &c2, const double t) -> std::vector<Cub
|
||||
} else if (yDiff == 0.5) {
|
||||
cubes.push_back(FixupCube(newX, xDiff, floatY - 0.5, yDiff, newZ, zDiff));
|
||||
}
|
||||
|
||||
return cubes;
|
||||
}
|
||||
|
||||
// Slight variation of the method described in
|
||||
@@ -119,9 +116,7 @@ auto CubeLineInclusive(const Cube &c1, const Cube &c2) -> std::vector<Cube> {
|
||||
cubes.reserve(2 * (dist + 1));
|
||||
const double reciprocal = 1.0 / dist;
|
||||
|
||||
for (int i = 0; i <= dist; ++i) {
|
||||
for (const auto &c : CubeLerp(c1, c2, i * reciprocal)) { cubes.push_back(c); }
|
||||
}
|
||||
for (int i = 0; i <= dist; ++i) { AppendCubeLerp(cubes, c1, c2, i * reciprocal); }
|
||||
|
||||
return cubes;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
#include "HexCubeUtils.hpp"
|
||||
#include "src/main/cpp/net/eagle0/common/FilesystemUtils.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
|
||||
|
||||
namespace shardok {
|
||||
@@ -694,6 +695,25 @@ auto KnownAdjacentEnemies(
|
||||
return knownAdjacentEnemies;
|
||||
}
|
||||
|
||||
auto KnownAdjacentEnemies(
|
||||
const PlayerId playerId,
|
||||
const GameStateW &gameState,
|
||||
const std::vector<PlayerId> &allyPids,
|
||||
const Coords &coords) -> std::vector<const Unit *> {
|
||||
const CoordsSet &adjacentCoords = HexMapUtils::GetAdjacentCoords(gameState->hex_map(), coords);
|
||||
std::vector<const Unit *> knownAdjacentEnemies{};
|
||||
knownAdjacentEnemies.reserve(adjacentCoords.size());
|
||||
|
||||
for (const Coords &adjCoords : adjacentCoords) {
|
||||
if (const auto *possibleEnemy =
|
||||
gameState.GetKnownEnemyOccupant(playerId, allyPids, adjCoords)) {
|
||||
knownAdjacentEnemies.push_back(possibleEnemy);
|
||||
}
|
||||
}
|
||||
|
||||
return knownAdjacentEnemies;
|
||||
}
|
||||
|
||||
auto KnownEnemyOccupant(
|
||||
const PlayerId playerId,
|
||||
const Units *units,
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
#include "src/main/protobuf/net/eagle0/shardok/api/unit_view.pb.h"
|
||||
|
||||
namespace shardok {
|
||||
class GameStateW;
|
||||
|
||||
using Coords = net::eagle0::shardok::storage::fb::Coords;
|
||||
using HexMap = net::eagle0::shardok::storage::fb::HexMap;
|
||||
using HexMapT = net::eagle0::shardok::storage::fb::HexMapT;
|
||||
@@ -288,6 +290,12 @@ template<class UnitContainer>
|
||||
const std::vector<PlayerId> &allyPids,
|
||||
const Coords &coords) -> std::vector<const Unit *>;
|
||||
|
||||
[[nodiscard]] auto KnownAdjacentEnemies(
|
||||
PlayerId playerId,
|
||||
const GameStateW &gameState,
|
||||
const std::vector<PlayerId> &allyPids,
|
||||
const Coords &coords) -> std::vector<const Unit *>;
|
||||
|
||||
[[nodiscard]] auto DirectionsTo(int columnCount, const Coords &origin, const Coords &destination)
|
||||
-> HexMapDirectionsTo;
|
||||
|
||||
|
||||
@@ -590,7 +590,10 @@ auto GuessedHero(const HeroViewProto &hv) -> net::eagle0::shardok::storage::fb::
|
||||
hero.mutate_bravery(stats.bravery());
|
||||
|
||||
hero.mutate_vigor(stats.vigor());
|
||||
hero.mutate_starting_vigor(stats.starting_vigor());
|
||||
// The battle-start recovery ceiling is internal state and is not part of HeroView.
|
||||
// Use the visible current vigor as a conservative ceiling rather than inventing
|
||||
// recovery capacity that the player cannot observe.
|
||||
hero.mutate_starting_vigor(stats.vigor());
|
||||
hero.mutate_spent_vigor(kGuessedHeroStat);
|
||||
} else {
|
||||
// Don't believe it's destroyed
|
||||
|
||||
@@ -67,8 +67,6 @@ using net::eagle0::shardok::api::HeroView;
|
||||
stats->set_vigor(hero.vigor());
|
||||
|
||||
if (belongsToAsker) {
|
||||
stats->set_starting_vigor(hero.vigor());
|
||||
|
||||
HeroView::StatXp *statXp = filtered.mutable_stat_xp();
|
||||
statXp->set_strength_xp(hero.strength_xp());
|
||||
statXp->set_agility_xp(hero.agility_xp());
|
||||
@@ -80,4 +78,4 @@ using net::eagle0::shardok::api::HeroView;
|
||||
return filtered;
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
} // namespace shardok
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
load("@rules_cc//cc:defs.bzl", "cc_library")
|
||||
load("//tools:copts.bzl", "COPTS")
|
||||
|
||||
cc_library(
|
||||
name = "cpu_profiler",
|
||||
srcs = ["CpuProfiler.cpp"],
|
||||
hdrs = ["CpuProfiler.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = [
|
||||
"//src/main/cpp/net/eagle0/shardok:__subpackages__",
|
||||
"//src/test/cpp/net/eagle0/shardok:__subpackages__",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "games_manager",
|
||||
srcs = ["ShardokGamesManager.cpp"],
|
||||
@@ -75,6 +86,7 @@ cc_library(
|
||||
copts = COPTS,
|
||||
visibility = ["//src/main/cpp/net/eagle0/shardok:__subpackages__"],
|
||||
deps = [
|
||||
":cpu_profiler",
|
||||
":game_over_response_populator",
|
||||
":games_manager",
|
||||
":token_auth",
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
#include "CpuProfiler.hpp"
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <signal.h>
|
||||
#include <spawn.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cerrno>
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <system_error>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
extern char** environ;
|
||||
|
||||
namespace shardok {
|
||||
namespace {
|
||||
|
||||
auto EpochMillisNow() -> int64_t {
|
||||
return std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch())
|
||||
.count();
|
||||
}
|
||||
|
||||
auto FileSize(const std::string& path) -> int64_t {
|
||||
std::error_code error;
|
||||
const auto size = std::filesystem::file_size(path, error);
|
||||
if (error) { return 0; }
|
||||
return static_cast<int64_t>(size);
|
||||
}
|
||||
|
||||
auto ReadFile(const std::string& path) -> std::string {
|
||||
std::ifstream input(path);
|
||||
if (!input.is_open()) { return {}; }
|
||||
std::ostringstream contents;
|
||||
contents << input.rdbuf();
|
||||
return contents.str();
|
||||
}
|
||||
|
||||
auto PerfExecutable() -> std::string {
|
||||
if (const char* configured = std::getenv("SHARDOK_PERF_EXECUTABLE");
|
||||
configured != nullptr && *configured != '\0') {
|
||||
return configured;
|
||||
}
|
||||
return "perf";
|
||||
}
|
||||
|
||||
class PerfCpuProfileProcess final : public CpuProfileProcess {
|
||||
private:
|
||||
pid_t pid_ = -1;
|
||||
std::string errorPath_;
|
||||
|
||||
void Reap() {
|
||||
if (pid_ <= 0) { return; }
|
||||
int status = 0;
|
||||
while (waitpid(pid_, &status, 0) < 0 && errno == EINTR) {}
|
||||
pid_ = -1;
|
||||
}
|
||||
|
||||
public:
|
||||
auto Start(const std::string& profilePath, const int32_t sampleFrequencyHz)
|
||||
-> std::optional<std::string> override {
|
||||
errorPath_ = profilePath + ".stderr";
|
||||
std::error_code removeError;
|
||||
std::filesystem::remove(errorPath_, removeError);
|
||||
|
||||
const std::string executable = PerfExecutable();
|
||||
std::vector<std::string> arguments{
|
||||
executable,
|
||||
"record",
|
||||
"--output",
|
||||
profilePath,
|
||||
"--freq",
|
||||
std::to_string(sampleFrequencyHz),
|
||||
"--event",
|
||||
"cpu-clock:u",
|
||||
"--inherit",
|
||||
"--call-graph",
|
||||
"dwarf,8192",
|
||||
"--pid",
|
||||
std::to_string(getpid()),
|
||||
};
|
||||
std::vector<char*> argumentPointers;
|
||||
argumentPointers.reserve(arguments.size() + 1);
|
||||
for (auto& argument : arguments) { argumentPointers.push_back(argument.data()); }
|
||||
argumentPointers.push_back(nullptr);
|
||||
|
||||
posix_spawn_file_actions_t fileActions;
|
||||
int result = posix_spawn_file_actions_init(&fileActions);
|
||||
if (result != 0) {
|
||||
return "Unable to initialize perf output capture: " + std::string(strerror(result));
|
||||
}
|
||||
result = posix_spawn_file_actions_addopen(
|
||||
&fileActions,
|
||||
STDERR_FILENO,
|
||||
errorPath_.c_str(),
|
||||
O_WRONLY | O_CREAT | O_TRUNC,
|
||||
0600);
|
||||
if (result == 0) {
|
||||
result = posix_spawnp(
|
||||
&pid_,
|
||||
executable.c_str(),
|
||||
&fileActions,
|
||||
nullptr,
|
||||
argumentPointers.data(),
|
||||
environ);
|
||||
}
|
||||
posix_spawn_file_actions_destroy(&fileActions);
|
||||
if (result != 0) {
|
||||
pid_ = -1;
|
||||
return "Unable to start perf: " + std::string(strerror(result));
|
||||
}
|
||||
|
||||
// Permission and unsupported-event failures happen immediately. Give perf enough time to
|
||||
// report them so the start RPC returns a useful error instead of a false recording state.
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
int status = 0;
|
||||
const pid_t waitResult = waitpid(pid_, &status, WNOHANG);
|
||||
if (waitResult == pid_) {
|
||||
pid_ = -1;
|
||||
std::string detail = ReadFile(errorPath_);
|
||||
if (detail.empty()) { detail = "perf exited before recording began"; }
|
||||
return "perf failed to start the Shardok CPU profile: " + detail;
|
||||
}
|
||||
if (waitResult < 0) {
|
||||
const std::string error = strerror(errno);
|
||||
pid_ = -1;
|
||||
return "Unable to observe perf after startup: " + error;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void Stop() override {
|
||||
if (pid_ <= 0) { return; }
|
||||
if (kill(pid_, SIGINT) < 0 && errno != ESRCH) {
|
||||
// The child belongs to this process, so this is only a last-resort cleanup path.
|
||||
(void)kill(pid_, SIGKILL);
|
||||
}
|
||||
Reap();
|
||||
}
|
||||
|
||||
auto IsRunning() -> bool override {
|
||||
if (pid_ <= 0) { return false; }
|
||||
int status = 0;
|
||||
pid_t waitResult = 0;
|
||||
do {
|
||||
waitResult = waitpid(pid_, &status, WNOHANG);
|
||||
} while (waitResult < 0 && errno == EINTR);
|
||||
if (waitResult == 0) { return true; }
|
||||
pid_ = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
~PerfCpuProfileProcess() override { Stop(); }
|
||||
};
|
||||
|
||||
auto DefaultProcessFactory() -> std::unique_ptr<CpuProfileProcess> {
|
||||
return std::make_unique<PerfCpuProfileProcess>();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
CpuProfiler::CpuProfiler(
|
||||
std::string profilePath,
|
||||
std::string executablePath,
|
||||
const std::chrono::milliseconds maxDuration,
|
||||
const int64_t maxProfileSizeBytes,
|
||||
CpuProfileProcessFactory processFactory)
|
||||
: state_(std::make_shared<State>()),
|
||||
executablePath_(std::move(executablePath)) {
|
||||
state_->profilePath = std::move(profilePath);
|
||||
state_->maxDuration = maxDuration;
|
||||
state_->maxProfileSizeBytes = maxProfileSizeBytes;
|
||||
state_->processFactory = processFactory ? std::move(processFactory)
|
||||
: CpuProfileProcessFactory(DefaultProcessFactory);
|
||||
}
|
||||
|
||||
CpuProfiler::~CpuProfiler() { Stop(); }
|
||||
|
||||
auto CpuProfiler::Start() -> std::optional<std::string> {
|
||||
auto state = state_;
|
||||
std::lock_guard lock(state->mutex);
|
||||
if (state->recording) { return "A Shardok CPU profile is already recording"; }
|
||||
|
||||
std::error_code removeError;
|
||||
std::filesystem::remove(state->profilePath, removeError);
|
||||
if (removeError) {
|
||||
return "Unable to remove the previous Shardok CPU profile: " + removeError.message();
|
||||
}
|
||||
|
||||
state->process = state->processFactory();
|
||||
if (const auto error = state->process->Start(state->profilePath, kSampleFrequencyHz)) {
|
||||
state->process.reset();
|
||||
return error;
|
||||
}
|
||||
|
||||
state->recording = true;
|
||||
state->startedAtEpochMillis = EpochMillisNow();
|
||||
const uint64_t generation = ++state->generation;
|
||||
const auto deadline = std::chrono::steady_clock::now() + state->maxDuration;
|
||||
const auto monitorInterval = std::min(std::chrono::milliseconds(1000), state->maxDuration);
|
||||
|
||||
std::thread([state = std::move(state), generation, deadline, monitorInterval]() {
|
||||
while (true) {
|
||||
std::this_thread::sleep_for(monitorInterval);
|
||||
std::lock_guard monitorLock(state->mutex);
|
||||
if (!state->recording || state->generation != generation) { return; }
|
||||
|
||||
if (!state->process->IsRunning()) {
|
||||
state->recording = false;
|
||||
state->process.reset();
|
||||
return;
|
||||
}
|
||||
if (std::chrono::steady_clock::now() >= deadline ||
|
||||
FileSize(state->profilePath) >= state->maxProfileSizeBytes) {
|
||||
CpuProfiler::StopLocked(*state);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}).detach();
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void CpuProfiler::StopLocked(State& state) {
|
||||
if (!state.recording) { return; }
|
||||
state.process->Stop();
|
||||
state.process.reset();
|
||||
state.recording = false;
|
||||
}
|
||||
|
||||
void CpuProfiler::Stop() {
|
||||
std::lock_guard lock(state_->mutex);
|
||||
StopLocked(*state_);
|
||||
}
|
||||
|
||||
auto CpuProfiler::Snapshot() const -> CpuProfileSnapshot {
|
||||
std::lock_guard lock(state_->mutex);
|
||||
const int64_t profileSize = FileSize(state_->profilePath);
|
||||
return CpuProfileSnapshot{
|
||||
.recording = state_->recording,
|
||||
.profileAvailable = !state_->recording && profileSize > 0,
|
||||
.startedAtEpochMillis = state_->startedAtEpochMillis,
|
||||
.profileSizeBytes = profileSize,
|
||||
};
|
||||
}
|
||||
|
||||
auto CpuProfiler::ProfilePath() const -> const std::string& { return state_->profilePath; }
|
||||
|
||||
auto CpuProfiler::ExecutablePath() const -> const std::string& { return executablePath_; }
|
||||
|
||||
} // namespace shardok
|
||||
@@ -0,0 +1,77 @@
|
||||
#ifndef EAGLE0_SHARDOK_SERVER_CPU_PROFILER_HPP
|
||||
#define EAGLE0_SHARDOK_SERVER_CPU_PROFILER_HPP
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace shardok {
|
||||
|
||||
class CpuProfileProcess {
|
||||
public:
|
||||
virtual ~CpuProfileProcess() = default;
|
||||
|
||||
virtual auto Start(const std::string& profilePath, int32_t sampleFrequencyHz)
|
||||
-> std::optional<std::string> = 0;
|
||||
virtual void Stop() = 0;
|
||||
[[nodiscard]] virtual auto IsRunning() -> bool = 0;
|
||||
};
|
||||
|
||||
using CpuProfileProcessFactory = std::function<std::unique_ptr<CpuProfileProcess>()>;
|
||||
|
||||
struct CpuProfileSnapshot {
|
||||
bool recording;
|
||||
bool profileAvailable;
|
||||
int64_t startedAtEpochMillis;
|
||||
int64_t profileSizeBytes;
|
||||
};
|
||||
|
||||
class CpuProfiler {
|
||||
private:
|
||||
struct State {
|
||||
std::mutex mutex;
|
||||
bool recording = false;
|
||||
int64_t startedAtEpochMillis = 0;
|
||||
uint64_t generation = 0;
|
||||
std::string profilePath;
|
||||
std::chrono::milliseconds maxDuration;
|
||||
int64_t maxProfileSizeBytes;
|
||||
CpuProfileProcessFactory processFactory;
|
||||
std::unique_ptr<CpuProfileProcess> process;
|
||||
};
|
||||
|
||||
std::shared_ptr<State> state_;
|
||||
std::string executablePath_;
|
||||
|
||||
static void StopLocked(State& state);
|
||||
|
||||
public:
|
||||
static constexpr int32_t kSampleFrequencyHz = 99;
|
||||
static constexpr std::chrono::milliseconds kMaxDuration = std::chrono::hours(1);
|
||||
static constexpr int64_t kMaxProfileSizeBytes = 500 * 1024 * 1024;
|
||||
|
||||
explicit CpuProfiler(
|
||||
std::string profilePath = "/tmp/shardok-admin.data",
|
||||
std::string executablePath = "/proc/self/exe",
|
||||
std::chrono::milliseconds maxDuration = kMaxDuration,
|
||||
int64_t maxProfileSizeBytes = kMaxProfileSizeBytes,
|
||||
CpuProfileProcessFactory processFactory = {});
|
||||
~CpuProfiler();
|
||||
|
||||
CpuProfiler(const CpuProfiler&) = delete;
|
||||
auto operator=(const CpuProfiler&) -> CpuProfiler& = delete;
|
||||
|
||||
auto Start() -> std::optional<std::string>;
|
||||
void Stop();
|
||||
[[nodiscard]] auto Snapshot() const -> CpuProfileSnapshot;
|
||||
[[nodiscard]] auto ProfilePath() const -> const std::string&;
|
||||
[[nodiscard]] auto ExecutablePath() const -> const std::string&;
|
||||
};
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
#endif // EAGLE0_SHARDOK_SERVER_CPU_PROFILER_HPP
|
||||
@@ -9,11 +9,15 @@
|
||||
#include "EagleInterfaceGrpcServer.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <iterator>
|
||||
#include <memory>
|
||||
#include <ranges>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
|
||||
@@ -29,6 +33,10 @@
|
||||
namespace shardok {
|
||||
using grpc::Status;
|
||||
using grpc::StatusCode;
|
||||
using net::eagle0::common::CPU_PROFILE_ARTIFACT_TYPE_C_LIBRARY;
|
||||
using net::eagle0::common::CPU_PROFILE_ARTIFACT_TYPE_DYNAMIC_LINKER;
|
||||
using net::eagle0::common::CPU_PROFILE_ARTIFACT_TYPE_EXECUTABLE;
|
||||
using net::eagle0::common::CPU_PROFILE_ARTIFACT_TYPE_PROFILE;
|
||||
using net::eagle0::common::GameSetupInfo;
|
||||
using net::eagle0::common::ScopedShardokLatencyTrace;
|
||||
using net::eagle0::common::ShardokLatencyFields;
|
||||
@@ -36,6 +44,28 @@ using net::eagle0::common::VictoryCondition;
|
||||
using net::eagle0::shardok::api::PlacementCommand;
|
||||
using net::eagle0::shardok::common::EndGameCondition;
|
||||
|
||||
namespace {
|
||||
|
||||
auto FindMappedRuntimeFile(
|
||||
const std::string_view exactFilename,
|
||||
const std::string_view filenamePrefix = {}) -> std::string {
|
||||
std::ifstream maps("/proc/self/maps");
|
||||
for (std::string line; std::getline(maps, line);) {
|
||||
const auto pathStart = line.find('/');
|
||||
if (pathStart == std::string::npos) { continue; }
|
||||
|
||||
auto path = line.substr(pathStart);
|
||||
const auto filename = std::filesystem::path(path).filename().string();
|
||||
if (filename == exactFilename ||
|
||||
(!filenamePrefix.empty() && filename.starts_with(filenamePrefix))) {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
class NewGameException : public std::exception {
|
||||
private:
|
||||
Status status;
|
||||
@@ -46,6 +76,16 @@ public:
|
||||
auto GetStatus() const -> Status { return status; }
|
||||
};
|
||||
|
||||
void PopulateCpuProfileStatus(
|
||||
const CpuProfileSnapshot &snapshot,
|
||||
CpuProfileStatusResponse *response) {
|
||||
response->set_recording(snapshot.recording);
|
||||
response->set_profile_available(snapshot.profileAvailable);
|
||||
response->set_started_at_epoch_millis(snapshot.startedAtEpochMillis);
|
||||
response->set_profile_size_bytes(snapshot.profileSizeBytes);
|
||||
response->set_sample_frequency_hz(CpuProfiler::kSampleFrequencyHz);
|
||||
}
|
||||
|
||||
auto EagleInterfaceImpl::ControllerForGame(const GameId &gameId, const GameSetupInfo &setupInfo)
|
||||
-> std::shared_ptr<ShardokGameController> {
|
||||
ScopedShardokLatencyTrace trace(
|
||||
@@ -96,6 +136,89 @@ EagleInterfaceImpl::EagleInterfaceImpl(
|
||||
}
|
||||
}
|
||||
|
||||
auto EagleInterfaceImpl::GetCpuProfileStatus(
|
||||
ServerContext *context,
|
||||
const CpuProfileStatusRequest * /*request*/,
|
||||
CpuProfileStatusResponse *response) -> Status {
|
||||
if (auto authStatus = tokenValidator_.ValidateOrStatus(context)) { return *authStatus; }
|
||||
PopulateCpuProfileStatus(cpuProfiler_.Snapshot(), response);
|
||||
return Status::OK;
|
||||
}
|
||||
|
||||
auto EagleInterfaceImpl::StartCpuProfile(
|
||||
ServerContext *context,
|
||||
const StartCpuProfileRequest * /*request*/,
|
||||
CpuProfileStatusResponse *response) -> Status {
|
||||
if (auto authStatus = tokenValidator_.ValidateOrStatus(context)) { return *authStatus; }
|
||||
|
||||
if (const auto error = cpuProfiler_.Start()) {
|
||||
return Status(StatusCode::FAILED_PRECONDITION, *error);
|
||||
}
|
||||
|
||||
PopulateCpuProfileStatus(cpuProfiler_.Snapshot(), response);
|
||||
return Status::OK;
|
||||
}
|
||||
|
||||
auto EagleInterfaceImpl::StopCpuProfile(
|
||||
ServerContext *context,
|
||||
const StopCpuProfileRequest * /*request*/,
|
||||
CpuProfileStatusResponse *response) -> Status {
|
||||
if (auto authStatus = tokenValidator_.ValidateOrStatus(context)) { return *authStatus; }
|
||||
cpuProfiler_.Stop();
|
||||
PopulateCpuProfileStatus(cpuProfiler_.Snapshot(), response);
|
||||
return Status::OK;
|
||||
}
|
||||
|
||||
auto EagleInterfaceImpl::DownloadCpuProfile(
|
||||
ServerContext *context,
|
||||
const CpuProfileDownloadRequest *request,
|
||||
grpc::ServerWriter<CpuProfileChunk> *writer) -> Status {
|
||||
if (auto authStatus = tokenValidator_.ValidateOrStatus(context)) { return *authStatus; }
|
||||
|
||||
std::string path;
|
||||
switch (request->artifact_type()) {
|
||||
case CPU_PROFILE_ARTIFACT_TYPE_PROFILE:
|
||||
if (!cpuProfiler_.Snapshot().profileAvailable) {
|
||||
return Status(
|
||||
StatusCode::FAILED_PRECONDITION,
|
||||
"No Shardok CPU profile is available");
|
||||
}
|
||||
path = cpuProfiler_.ProfilePath();
|
||||
break;
|
||||
case CPU_PROFILE_ARTIFACT_TYPE_EXECUTABLE: path = cpuProfiler_.ExecutablePath(); break;
|
||||
case CPU_PROFILE_ARTIFACT_TYPE_C_LIBRARY: path = FindMappedRuntimeFile("libc.so.6"); break;
|
||||
case CPU_PROFILE_ARTIFACT_TYPE_DYNAMIC_LINKER:
|
||||
path = FindMappedRuntimeFile("", "ld-linux-");
|
||||
break;
|
||||
default: return Status(StatusCode::INVALID_ARGUMENT, "Unknown CPU profile artifact type");
|
||||
}
|
||||
|
||||
if (path.empty()) {
|
||||
return Status(StatusCode::NOT_FOUND, "CPU profile runtime artifact is not mapped");
|
||||
}
|
||||
std::ifstream input(path, std::ios::binary);
|
||||
if (!input.is_open()) {
|
||||
return Status(StatusCode::NOT_FOUND, "Unable to open CPU profile artifact");
|
||||
}
|
||||
|
||||
std::array<char, 64 * 1024> buffer{};
|
||||
while (input.good()) {
|
||||
input.read(buffer.data(), static_cast<std::streamsize>(buffer.size()));
|
||||
const std::streamsize count = input.gcount();
|
||||
if (count <= 0) { break; }
|
||||
CpuProfileChunk chunk;
|
||||
chunk.set_data(buffer.data(), static_cast<size_t>(count));
|
||||
if (!writer->Write(chunk)) {
|
||||
return Status(StatusCode::CANCELLED, "Profile download cancelled");
|
||||
}
|
||||
}
|
||||
if (input.bad()) {
|
||||
return Status(StatusCode::INTERNAL, "Failed while reading CPU profile artifact");
|
||||
}
|
||||
|
||||
return Status::OK;
|
||||
}
|
||||
|
||||
auto ConvertPlayerInfo(
|
||||
const google::protobuf::RepeatedPtrField<net::eagle0::common::PlayerSetupInfo> &allPis,
|
||||
PlayerId shardokPid) -> PlayerInfoWithUnits {
|
||||
@@ -420,10 +543,11 @@ void EagleInterfaceImpl::PopulateGameStatusResponse(
|
||||
string(controller->GetCurrentGameStateBytes());
|
||||
|
||||
response->mutable_game_update_response()->set_current_round(results.currentRound);
|
||||
for (const auto &[playerId, troopCount] : results.playerTroopCounts) {
|
||||
for (const auto &[playerId, troopCount, isRetreating] : results.playerBattleProgress) {
|
||||
auto *ptc = response->mutable_game_update_response()->add_player_troop_counts();
|
||||
ptc->set_player_id(playerId);
|
||||
ptc->set_troop_count(troopCount);
|
||||
ptc->set_is_retreating(isRetreating);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -539,7 +663,7 @@ auto EagleInterfaceImpl::SubscribeToGame(
|
||||
int32_t newUnfilteredCount,
|
||||
const byte_vector ¤tGameState,
|
||||
int32_t currentRound,
|
||||
const vector<std::pair<int32_t, int32_t>> &playerTroopCounts) override {
|
||||
const vector<PlayerBattleProgress> &playerBattleProgress) override {
|
||||
if (!active_) return;
|
||||
|
||||
GameStatusResponse response;
|
||||
@@ -568,10 +692,11 @@ auto EagleInterfaceImpl::SubscribeToGame(
|
||||
std::string(currentGameState.begin(), currentGameState.end());
|
||||
|
||||
response.mutable_game_update_response()->set_current_round(currentRound);
|
||||
for (const auto &[playerId, troopCount] : playerTroopCounts) {
|
||||
for (const auto &[playerId, troopCount, isRetreating] : playerBattleProgress) {
|
||||
auto *ptc = response.mutable_game_update_response()->add_player_troop_counts();
|
||||
ptc->set_player_id(playerId);
|
||||
ptc->set_troop_count(troopCount);
|
||||
ptc->set_is_retreating(isRetreating);
|
||||
}
|
||||
|
||||
ScopedShardokLatencyTrace trace(
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <thread>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/common/ProtobufWarningSuppression.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/server/CpuProfiler.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/server/ShardokGamesManager.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/server/TokenAuthInterceptor.hpp"
|
||||
#pragma GCC diagnostic push
|
||||
@@ -31,6 +32,10 @@ SUPPRESS_PROTOBUF_WARNINGS
|
||||
namespace shardok {
|
||||
using grpc::ServerContext;
|
||||
using grpc::Status;
|
||||
using net::eagle0::common::CpuProfileChunk;
|
||||
using net::eagle0::common::CpuProfileDownloadRequest;
|
||||
using net::eagle0::common::CpuProfileStatusRequest;
|
||||
using net::eagle0::common::CpuProfileStatusResponse;
|
||||
using net::eagle0::common::GameSetupInfo;
|
||||
using net::eagle0::common::GameStatusResponse;
|
||||
using net::eagle0::common::GameSubscriptionRequest;
|
||||
@@ -42,11 +47,14 @@ using net::eagle0::common::NewGameRequest;
|
||||
using net::eagle0::common::PlacementCommandsRequest;
|
||||
using net::eagle0::common::PostCommandRequest;
|
||||
using net::eagle0::common::ShardokInternalInterface;
|
||||
using net::eagle0::common::StartCpuProfileRequest;
|
||||
using net::eagle0::common::StopCpuProfileRequest;
|
||||
|
||||
class EagleInterfaceImpl final : public ShardokInternalInterface::Service {
|
||||
private:
|
||||
std::shared_ptr<ShardokGamesManager> gamesManager;
|
||||
TokenValidator tokenValidator_;
|
||||
CpuProfiler cpuProfiler_;
|
||||
|
||||
auto ControllerForGame(const GameId& gameId, const GameSetupInfo& setupInfo)
|
||||
-> std::shared_ptr<ShardokGameController>;
|
||||
@@ -87,6 +95,23 @@ public:
|
||||
ServerContext* context,
|
||||
const GameSubscriptionRequest* request,
|
||||
grpc::ServerWriter<GameStatusResponse>* writer) -> Status override;
|
||||
|
||||
auto GetCpuProfileStatus(
|
||||
ServerContext* context,
|
||||
const CpuProfileStatusRequest* request,
|
||||
CpuProfileStatusResponse* response) -> Status override;
|
||||
auto StartCpuProfile(
|
||||
ServerContext* context,
|
||||
const StartCpuProfileRequest* request,
|
||||
CpuProfileStatusResponse* response) -> Status override;
|
||||
auto StopCpuProfile(
|
||||
ServerContext* context,
|
||||
const StopCpuProfileRequest* request,
|
||||
CpuProfileStatusResponse* response) -> Status override;
|
||||
auto DownloadCpuProfile(
|
||||
ServerContext* context,
|
||||
const CpuProfileDownloadRequest* request,
|
||||
grpc::ServerWriter<CpuProfileChunk>* writer) -> Status override;
|
||||
};
|
||||
} // namespace shardok
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using common;
|
||||
using Grpc.Core;
|
||||
@@ -26,6 +27,10 @@ namespace Auth {
|
||||
private string _pendingOAuthState; // Queued if OAuth result arrives before auth
|
||||
// client ready
|
||||
|
||||
private const float TokenRefreshCheckIntervalSeconds = 5 * 60f;
|
||||
private float _nextTokenRefreshCheckTime;
|
||||
private readonly SemaphoreSlim _tokenRefreshSemaphore = new(1, 1);
|
||||
|
||||
// Events for UI updates
|
||||
public event Action<UserInfo> OnLoginSuccess;
|
||||
public event Action<string> OnLoginFailed;
|
||||
@@ -66,6 +71,40 @@ namespace Auth {
|
||||
CheckCommandLineForDeepLink();
|
||||
}
|
||||
|
||||
private void Update() {
|
||||
if (Time.unscaledTime < _nextTokenRefreshCheckTime) return;
|
||||
|
||||
_nextTokenRefreshCheckTime = Time.unscaledTime + TokenRefreshCheckIntervalSeconds;
|
||||
TryProactiveTokenRefresh();
|
||||
}
|
||||
|
||||
private void OnApplicationFocus(bool hasFocus) {
|
||||
if (hasFocus) { TryProactiveTokenRefresh(); }
|
||||
}
|
||||
|
||||
private void OnApplicationPause(bool pauseStatus) {
|
||||
if (!pauseStatus) { TryProactiveTokenRefresh(); }
|
||||
}
|
||||
|
||||
private void TryProactiveTokenRefresh() {
|
||||
if (_authClient == null || !TokenStorage.NeedsRefresh ||
|
||||
!TokenStorage.HasRefreshToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
_ = RefreshTokenProactivelyAsync();
|
||||
}
|
||||
|
||||
private async Task RefreshTokenProactivelyAsync() {
|
||||
try {
|
||||
await EnsureValidTokenAsync();
|
||||
} catch (Exception ex) {
|
||||
// A reconnect will make one final refresh attempt and surface an explicit
|
||||
// authentication error if the refresh token is no longer usable.
|
||||
Debug.LogWarning($"[OAuthManager] Proactive token refresh failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckCommandLineForDeepLink() {
|
||||
var args = System.Environment.GetCommandLineArgs();
|
||||
// Args[0] is the executable path, check remaining args for eagle0:// URLs
|
||||
@@ -415,9 +454,15 @@ namespace Auth {
|
||||
/// </summary>
|
||||
public async Task EnsureValidTokenAsync() {
|
||||
EnsureConfigured();
|
||||
if (TokenStorage.NeedsRefresh && TokenStorage.HasRefreshToken) {
|
||||
await _authClient.RefreshTokenAsync();
|
||||
}
|
||||
|
||||
await _tokenRefreshSemaphore.WaitAsync();
|
||||
try {
|
||||
// Re-check after acquiring the semaphore because another caller may have
|
||||
// completed the refresh while this caller was waiting.
|
||||
if (TokenStorage.NeedsRefresh && TokenStorage.HasRefreshToken) {
|
||||
await _authClient.RefreshTokenAsync();
|
||||
}
|
||||
} finally { _tokenRefreshSemaphore.Release(); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -8,6 +8,8 @@ namespace Auth {
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class StoredAccount {
|
||||
private const long RefreshEarlySeconds = 30 * 60;
|
||||
|
||||
public string Provider; // "discord" or "google"
|
||||
public string UserId; // OAuth provider user ID
|
||||
public string DisplayName; // User's display name
|
||||
@@ -26,7 +28,7 @@ namespace Auth {
|
||||
public bool NeedsRefresh {
|
||||
get {
|
||||
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
return ExpiresAt > 0 && ExpiresAt - now < 300; // 5 minutes
|
||||
return ExpiresAt > 0 && ExpiresAt - now < RefreshEarlySeconds;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+30
-7
@@ -780,12 +780,12 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
fetchedNewGameLeaders.AddRange(lobbyResponse.NewGameOptions.AvailableLeaders);
|
||||
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
// Skip lobby UI updates if we've already left the connection UI.
|
||||
// The lobby response may arrive after the game has started (e.g., when creating a new
|
||||
// game), and the connection canvas is inactive, causing GetComponentInParent to fail.
|
||||
// This check must be inside the queue callback since the state may change between
|
||||
// when the response arrives and when this callback executes.
|
||||
if (!IsConnectionModeActive()) return;
|
||||
// Skip lobby UI updates if we've already left the connection UI or opened the custom
|
||||
// battle panel. A lobby response may already be in flight when either transition
|
||||
// happens, so it must not switch the visible panel back to the lobby.
|
||||
// This check must be inside the queue callback since the state may change between when
|
||||
// the response arrives and when this callback executes.
|
||||
if (!IsConnectionModeActive() || customBattlePanel.gameObject.activeSelf) return;
|
||||
|
||||
ResourceFetcher.headshotFetcher.Prefetch(
|
||||
fetchedNewGameLeaders.Select(a => a.ImagePath));
|
||||
@@ -894,7 +894,8 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
_persistentClientConnection = new PersistentClientConnection(
|
||||
eagleConnection.EagleGrpcClient,
|
||||
eagleConnection.credentials,
|
||||
EagleConnection.EagleCancellationToken);
|
||||
EagleConnection.EagleCancellationToken,
|
||||
TryRefreshAuthenticationOnMainThreadAsync);
|
||||
_persistentClientConnection.OnChannelDead = () => {
|
||||
// Queue to main thread to avoid threading issues
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
@@ -915,6 +916,28 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
ResourceFetcher.SetUpConnection(_httpClient);
|
||||
}
|
||||
|
||||
private Task<bool> TryRefreshAuthenticationOnMainThreadAsync() {
|
||||
var completion =
|
||||
new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
MainQueue.Q.Enqueue(async () => {
|
||||
try {
|
||||
if (OAuthManager.Instance == null) {
|
||||
completion.TrySetResult(false);
|
||||
return;
|
||||
}
|
||||
|
||||
await OAuthManager.Instance.EnsureValidTokenAsync();
|
||||
completion.TrySetResult(TokenStorage.HasValidToken);
|
||||
} catch (Exception ex) {
|
||||
Debug.LogWarning($"[ConnectionHandler] Session refresh failed: {ex.Message}");
|
||||
completion.TrySetResult(false);
|
||||
}
|
||||
});
|
||||
|
||||
return completion.Task;
|
||||
}
|
||||
|
||||
private void _internalCustomBattle() {
|
||||
// Connection already exists when called from lobby
|
||||
_persistentClientConnection.SetLobbySubscriber(this);
|
||||
|
||||
+5
@@ -44,6 +44,11 @@ public class CustomBattleHandler : MonoBehaviour, IClientConnectionSubscriber {
|
||||
_eagleConnection = conn;
|
||||
_persistentClientConnection = persistentClientConnection;
|
||||
|
||||
yourUnitsController.RowCount = 0;
|
||||
aiUnitsController.RowCount = 0;
|
||||
_heroNames.Clear();
|
||||
_heroImages.Clear();
|
||||
|
||||
var random = new Random();
|
||||
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
string gameId = new string(
|
||||
|
||||
+10
-3
@@ -24,9 +24,11 @@ namespace eagle {
|
||||
public void
|
||||
Populate(ShardokBattleView battle, IGameModel model, Action<string> goToBattleAction) {
|
||||
model.Provinces.TryGetValue(battle.DefenderProvince, out var pv);
|
||||
provinceLabel.text = AttackerOriginsText(battle, model);
|
||||
defenderProvinceLabel.text =
|
||||
pv != null ? pv.Name : $"Province {battle.DefenderProvince}";
|
||||
provinceLabel.text =
|
||||
RetreatingLabel(AttackerOriginsText(battle, model), battle.AttackerRetreating);
|
||||
defenderProvinceLabel.text = RetreatingLabel(
|
||||
pv != null ? pv.Name : $"Province {battle.DefenderProvince}",
|
||||
battle.DefenderRetreating);
|
||||
|
||||
bool isSelf = battle.PlayerInfos.Any(pi => pi.Hostility == Hostility.SelfHostility);
|
||||
bool isAllied = isSelf ||
|
||||
@@ -136,6 +138,11 @@ namespace eagle {
|
||||
return $"Province {provinceId}";
|
||||
}
|
||||
|
||||
private static string RetreatingLabel(string label, bool isRetreating) {
|
||||
if (!isRetreating) return label;
|
||||
return string.IsNullOrEmpty(label) ? "Retreating" : $"{label} (Retreating)";
|
||||
}
|
||||
|
||||
private void SetFightButtonTutorialRegistration(bool shouldRegister) {
|
||||
var registry = TutorialManager.Instance?.TargetRegistry;
|
||||
if (registry == null) return;
|
||||
|
||||
@@ -41,7 +41,7 @@ namespace eagle {
|
||||
}
|
||||
|
||||
public static void SaveCompletedText(long gameId, string llmId, string text) {
|
||||
if (String.IsNullOrEmpty(llmId)) { return; }
|
||||
if (String.IsNullOrEmpty(llmId) || String.IsNullOrEmpty(text)) { return; }
|
||||
|
||||
lock (Lock) {
|
||||
if (_cacheDirectory == null) {
|
||||
@@ -97,8 +97,8 @@ namespace eagle {
|
||||
if (cache?.entries == null) { return; }
|
||||
|
||||
foreach (var entry in cache.entries) {
|
||||
if (!String.IsNullOrEmpty(entry.llmId)) {
|
||||
gameCache.entries[entry.llmId] = entry.text ?? "";
|
||||
if (!String.IsNullOrEmpty(entry.llmId) && !String.IsNullOrEmpty(entry.text)) {
|
||||
gameCache.entries[entry.llmId] = entry.text;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
|
||||
@@ -12,10 +12,13 @@ namespace eagle {
|
||||
public class TextEntry {
|
||||
public string Text { get; }
|
||||
public bool Completed { get; }
|
||||
public string StreamAttemptId { get; }
|
||||
|
||||
public TextEntry(string text, bool completed) {
|
||||
public TextEntry(string text, bool completed, string streamAttemptId = "") {
|
||||
Text = text ?? throw new ArgumentNullException(nameof(text));
|
||||
Completed = completed;
|
||||
StreamAttemptId =
|
||||
streamAttemptId ?? throw new ArgumentNullException(nameof(streamAttemptId));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,8 +63,11 @@ namespace eagle {
|
||||
_pendingUpdates.Clear();
|
||||
|
||||
foreach (var (llmId, text) in completedTexts) {
|
||||
if (String.IsNullOrEmpty(llmId)) { continue; }
|
||||
_streamingTexts[llmId] = new TextEntry(text ?? "", true);
|
||||
// A completed entry with no content was produced by an invalid empty LLM
|
||||
// stream. Do not tell the server that this legacy cache entry is complete;
|
||||
// omitting it lets the repaired server request regenerate and resend it.
|
||||
if (String.IsNullOrEmpty(llmId) || String.IsNullOrEmpty(text)) { continue; }
|
||||
_streamingTexts[llmId] = new TextEntry(text, true);
|
||||
_pendingUpdates.Add(llmId);
|
||||
}
|
||||
}
|
||||
@@ -71,20 +77,33 @@ namespace eagle {
|
||||
/// Updates the text dictionary. Thread-safe - can be called from any thread.
|
||||
/// Listeners are NOT notified here; call ProcessPendingUpdates from main thread.
|
||||
/// </summary>
|
||||
public String
|
||||
HandleNewStreamingText(String llmId, String newText, Int32 knownByteCount, bool completed) {
|
||||
public String HandleNewStreamingText(
|
||||
String llmId,
|
||||
String newText,
|
||||
Int32 knownByteCount,
|
||||
bool completed,
|
||||
String streamAttemptId = "") {
|
||||
if (llmId == null) { throw new ArgumentNullException(nameof(llmId)); }
|
||||
if (newText == null) { throw new ArgumentNullException(nameof(newText)); }
|
||||
if (streamAttemptId == null) {
|
||||
throw new ArgumentNullException(nameof(streamAttemptId));
|
||||
}
|
||||
|
||||
lock (_lock) {
|
||||
var currentText = "";
|
||||
if (_streamingTexts.TryGetValue(llmId, out var entry)) { currentText = entry.Text; }
|
||||
var retainedByteCount = 0;
|
||||
if (_streamingTexts.TryGetValue(llmId, out var entry)) {
|
||||
currentText = entry.Text;
|
||||
if (entry.StreamAttemptId == streamAttemptId) {
|
||||
retainedByteCount = knownByteCount;
|
||||
}
|
||||
}
|
||||
|
||||
var currentTextBytes = Encoding.UTF8.GetBytes(currentText);
|
||||
var truncatedBytes = currentTextBytes.Take(knownByteCount).ToArray();
|
||||
var truncatedBytes = currentTextBytes.Take(retainedByteCount).ToArray();
|
||||
|
||||
var updatedText = Encoding.UTF8.GetString(truncatedBytes) + newText;
|
||||
_streamingTexts[llmId] = new TextEntry(updatedText, completed);
|
||||
_streamingTexts[llmId] = new TextEntry(updatedText, completed, streamAttemptId);
|
||||
|
||||
// Mark this text ID as having pending updates
|
||||
_pendingUpdates.Add(llmId);
|
||||
|
||||
@@ -5314,7 +5314,7 @@ PrefabInstance:
|
||||
- target: {fileID: 1555308994113599573, guid: 911c5413c29434fa8a462a0442ce7a75,
|
||||
type: 3}
|
||||
propertyPath: m_Name
|
||||
value: Travel Panel
|
||||
value: Visit Town Panel
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 7194983473711936932, guid: 911c5413c29434fa8a462a0442ce7a75,
|
||||
type: 3}
|
||||
|
||||
+46
-28
@@ -63,7 +63,7 @@ namespace eagle {
|
||||
{ AvailableCommand.SealedValueOneofCase.SwearBrotherhoodCommand, "swear-kinship" },
|
||||
{ AvailableCommand.SealedValueOneofCase.TradeCommand, "trade" },
|
||||
{ AvailableCommand.SealedValueOneofCase.TrainCommand, "train" },
|
||||
{ AvailableCommand.SealedValueOneofCase.TravelCommand, "travel" },
|
||||
{ AvailableCommand.SealedValueOneofCase.VisitTownCommand, "visit-town" },
|
||||
};
|
||||
|
||||
private readonly struct CommandInfo {
|
||||
@@ -219,8 +219,10 @@ namespace eagle {
|
||||
"Exchange food and gold using the province's buy and sell prices.") },
|
||||
{ AvailableCommand.SealedValueOneofCase.TrainCommand,
|
||||
new CommandInfo("Train", "Have a hero train to improve their abilities.") },
|
||||
{ AvailableCommand.SealedValueOneofCase.TravelCommand,
|
||||
new CommandInfo("Travel", "Move a hero between provinces without moving an army.") },
|
||||
{ AvailableCommand.SealedValueOneofCase.VisitTownCommand,
|
||||
new CommandInfo(
|
||||
"Visit Town",
|
||||
"Have the ruling hero visit the current province's town to unlock local commands.") },
|
||||
};
|
||||
|
||||
public EagleGameController mainController;
|
||||
@@ -277,6 +279,7 @@ namespace eagle {
|
||||
Selector.CurrentLayoutMode = mode;
|
||||
Selector.UpdateAvailableCommand(Model, AvailableCommand);
|
||||
headerLabel.text = Selector.HeaderString;
|
||||
RebuildHeaderLayout();
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -296,6 +299,8 @@ namespace eagle {
|
||||
if (AvailableCommand == null) {
|
||||
Selector = null;
|
||||
headerLabel.text = null;
|
||||
UpdateCommandControls();
|
||||
RebuildHeaderLayout();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -303,6 +308,8 @@ namespace eagle {
|
||||
Selector.CurrentLayoutMode = mainController.CurrentLayoutMode;
|
||||
Selector.UpdateAvailableCommand(Model, AvailableCommand);
|
||||
headerLabel.text = Selector.HeaderString;
|
||||
UpdateCommandControls();
|
||||
RebuildHeaderLayout();
|
||||
|
||||
// If help was showing for previous command, show help for new command
|
||||
if (helpWasShowing) { ShowCurrentCommandHelp(); }
|
||||
@@ -312,6 +319,41 @@ namespace eagle {
|
||||
}
|
||||
}
|
||||
|
||||
private void RebuildHeaderLayout() {
|
||||
// The title's width depends on the active header controls. Rebuild both now so the
|
||||
// old text geometry cannot render for a frame in the new layout.
|
||||
LayoutRebuilder.ForceRebuildLayoutImmediate(
|
||||
(RectTransform)headerLabel.transform.parent);
|
||||
headerLabel.ForceMeshUpdate();
|
||||
}
|
||||
|
||||
private void UpdateCommandControls() {
|
||||
if (Selector == null) {
|
||||
commitCommandButton.gameObject.SetActive(false);
|
||||
if (infoButton != null) { infoButton.gameObject.SetActive(false); }
|
||||
return;
|
||||
}
|
||||
|
||||
bool clearWarningPanel = !commitCommandButton.gameObject.activeSelf;
|
||||
|
||||
if (Selector.WarnOnCommitButton) {
|
||||
commitButtonText.text = $"<color=#FF0000FF>{Selector.CommitButtonString}</color>";
|
||||
} else {
|
||||
commitButtonText.text = Selector.CommitButtonString;
|
||||
}
|
||||
commitCommandButton.gameObject.SetActive(true);
|
||||
commitCommandButton.interactable = Selector.EnableCommitButton;
|
||||
|
||||
if (infoButton != null) { infoButton.gameObject.SetActive(true); }
|
||||
|
||||
if (clearWarningPanel) {
|
||||
MainQueue.Q.EnqueueForNextUpdate(() => {
|
||||
commandWarningPanelController.ClearWarning();
|
||||
_commandInfoShowing = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows help for the current command (used when switching commands while help is open).
|
||||
/// </summary>
|
||||
@@ -435,31 +477,7 @@ namespace eagle {
|
||||
public void OnGUI() {
|
||||
var eventType = Event.current.type;
|
||||
try {
|
||||
if (Selector == null) {
|
||||
commitCommandButton.gameObject.SetActive(false);
|
||||
if (infoButton != null) { infoButton.gameObject.SetActive(false); }
|
||||
} else {
|
||||
bool disableWarningPanel = !commitCommandButton.gameObject.activeSelf;
|
||||
|
||||
if (Selector.WarnOnCommitButton) {
|
||||
commitButtonText.text =
|
||||
$"<color=#FF0000FF>{Selector.CommitButtonString}</color>";
|
||||
} else {
|
||||
commitButtonText.text = Selector.CommitButtonString;
|
||||
}
|
||||
commitCommandButton.gameObject.SetActive(true);
|
||||
commitCommandButton.interactable = Selector.EnableCommitButton;
|
||||
|
||||
// Show info button when a command is selected
|
||||
if (infoButton != null) { infoButton.gameObject.SetActive(true); }
|
||||
|
||||
if (disableWarningPanel) {
|
||||
MainQueue.Q.EnqueueForNextUpdate(() => {
|
||||
commandWarningPanelController.ClearWarning();
|
||||
_commandInfoShowing = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
UpdateCommandControls();
|
||||
} catch (Exception e) {
|
||||
Debug.Log($"Event type is {eventType}");
|
||||
errorHandler.Add(e);
|
||||
|
||||
+2
-2
@@ -13,7 +13,7 @@ GameObject:
|
||||
- component: {fileID: 6289363470016652304}
|
||||
- component: {fileID: 1398189250911451454}
|
||||
m_Layer: 0
|
||||
m_Name: Travel Panel
|
||||
m_Name: Visit Town Panel
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
@@ -102,7 +102,7 @@ GameObject:
|
||||
- component: {fileID: 7408366842504297803}
|
||||
- component: {fileID: 2299640137554880340}
|
||||
m_Layer: 0
|
||||
m_Name: Travel command description
|
||||
m_Name: Visit Town command description
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
+2
@@ -114,5 +114,7 @@ namespace eagle {
|
||||
// Note: Command tutorials are now opened explicitly instead of automatic popups.
|
||||
// See CommandPanelController.ShowCurrentCommandHelp() for the help-panel flow.
|
||||
}
|
||||
|
||||
public void UpdateModel(IGameModel model) { _model = model; }
|
||||
}
|
||||
}
|
||||
|
||||
+9
-1
@@ -158,7 +158,15 @@ namespace eagle {
|
||||
var attackingFactionNames = DefendCommand.HostileFactionIds
|
||||
.Select(factionId => _model.FactionName(factionId))
|
||||
.ToList();
|
||||
marchText.text = $"Attack from {String.Join(", ", attackingFactionNames)}!";
|
||||
var originProvinceNames =
|
||||
_model.Provinces[ActingProvinceId]
|
||||
.KnownIncomingArmies
|
||||
.Where(army => DefendCommand.HostileFactionIds.Contains(army.FactionId))
|
||||
.Select(army => _model.Provinces[army.OriginProvinceId].Name)
|
||||
.Distinct()
|
||||
.OrderBy(name => name);
|
||||
marchText.text = $"Attack from {String.Join(", ", originProvinceNames)} by " +
|
||||
$"{String.Join(", ", attackingFactionNames)}!";
|
||||
|
||||
commanderCountText.text = $"{DefendCommand.HostileHeroCount}";
|
||||
troopCountText.text = $"{DefendCommand.HostileTroopCount}";
|
||||
|
||||
+14
-5
@@ -64,14 +64,23 @@ namespace eagle {
|
||||
|
||||
var demandingFaction = _model.MaybeDestroyedFaction(SelectedDemand.DemandingFactionId);
|
||||
|
||||
factionDescription.text =
|
||||
$"{DisplayNames.ResolvedFactionString(demandingFaction, _model)} demands tribute!";
|
||||
|
||||
goldAmountLabel.text = SelectedDemand.TributeDemanded.Gold.ToString();
|
||||
foodAmountLabel.text = SelectedDemand.TributeDemanded.Food.ToString();
|
||||
|
||||
var armiesForFaction = _model.Provinces[ActingProvinceId].KnownIncomingArmies.Where(
|
||||
ai => ai.FactionId == SelectedDemand.DemandingFactionId);
|
||||
var armiesForFaction =
|
||||
_model.Provinces[ActingProvinceId]
|
||||
.KnownIncomingArmies
|
||||
.Where(ai => ai.FactionId == SelectedDemand.DemandingFactionId)
|
||||
.ToList();
|
||||
var originProvinceNames =
|
||||
armiesForFaction.Select(army => _model.Provinces[army.OriginProvinceId].Name)
|
||||
.Distinct()
|
||||
.OrderBy(name => name);
|
||||
var incomingArmyLabel = armiesForFaction.Count == 1 ? "army" : "armies";
|
||||
factionDescription.text =
|
||||
$"{DisplayNames.ResolvedFactionString(demandingFaction, _model)} demands tribute!\n" +
|
||||
$"Incoming {incomingArmyLabel} from {String.Join(", ", originProvinceNames)}.";
|
||||
|
||||
incomingHeroCount.text = SelectedDemand.HeroCount.ToString();
|
||||
incomingTroopsCount.text = SelectedDemand.TroopCount.ToString();
|
||||
|
||||
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
using System;
|
||||
using Net.Eagle0.Eagle.Api;
|
||||
|
||||
namespace eagle {
|
||||
using ProvinceId = Int32;
|
||||
|
||||
public class TravelCommandSelector : CommandSelector {
|
||||
public override AvailableCommand.SealedValueOneofCase CommandType =>
|
||||
AvailableCommand.SealedValueOneofCase.TravelCommand;
|
||||
public override string HeaderString => "Travel";
|
||||
public override string CommitButtonString => "Commit Travel";
|
||||
|
||||
private TravelAvailableCommand TravelAvailableCommand => _availableCommand.TravelCommand;
|
||||
private ProvinceId ActingProvinceId => TravelAvailableCommand.ActingProvinceId;
|
||||
|
||||
protected override void SetUpUI() {}
|
||||
|
||||
public override SelectedCommand Command =>
|
||||
new SelectedCommand { TravelCommand = new TravelSelectedCommand() };
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using Net.Eagle0.Eagle.Api;
|
||||
|
||||
namespace eagle {
|
||||
using ProvinceId = Int32;
|
||||
|
||||
public class VisitTownCommandSelector : CommandSelector {
|
||||
public override AvailableCommand.SealedValueOneofCase CommandType =>
|
||||
AvailableCommand.SealedValueOneofCase.VisitTownCommand;
|
||||
public override string HeaderString => "Visit Town";
|
||||
public override string CommitButtonString => "Commit Visit Town";
|
||||
|
||||
private VisitTownAvailableCommand VisitTownAvailableCommand =>
|
||||
_availableCommand.VisitTownCommand;
|
||||
private ProvinceId ActingProvinceId => VisitTownAvailableCommand.ActingProvinceId;
|
||||
|
||||
protected override void SetUpUI() {}
|
||||
|
||||
public override SelectedCommand Command =>
|
||||
new SelectedCommand { VisitTownCommand = new VisitTownSelectedCommand() };
|
||||
}
|
||||
}
|
||||
@@ -108,6 +108,21 @@ namespace eagle {
|
||||
}
|
||||
|
||||
private void UpdateDisplay() {
|
||||
// Authentication failures are actionable by the player and must not be hidden by
|
||||
// a circuit breaker that may have opened during an earlier network failure.
|
||||
var state = _connection.CurrentState;
|
||||
if (state == ConnectionState.RefreshingAuthentication) {
|
||||
SetRetryButtonVisible(false);
|
||||
statusText.text = "<color=yellow>●</color> Refreshing session...";
|
||||
return;
|
||||
}
|
||||
if (state == ConnectionState.AuthenticationRequired) {
|
||||
SetRetryButtonVisible(false);
|
||||
statusText.text =
|
||||
"<color=red>●</color> Session expired. Return to lobby to sign in again.";
|
||||
return;
|
||||
}
|
||||
|
||||
// Check circuit breaker state first - it takes precedence
|
||||
var circuitState = _connection.CircuitBreaker.CurrentState;
|
||||
if (circuitState == ConnectionCircuitBreaker.State.Open) {
|
||||
@@ -130,7 +145,6 @@ namespace eagle {
|
||||
}
|
||||
|
||||
// Normal connection state display
|
||||
var state = _connection.CurrentState;
|
||||
var nextAttempt = _connection.NextReconnectAttempt;
|
||||
|
||||
// Show retry button if we're counting down to a reconnect attempt
|
||||
|
||||
@@ -189,6 +189,7 @@ namespace eagle {
|
||||
case SealedValueOneofCase.SwearBrotherhoodWithHeroQuest: return "Swear Brotherhood";
|
||||
case SealedValueOneofCase.BetrayAllyQuest: return "Betray Ally";
|
||||
case SealedValueOneofCase.BorderSecurityQuest: return "Border Security";
|
||||
case SealedValueOneofCase.FocusProvinceQuest: return "Focus Province";
|
||||
case SealedValueOneofCase.WinBattleOutnumberedQuest:
|
||||
return "Win Battle Outnumbered";
|
||||
case SealedValueOneofCase.WinBattlesQuest: return "Win Battles";
|
||||
|
||||
+87
-54
@@ -351,9 +351,11 @@ namespace eagle {
|
||||
TutorialManager.Instance?.Initialize(this, null);
|
||||
// Register command panel for tutorial positioning
|
||||
if (TutorialManager.Instance?.TargetRegistry != null && commandPanel != null) {
|
||||
TutorialManager.Instance.TargetRegistry.RegisterTarget(
|
||||
"CommandPanel",
|
||||
commandPanel.GetComponent<RectTransform>());
|
||||
var registry = TutorialManager.Instance.TargetRegistry;
|
||||
registry.RegisterTarget("CommandPanel", commandPanel.GetComponent<RectTransform>());
|
||||
registry.RegisterTarget(
|
||||
"InfoButton",
|
||||
_commandPanelController.infoButton.GetComponent<RectTransform>());
|
||||
}
|
||||
// Register province info panel fields for dialogue highlight
|
||||
if (TutorialManager.Instance?.TargetRegistry != null &&
|
||||
@@ -434,10 +436,10 @@ namespace eagle {
|
||||
Model.AvailableCommandsByProvince.Count > 0);
|
||||
}
|
||||
|
||||
public void ProvinceWasSelected(ProvinceId? pid) {
|
||||
public void ProvinceWasSelected(ProvinceId? pid, bool selectRecommendedCommand = true) {
|
||||
if (Model == null) { return; }
|
||||
if (pid.HasValue) {
|
||||
SelectRecommendedCommand(pid.Value);
|
||||
if (selectRecommendedCommand) { SelectRecommendedCommand(pid.Value); }
|
||||
var currentProvince = Model.Provinces[pid.Value];
|
||||
provinceInfoPanelController.Province = currentProvince;
|
||||
freeHeroesTableController.CurrentProvince = currentProvince;
|
||||
@@ -447,7 +449,7 @@ namespace eagle {
|
||||
factionsTableControllerWide.CurrentProvince = currentProvince;
|
||||
heroesAndBattalionsPanelController.CurrentProvince = currentProvince;
|
||||
} else {
|
||||
SelectRecommendedCommand(null);
|
||||
if (selectRecommendedCommand) { SelectRecommendedCommand(null); }
|
||||
provinceInfoPanelController.Province = null;
|
||||
freeHeroesTableController.CurrentProvince = null;
|
||||
movingArmiesTableControllerNarrow.CurrentProvince = null;
|
||||
@@ -492,8 +494,8 @@ namespace eagle {
|
||||
if (SelectedProvince is ProvinceId pid) {
|
||||
var province = Model.Provinces[pid];
|
||||
if (province.RulingFactionId is FactionId fid && fid == Model.PlayerId &&
|
||||
province.FullInfo.RulerIsTraveling) {
|
||||
soundManager.CurrentMusicType = SoundManager.MusicType.MusicTraveling;
|
||||
province.FullInfo.RulerIsVisitingTown) {
|
||||
soundManager.CurrentMusicType = SoundManager.MusicType.MusicVisitTown;
|
||||
return;
|
||||
}
|
||||
if (Model.AvailableCommandsForProvince(pid).Exists(
|
||||
@@ -584,8 +586,7 @@ namespace eagle {
|
||||
|
||||
// Preserve the user's selection across non-strategic refreshes. A result-bearing
|
||||
// strategic update must still flow through even when its command token is unchanged:
|
||||
// automatic results can change visible state and command availability without
|
||||
// advancing the posted-command token.
|
||||
// automatic results can change visible state without changing available commands.
|
||||
if (hasExistingCommands && hasIncomingCommands && !commandTokenChanged &&
|
||||
!strategicUpdateChanged) {
|
||||
mapController.UpdateBattleEffects(_newModel);
|
||||
@@ -594,8 +595,16 @@ namespace eagle {
|
||||
}
|
||||
|
||||
var oldModel = Model;
|
||||
var activeCommandStillAvailable = CommandStillAvailable(
|
||||
_newModel,
|
||||
SelectedProvince,
|
||||
_commandPanelController.AvailableCommand);
|
||||
Model = _newModel;
|
||||
|
||||
if (availableCommandsChanged && !commandTokenChanged) {
|
||||
LogCommandTokenInvariantViolation(oldModel, _newModel);
|
||||
}
|
||||
|
||||
provinceInfoPanelController.Model = _newModel;
|
||||
freeHeroesTableController.Model = _newModel;
|
||||
movingArmiesTableControllerNarrow.Model = _newModel;
|
||||
@@ -608,6 +617,10 @@ namespace eagle {
|
||||
dominionPanelControllerWide.Model = _newModel;
|
||||
mapController.Model = _newModel;
|
||||
|
||||
if (activeCommandStillAvailable) {
|
||||
_commandPanelController.Selector.UpdateModel(Model);
|
||||
}
|
||||
|
||||
if (Model == null) {
|
||||
battleProgressPanel.gameObject.SetActive(false);
|
||||
return;
|
||||
@@ -625,49 +638,11 @@ namespace eagle {
|
||||
|
||||
SetMusic();
|
||||
|
||||
if ((oldModel == null || commandTokenChanged || availableCommandsChanged) &&
|
||||
if (!activeCommandStillAvailable &&
|
||||
(oldModel == null || commandTokenChanged || availableCommandsChanged) &&
|
||||
Model.AvailableCommandsByProvince.Count > 0) {
|
||||
if (oldModel != null && oldModel.AvailableCommandsByProvince.Count > 0 &&
|
||||
(commandTokenChanged || availableCommandsChanged)) {
|
||||
string str = "We got replacement commands, but we already had commands. " +
|
||||
oldModel.CommandToken + " " + _newModel.CommandToken;
|
||||
|
||||
var oldProvinces = oldModel.AvailableCommandsByProvince.Keys.ToHashSet();
|
||||
var newProvinces = _newModel.AvailableCommandsByProvince.Keys.ToHashSet();
|
||||
|
||||
if (!oldProvinces.SetEquals(newProvinces)) {
|
||||
var oldNotNew = oldProvinces.Except(newProvinces).ToList();
|
||||
if (oldNotNew.Any()) {
|
||||
str += $"\nWe used to have commands for provinces {string.Join(", ", oldNotNew)} but now we don't";
|
||||
}
|
||||
|
||||
var newNotOld = newProvinces.Except(oldProvinces).ToList();
|
||||
if (newNotOld.Any()) {
|
||||
str += $"\nWe now have commands for provinces {string.Join(", ", newNotOld)} where we didn't before";
|
||||
}
|
||||
}
|
||||
foreach (var kv in oldModel.AvailableCommandsByProvince) {
|
||||
if (_newModel.AvailableCommandsByProvince.ContainsKey(kv.Key)) {
|
||||
var oldCommands = kv.Value;
|
||||
var newCommands = _newModel.AvailableCommandsByProvince[kv.Key];
|
||||
if (oldCommands.Commands.Count != newCommands.Commands.Count) {
|
||||
str += $"\nWe had {oldCommands.Commands.Count} but now we have {newCommands.Commands.Count}";
|
||||
}
|
||||
|
||||
foreach (var cmd in oldCommands.Commands) {
|
||||
if (!newCommands.Commands.Contains(cmd)) {
|
||||
str += $"\nWe had {cmd} but now we don't";
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var cmd in newCommands.Commands) {
|
||||
if (!oldCommands.Commands.Contains(cmd)) {
|
||||
str += $"\nWe have {cmd}, we didn't before";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Debug.LogWarning(str);
|
||||
ClearActiveCommandUi();
|
||||
}
|
||||
|
||||
@@ -765,12 +740,67 @@ namespace eagle {
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool
|
||||
CommandStillAvailable(IGameModel model, ProvinceId? provinceId, AvailableCommand command) {
|
||||
return model != null && provinceId is ProvinceId pid && command != null &&
|
||||
model.AvailableCommandsByProvince.TryGetValue(pid, out var commands) &&
|
||||
commands.Commands.Contains(command);
|
||||
}
|
||||
|
||||
private static void LogCommandTokenInvariantViolation(
|
||||
IGameModel oldModel,
|
||||
IGameModel newModel) {
|
||||
string str = "Available commands changed without command token advancement. Token " +
|
||||
oldModel.CommandToken;
|
||||
|
||||
var oldProvinces = oldModel.AvailableCommandsByProvince.Keys.ToHashSet();
|
||||
var newProvinces = newModel.AvailableCommandsByProvince.Keys.ToHashSet();
|
||||
|
||||
var oldNotNew = oldProvinces.Except(newProvinces).ToList();
|
||||
if (oldNotNew.Any()) {
|
||||
str += $"\nWe used to have commands for provinces {string.Join(", ", oldNotNew)} but now we don't";
|
||||
}
|
||||
|
||||
var newNotOld = newProvinces.Except(oldProvinces).ToList();
|
||||
if (newNotOld.Any()) {
|
||||
str += $"\nWe now have commands for provinces {string.Join(", ", newNotOld)} where we didn't before";
|
||||
}
|
||||
|
||||
foreach (var kv in oldModel.AvailableCommandsByProvince) {
|
||||
if (!newModel.AvailableCommandsByProvince.TryGetValue(
|
||||
kv.Key,
|
||||
out var newCommands)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var oldCommands = kv.Value;
|
||||
if (oldCommands.Commands.Count != newCommands.Commands.Count) {
|
||||
str += $"\nWe had {oldCommands.Commands.Count} but now we have {newCommands.Commands.Count}";
|
||||
}
|
||||
|
||||
foreach (var cmd in oldCommands.Commands) {
|
||||
if (!newCommands.Commands.Contains(cmd)) {
|
||||
str += $"\nWe had {cmd} but now we don't";
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var cmd in newCommands.Commands) {
|
||||
if (!oldCommands.Commands.Contains(cmd)) {
|
||||
str += $"\nWe have {cmd}, we didn't before";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Debug.LogError(str);
|
||||
}
|
||||
|
||||
private void UpdateBattleProgressDisplay(IGameModel model) {
|
||||
if (model.ShardokBattles.Any()) {
|
||||
battleProgressPanel.gameObject.SetActive(true);
|
||||
var hasBattles = model.ShardokBattles.Any();
|
||||
commandPanel.SetActive(!hasBattles);
|
||||
battleProgressPanel.gameObject.SetActive(hasBattles);
|
||||
|
||||
if (hasBattles) {
|
||||
battleProgressPanel.UpdateBattles(model.ShardokBattles, model, model.BattleDay);
|
||||
} else {
|
||||
battleProgressPanel.gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -782,6 +812,7 @@ namespace eagle {
|
||||
}
|
||||
|
||||
private void PostCommittedCommand(ProvinceId provinceId, SelectedCommand selectedCommand) {
|
||||
TutorialManager.Instance?.DialogueManager?.OnPlayerAction();
|
||||
ModelUpdater.PostCommand(provinceId: provinceId, command: selectedCommand)
|
||||
.ContinueWith(response => {
|
||||
if (response.IsFaulted) { errorHandler.Add(response.Exception); }
|
||||
@@ -813,6 +844,7 @@ namespace eagle {
|
||||
public async void GoToBattle() {
|
||||
// End any active dialogue before transitioning to battle
|
||||
TutorialManager.Instance?.DialogueManager?.EndDialogueWithoutCompletion();
|
||||
TutorialManager.Instance?.DialogueManager?.OnPlayerAction();
|
||||
|
||||
if (Model == null) { throw new InvalidOperationException("GoToBattle: Model is null"); }
|
||||
|
||||
@@ -841,6 +873,7 @@ namespace eagle {
|
||||
|
||||
public async void GoToBattle(string shardokGameId) {
|
||||
TutorialManager.Instance?.DialogueManager?.EndDialogueWithoutCompletion();
|
||||
TutorialManager.Instance?.DialogueManager?.OnPlayerAction();
|
||||
battleProgressPanel.gameObject.SetActive(false);
|
||||
|
||||
if (Model == null) { throw new InvalidOperationException("GoToBattle: Model is null"); }
|
||||
|
||||
@@ -127,7 +127,8 @@ namespace eagle {
|
||||
.Select(kv => new IClientConnectionSubscriber.StreamingTextStatus {
|
||||
llmIdentifier = kv.Key,
|
||||
knownByteCount = Encoding.UTF8.GetBytes(kv.Value.Text).Length,
|
||||
knownComplete = kv.Value.Completed
|
||||
knownComplete = kv.Value.Completed,
|
||||
streamAttemptId = kv.Value.StreamAttemptId
|
||||
})
|
||||
.ToList();
|
||||
|
||||
@@ -1026,8 +1027,8 @@ namespace eagle {
|
||||
|
||||
if (nfi.Support != null) province.FullInfo.Support = nfi.Support;
|
||||
|
||||
if (nfi.SetRulerIsTraveling is {} newRulerIsTraveling)
|
||||
province.FullInfo.RulerIsTraveling = newRulerIsTraveling;
|
||||
if (nfi.SetRulerIsVisitingTown is {} newRulerIsVisitingTown)
|
||||
province.FullInfo.RulerIsVisitingTown = newRulerIsVisitingTown;
|
||||
|
||||
if (nfi.NewProvinceOrders != null)
|
||||
province.FullInfo.ProvinceOrders = nfi.NewProvinceOrders.Orders;
|
||||
@@ -1097,6 +1098,10 @@ namespace eagle {
|
||||
|
||||
if (entry.NewDate is {} newDate) {
|
||||
_currentModel.GsView.CurrentDate = newDate;
|
||||
foreach (var shardokGameId in _currentModel.ShardokGameModels.Keys) {
|
||||
_shardokResultCounts.TryRemove(shardokGameId, out _);
|
||||
_shardokNeedsResync.TryRemove(shardokGameId, out _);
|
||||
}
|
||||
_currentModel.ShardokGameModels.Clear();
|
||||
}
|
||||
|
||||
@@ -1135,9 +1140,14 @@ namespace eagle {
|
||||
// the Eagle update removing the battle arrives before the Shardok Victory update.
|
||||
if (_currentModel.ShardokGameModels.TryGetValue(rb, out var sgm)) {
|
||||
sgm.MarkBattleEnded("Battle has ended.");
|
||||
_currentModel.ShardokGameModels.TryRemove(rb, out _);
|
||||
// Keep the model and its result count until the normal date-change cleanup.
|
||||
// A final Shardok result can still be queued behind this Eagle update. It
|
||||
// must reach the existing model so the last action and unit removals appear
|
||||
// before the player exits the battle.
|
||||
} else {
|
||||
_shardokResultCounts.TryRemove(rb, out _);
|
||||
_shardokNeedsResync.TryRemove(rb, out _);
|
||||
}
|
||||
_shardokResultCounts.TryRemove(rb, out _);
|
||||
|
||||
for (int i = 0; i < _currentModel.ShardokBattles.Count; i++) {
|
||||
if (_currentModel.ShardokBattles[i].ShardokGameId == rb) {
|
||||
@@ -1170,7 +1180,7 @@ namespace eagle {
|
||||
_currentModel.HistoryCount++;
|
||||
|
||||
MaybeSendNotification(arv, invitationAcceptedFactionIds);
|
||||
TutorialManager.Instance?.OnStrategicActionResult(arv);
|
||||
TutorialManager.Instance?.OnStrategicActionResult(GameId, arv);
|
||||
ApplyGameStateViewDiff(arv.GameStateDiff);
|
||||
ActionResultAction?.Invoke(arv);
|
||||
}
|
||||
@@ -1182,7 +1192,8 @@ namespace eagle {
|
||||
str.LlmIdentifier,
|
||||
str.NewText,
|
||||
str.StartingByteCount,
|
||||
str.Completed);
|
||||
str.Completed,
|
||||
str.StreamAttemptId);
|
||||
if (str.Completed) {
|
||||
ClientTextDiskCache.SaveCompletedText(GameId, str.LlmIdentifier, updatedText);
|
||||
}
|
||||
|
||||
+18
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using eagle;
|
||||
using NUnit.Framework;
|
||||
@@ -50,6 +51,23 @@ namespace eagle0.Tests {
|
||||
"ProvinceWeatherController should update the project weather map material.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void MapAnimationRunsAfterModelUpdates() {
|
||||
Assert.Greater(
|
||||
ExecutionOrderFor(typeof(MapController)),
|
||||
ExecutionOrderFor(typeof(EagleGameController)),
|
||||
"MapController must animate province colors after EagleGameController can " +
|
||||
"replace the model and reset their base colors.");
|
||||
}
|
||||
|
||||
private static int ExecutionOrderFor(Type componentType) {
|
||||
var attribute =
|
||||
componentType.GetCustomAttributes(typeof(DefaultExecutionOrder), inherit: false)
|
||||
.Cast<DefaultExecutionOrder>()
|
||||
.SingleOrDefault();
|
||||
return attribute?.order ?? 0;
|
||||
}
|
||||
|
||||
private static Material LoadMaterial(string path, string expectedShaderName) {
|
||||
var material = AssetDatabase.LoadAssetAtPath<Material>(path);
|
||||
Assert.NotNull(material, $"{path} must exist.");
|
||||
|
||||
+11
@@ -193,6 +193,17 @@ namespace eagle {
|
||||
newRow.Battalion = battalion;
|
||||
SetBattalionRowSelections(newRow, battalion);
|
||||
});
|
||||
|
||||
var registry = TutorialManager.Instance?.TargetRegistry;
|
||||
if (registry != null) {
|
||||
registry.RegisterTargets(
|
||||
"BattalionArmamentValues",
|
||||
Enumerable.Range(0, battalionsTable.RowCount)
|
||||
.Select(index => battalionsTable
|
||||
.ComponentAt<BattalionRowController>(index)
|
||||
.armamentLabel
|
||||
.GetComponent<RectTransform>()));
|
||||
}
|
||||
}
|
||||
|
||||
void Start() {
|
||||
|
||||
+3
-1
@@ -38,9 +38,11 @@ namespace eagle {
|
||||
public Int32 knownByteCount;
|
||||
|
||||
public Boolean knownComplete;
|
||||
|
||||
public string streamAttemptId;
|
||||
}
|
||||
public int LastUnfilteredResultCount { get; }
|
||||
public List<ShardokViewStatus> ShardokViewStatuses { get; }
|
||||
public List<StreamingTextStatus> StreamingTextStatuses { get; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,9 @@ using ProvinceId = System.Int32;
|
||||
using FactionId = System.Int32;
|
||||
|
||||
namespace eagle {
|
||||
// Model and highlight state are updated by other controllers in Update. Run afterward so an
|
||||
// incoming model cannot overwrite the animated province colors before they are rendered.
|
||||
[DefaultExecutionOrder(100)]
|
||||
public class MapController : MonoBehaviour {
|
||||
public EagleGameController eagleGameController;
|
||||
|
||||
@@ -124,7 +127,8 @@ namespace eagle {
|
||||
|
||||
UpdateProvinceEffects(value);
|
||||
|
||||
eagleGameController.ProvinceWasSelected(_selectedProvinceId);
|
||||
eagleGameController
|
||||
.ProvinceWasSelected(_selectedProvinceId, selectRecommendedCommand: false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -163,7 +163,9 @@ namespace eagle {
|
||||
newRow.HeroCount = army.HeroCount;
|
||||
newRow.TroopCount = army.TroopCount;
|
||||
newRow.Origin = Model.Provinces[army.OriginProvinceId].Name;
|
||||
newRow.Destination = Model.Provinces[army.DestinationProvinceId].Name;
|
||||
var destination = Model.Provinces[army.DestinationProvinceId].Name;
|
||||
newRow.Destination =
|
||||
army.IsRetreating ? $"{destination} (Retreating)" : destination;
|
||||
if (CurrentProvince != null && army.DestinationProvinceId == CurrentProvince.Id) {
|
||||
newRow.DestinationColor = Color.blue;
|
||||
} else {
|
||||
|
||||
+1
@@ -41,6 +41,7 @@ namespace eagle.Notifications {
|
||||
{ PrisonerReleasedDetails, PrisonerReleasedDetailsNotificationGenerator.Generator },
|
||||
{ PrisonerExiledDetails, PrisonerExiledDetailsNotificationGenerator.Generator },
|
||||
{ PrisonerReturnedDetails, PrisonerReturnedDetailsNotificationGenerator.Generator },
|
||||
{ ProvinceConqueredDetails, ProvinceConqueredDetailsNotificationGenerator.Generator },
|
||||
{ ProvinceHeldDetails, ProvinceHeldDetailsNotificationGenerator.Generator },
|
||||
{ QuestFailed, QuestFailedDetailsNotificationGenerator.Generator },
|
||||
{ QuestFulfilled, QuestFulfilledDetailsNotificationGenerator.Generator },
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
using System.Collections.Generic;
|
||||
using Net.Eagle0.Eagle.Views;
|
||||
|
||||
namespace eagle.Notifications.ARNNotifications {
|
||||
public static class ProvinceConqueredDetailsNotificationGenerator {
|
||||
public static readonly ARNNotificationGenerator Generator = GenerateNotification;
|
||||
|
||||
private static IEnumerable<Notification> GenerateNotification(
|
||||
Net.Eagle0.Eagle.Common.Notification notification,
|
||||
IGameModel currentModel) {
|
||||
var playerId = currentModel.PlayerId;
|
||||
var details = notification.Details.ProvinceConqueredDetails;
|
||||
var province = currentModel.Provinces[details.ProvinceId];
|
||||
var winner = currentModel.MaybeDestroyedFaction(details.ConqueringFactionId);
|
||||
var loser = currentModel.MaybeDestroyedFaction(details.DefendingFactionId);
|
||||
|
||||
var winnerHead = currentModel.Heroes[winner.FactionHeadId];
|
||||
var loserHead = currentModel.Heroes[loser.FactionHeadId];
|
||||
|
||||
if (!playerId.HasValue || (playerId.Value != winner.Id && playerId.Value != loser.Id)) {
|
||||
yield return new Notification(
|
||||
title: "Province Conquered",
|
||||
text: $"{winner.Name} has conquered {province.Name} from {loser.Name}.",
|
||||
singleProvinceId: details.ProvinceId,
|
||||
displayedHeroes: new List<HeroView> { winnerHead, loserHead });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-2
@@ -16,7 +16,6 @@ namespace eagle.Notifications {
|
||||
{ NewRoundAction, NewRoundNotificationGenerator.Generator },
|
||||
{ PrisonerEscaped, PrisonerEscapedNotificationGenerator.Generator },
|
||||
{ PrisonerReturnTookEffect, PrisonerReturnedNotificationGenerator.Generator },
|
||||
{ ProvinceConquered, ProvinceConqueredNotificationGenerator.Generator },
|
||||
{ ProvinceEventsChanged, ProvinceEventsNotificationGenerator.Generator },
|
||||
{ RiotAverted, RiotAvertedNotificationGenerator.Generator },
|
||||
{ RiotOccurred, RiotOccurredNotificationGenerator.Generator },
|
||||
@@ -43,4 +42,4 @@ namespace eagle.Notifications {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user