Compare commits

...
Author SHA1 Message Date
adminandGitHub 676598b510 Add prisoner interrogation interface (#8245)
* Add prisoner interrogation interface

* Wire prisoner interrogation through command interfaces

* Add target province to interrogation interface
2026-07-28 21:54:46 -07:00
adminandGitHub c092002677 Route CI LFS through Gitea (#8812)
* Route CI LFS through Gitea

* Add fresh Gitea LFS verification mode

* Expose fresh LFS verification failures

* Avoid duplicate Gitea LFS auth headers
2026-07-28 11:44:41 -07:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
4162635431 Update dependency rules_rust to v0.72.0 (#8811)
* Update dependency rules_rust to v0.72.0

* Update generated dependency lockfiles

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-27 21:50:15 -07:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>admin
d8e2e887c0 Update dependency aspect_rules_js to v3.3.0 (#8808)
* Update dependency aspect_rules_js to v3.3.0

* Update aspect_rules_js override and lockfile

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Dan Crosby <dan@danielcrosby.net>
2026-07-27 21:49:59 -07:00
renovate[bot]andGitHub c6336979c8 Update dependency rules_jvm_external to v7.1 (#8810) 2026-07-27 20:31:53 -07:00
renovate[bot]andGitHub a7f630d89c Update dependency grpc to v1.83.0 (#8805) 2026-07-27 20:31:09 -07:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
fe18ffe9a3 Update dependency rules_go to v0.62.0 (#8809)
* Update dependency rules_go to v0.62.0

* Update generated dependency lockfiles

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-27 18:27:55 -07:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
f7ee55b888 Update dependency rules_distroless to v0.8.0 (#8807)
* Update dependency rules_distroless to v0.8.0

* Update generated dependency lockfiles

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-27 14:04:00 -07:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
d94f731c50 Update dependency io.sentry:sentry to v8.50.1 (#8806)
* Update dependency io.sentry:sentry to v8.50.1

* Update generated dependency lockfiles

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-27 14:03:46 -07:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
791d0b9b29 Update dependency gazelle to v0.52.2 (#8804)
* Update dependency gazelle to v0.52.2

* Update generated dependency lockfiles

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-27 06:38:25 -07:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
80417fd3bf Update dependency org.scalameta:scalafmt-cli_2.13 to v3.11.5 (#8803)
* Update dependency org.scalameta:scalafmt-cli_2.13 to v3.11.5

* Update generated dependency lockfiles

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-27 06:38:13 -07:00
renovate[bot]andGitHub 4c25b16cf1 Lock file maintenance (#8802) 2026-07-27 06:22:21 -07:00
renovate[bot]andGitHub 6165bb695e Update dependency apple_support to v2.8.0 (#8801) 2026-07-27 06:21:59 -07:00
renovate[bot]andGitHub 65501b046e Update aws-sdk-go-v2 monorepo (#8800) 2026-07-27 06:21:31 -07:00
renovate[bot]andGitHub 0419dc23fc Lock file maintenance (#8798) 2026-07-27 04:45:11 -07:00
renovate[bot]andGitHub 4df0e75137 Update dependency org.xerial:sqlite-jdbc to v3.53.2.1 (#8797) 2026-07-27 04:44:52 -07:00
renovate[bot]andGitHub c35126c6d1 Update dependency bazel_skylib to v1.9.2 (#8796) 2026-07-27 04:44:24 -07:00
adminandGitHub 809c1e256a Add encrypted offsite backups for Gitea (#8795)
* Add encrypted Gitea offsite backups

* Avoid Homebrew cleanup in Gitea backup

* Use dedicated Spaces credentials for Gitea backups

* Authenticate Gitea backups with a dedicated SSH key

* Use SCP for Synology Gitea backups

* Skip regenerable unreadable Gitea state

* Finalize scheduled Gitea backup workflow

* Group Gitea backup retention across runners
2026-07-26 21:23:17 -07:00
adminandGitHub a7990f65aa Cache player roles during AI scoring (#8793)
* Cache player roles during AI scoring

* Use dense player IDs for AI scoring
2026-07-26 07:28:38 -07:00
adminandGitHub 39a374fc42 Increase AI worker cap to twelve (#8794) 2026-07-26 07:04:53 -07:00
adminandGitHub 0f72494fef Use occupancy fast path for adjacent enemies (#8792) 2026-07-26 07:04:39 -07:00
adminandGitHub 9dba9deaa1 Capture Shardok CPU profiles with Linux perf (#8791) 2026-07-25 21:44:07 -07:00
adminandGitHub 6ff2a91417 Keep custom battle open during lobby updates (#8790) 2026-07-25 19:39:54 -07:00
adminandGitHub 3add561a82 Reuse effective destination connectivity checks (#8789) 2026-07-25 19:29:17 -07:00
adminandGitHub a7c8cb11fc Preserve ARM64 leaf frame pointers for profiling (#8788) 2026-07-25 19:28:48 -07:00
adminandGitHub 865d974a51 Scale hero cap by infrastructure (#8787) 2026-07-25 18:38:11 -07:00
adminandGitHub 4898a1fe80 Balance moving army province columns (#8744)
* Balance moving army province columns

* Let moving army destination use remaining width
2026-07-25 18:19:47 -07:00
adminandGitHub 3b819db9ee Use frame-pointer unwinding for Shardok profiles (#8786) 2026-07-25 18:16:05 -07:00
adminandGitHub 154702ad35 Account for bridges in AI land regions (#8785) 2026-07-25 11:53:55 -07:00
adminandGitHub 98eb64749f Reset custom battle armies on entry (#8783) 2026-07-25 08:58:43 -07:00
adminandGitHub 067aadfec1 Reduce water-crossing analysis overhead (#8784) 2026-07-25 08:48:24 -07:00
adminandGitHub 80d17ee750 Fix cached Shardok bridge path checks (#8782) 2026-07-25 08:02:25 -07:00
adminandGitHub 4e59580d5e Start Shardok profiling without confirmation (#8781) 2026-07-24 22:30:37 -07:00
adminandGitHub 4950bd3609 Skip exact water checks across static regions (#8780) 2026-07-24 22:27:17 -07:00
adminandGitHub f913116132 Short-circuit defender water-crossing checks (#8779) 2026-07-24 14:01:12 -07:00
adminandGitHub 82fc200d51 Skip unnecessary defender water-crossing analysis (#8778) 2026-07-24 11:39:21 -07:00
adminandGitHub af1e467cb8 Precompute land regions for water crossing (#8777) 2026-07-24 11:04:43 -07:00
adminandGitHub c7d5f36ad5 Make CoordsSet a forward range (#8776) 2026-07-24 10:39:15 -07:00
adminandGitHub 78e5d25b89 Avoid copying fire-free maps for water crossing (#8775) 2026-07-24 10:12:00 -07:00
adminandGitHub f672126a00 Avoid allocations while interpolating cube lines (#8773) 2026-07-24 09:24:34 -07:00
adminandGitHub 4ff32ec0b2 Offer Shardok profile download on stop (#8772) 2026-07-24 09:17:47 -07:00
adminandGitHub ecc61c7d2c Add cube line equivalence tests (#8771) 2026-07-24 09:17:23 -07:00
adminandGitHub 49185c103a Run Shardok profiles until stopped (#8770) 2026-07-24 09:05:05 -07:00
adminandGitHub 71a6c9f791 Hoist repeated APD score lookups (#8769) 2026-07-24 08:29:29 -07:00
adminandGitHub 6a91935a1f Recompute fire-cleared map hash (#8768) 2026-07-24 07:38:38 -07:00
adminandGitHub a148a8a2a3 Lower high loyalty display threshold (#8767) 2026-07-24 07:14:59 -07:00
adminandGitHub a588c2dd9c Combine castles and infrastructure for hero cap (#8766) 2026-07-23 22:18:15 -07:00
adminandGitHub 6ae78186e2 Rebuild inputs lost from remote cache (#8765) 2026-07-23 22:17:36 -07:00
adminandGitHub a9e51e619d Bound iterative-deepening AI worker threads (#8764) 2026-07-23 22:17:13 -07:00
adminandGitHub 912f53c42d Instrument Shardok command latency in Eagle (#8763) 2026-07-23 21:01:43 -07:00
adminandGitHub e371368e0b Stop printing AI transposition stats after searches (#8762) 2026-07-23 21:01:24 -07:00
adminandGitHub 9fa1cb9e5d Color province commander counts independently (#8761) 2026-07-23 21:00:56 -07:00
adminandGitHub 16a52d5557 Show hero cap in province commander count (#8758)
* Show hero cap in province commander count

* Keep province commander count on one line
2026-07-23 13:16:39 -07:00
adminandGitHub 2641be9c91 Include Linux runtime binaries in Shardok profiles (#8760) 2026-07-23 13:16:04 -07:00
adminandGitHub 132cf36673 Reduce available command assembly overhead (#8759) 2026-07-23 13:14:57 -07:00
adminandGitHub 9b42eb90a0 Run dropped game cleanup in background (#8756) 2026-07-23 12:39:21 -07:00
adminandGitHub e07b077b7f Base province hero cap on infrastructure (#8757) 2026-07-23 12:38:50 -07:00
adminandGitHub 0766b63b9e Add Shardok profiling admin controls (#8755) 2026-07-23 07:38:51 -07:00
adminandGitHub 46400dd6e1 Add Shardok profiling admin backend (#8754) 2026-07-23 07:15:34 -07:00
adminandGitHub 85b2fe39aa Add authenticated Shardok CPU profiling RPCs (#8753)
* Add admin-controlled Shardok CPU profiling

* Skip gperftools sampling under ASan

* Limit profiling PR to Shardok service
2026-07-23 07:10:22 -07:00
adminandGitHub 5e9109a523 Enable Renovate Unity editor updates (#8752) 2026-07-22 08:01:20 -07:00
adminandGitHub 2785015212 Build production Shardok optimized and profileable (#8750) 2026-07-21 22:22:54 -07:00
adminandGitHub e5be07b3b8 Record Shardok client command arrivals in JFR (#8749) 2026-07-21 09:18:34 -07:00
adminandGitHub 68873cc219 Animate enemy flee results (#8748) 2026-07-21 09:18:15 -07:00
adminandGitHub 54283cbeb1 Prevent remote marches from acted provinces (#8743)
* Prevent remote marches from acted provinces

* Clear vassal action flags before player commands

* Preserve leader action flags after vassal phase
2026-07-20 21:50:46 -07:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
d30dd07c28 Lock file maintenance (#8747)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-20 21:50:20 -07:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
34c3b244fd Update actions/setup-go action to v7 (#8746)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-20 21:50:09 -07:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
07163dec15 Update actions/setup-dotnet action to v6 (#8745)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-20 21:49:58 -07:00
adminandGitHub c7c7167b6a Label strategically retreating armies in battle progress (#8707)
* Label strategically retreating battle armies

* Show strategic retreats in initial battle progress
2026-07-20 21:49:25 -07:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
e6bac582b1 Update dependency rules_java to v9.7.0 (#8742)
* Update dependency rules_java to v9.7.0

* Update generated dependency lockfiles

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-20 11:48:19 -07:00
adminandGitHub e406229055 Make AI attack marches more aggressive (#8738)
* Make AI attack marches more aggressive

* Test aggressive AI march sequence
2026-07-20 11:42:38 -07:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
b0d1a8181b Update dependency io.sentry:sentry to v8.49.0 (#8741)
* Update dependency io.sentry:sentry to v8.49.0

* Update generated dependency lockfiles

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-20 11:41:57 -07:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
422abdb208 Update Rust crate serde_json to v1.0.151 (#8740)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-20 11:38:57 -07:00
adminandGitHub b3614f70b8 Skip no-op client text transactions (#8737) 2026-07-20 11:33:43 -07:00
adminandGitHub 18bcc871e5 Prevent short moving army origins from wrapping (#8736) 2026-07-20 08:47:45 -07:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2b6d5e8258 Update Rust crate serde to v1.0.229 (#8735)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-20 08:23:56 -07:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
62d8cc08ab Update Rust crate clap to v4.6.3 (#8734)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-20 08:22:54 -07:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
3bb4cd84dc Update module google.golang.org/grpc to v1.82.1 (#8733)
* Update module google.golang.org/grpc to v1.82.1

* Update generated Go artifacts

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-20 08:22:40 -07:00
adminandGitHub d17bb209a5 Instrument Postgres history append phases (#8732) 2026-07-20 08:16:19 -07:00
adminandGitHub 20bf47ab01 Batch generated text request writes (#8731) 2026-07-20 07:54:04 -07:00
adminandGitHub 741bc690fb Avoid state replay for chronicle events (#8730) 2026-07-20 07:42:07 -07:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
bedc104d0b Update module github.com/aws/aws-sdk-go-v2/service/s3 to v1.105.2 (#8729)
* Update module github.com/aws/aws-sdk-go-v2/service/s3 to v1.105.2

* Update generated Go artifacts

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-20 07:26:40 -07:00
adminandGitHub 6410ad2eef Avoid PostgreSQL for empty history projections (#8728) 2026-07-20 07:22:02 -07:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
8164e91125 Update dependency org.scalameta:scalafmt-cli_2.13 to v3.11.4 (#8727)
* Update dependency org.scalameta:scalafmt-cli_2.13 to v3.11.4

* Update generated dependency lockfiles

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-20 07:20:00 -07:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
70f40aea25 Update dependency grpc to v1.82.1 (#8726)
* Update dependency grpc to v1.82.1

* Update generated dependency lockfiles

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-20 07:19:42 -07:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
aec99378ad Update eclipse_temurin_25_jre:26-jre Docker digest to 882787e (#8724)
* Update eclipse_temurin_25_jre:26-jre Docker digest to 882787e

* Update generated dependency lockfiles

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-20 07:19:28 -07:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>admin
cd2e00cd47 Update dependency aspect_rules_js to v3.2.3 (#8725)
* Update dependency aspect_rules_js to v3.2.3

* Update aspect_rules_js override and lockfile

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Dan Crosby <dan@danielcrosby.net>
2026-07-20 07:09:09 -07:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
504374291d Update eclipse_temurin_25_jdk:26-jdk Docker digest to 6258898 (#8723)
* Update eclipse_temurin_25_jdk:26-jdk Docker digest to 6258898

* Update generated dependency lockfiles

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-20 07:08:31 -07:00
adminandGitHub 6b052bf3ad Instrument post-action processing phases (#8722) 2026-07-20 06:45:21 -07:00
adminandGitHub 0e1536842e Cache Postgres client text state in Eagle (#8721) 2026-07-19 22:45:26 -07:00
adminandGitHub c6b73d24be Avoid Postgres connection on cached state reads (#8719) 2026-07-19 12:49:07 -07:00
adminandGitHub ccde86cb81 Persist Shardok battle updates asynchronously (#8718) 2026-07-19 11:07:25 -07:00
adminandGitHub 8d54d80f4d Skip incomplete text query during battles (#8717) 2026-07-19 10:41:00 -07:00
adminandGitHub 5c1d68960b Cache live Shardok battle reads (#8715) 2026-07-19 09:28:49 -07:00
adminandGitHub 4a33d4b30f Hide command panel during battle progress (#8716) 2026-07-19 09:05:19 -07:00
adminandGitHub 5d21ee0915 Fix tribute co-attacker phase advancement (#8713) 2026-07-19 08:36:59 -07:00
adminandGitHub ce82c952c5 Disable Postgres leak tracing in production compose (#8714) 2026-07-19 08:35:02 -07:00
adminandGitHub 3b5deebb92 Batch AI client updates and saves (#8711) 2026-07-18 21:54:31 -07:00
adminandGitHub 027c5b6598 Disable Postgres leak tracing by default (#8712) 2026-07-18 21:54:00 -07:00
adminandGitHub 8a29847cc4 Fix deferred province conquest notifications (#8709) 2026-07-18 21:31:25 -07:00
adminandGitHub 33f4c41fdd Prevent command panel title flash (#8710) 2026-07-18 21:28:43 -07:00
173 changed files with 9516 additions and 2002 deletions
+3
View File
@@ -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"
+8
View File
@@ -59,6 +59,11 @@ on:
required: true
default: 'false'
type: boolean
verify_gitea_lfs:
description: 'Force LFS download from an empty temporary cache'
required: true
default: 'false'
type: boolean
# Only allow one deployment at a time; queue new ones (never cancel a running deploy).
# The build-all job checks if it's still the latest commit on main and skips if not,
@@ -134,6 +139,9 @@ jobs:
- name: Fetch LFS files needed for admin server
if: steps.check-latest.outputs.skip != 'true'
env:
GITEA_LFS_TOKEN: ${{ secrets.GITEA_LFS_TOKEN }}
GITEA_LFS_USERNAME: ${{ secrets.GITEA_LFS_USERNAME }}
GITEA_LFS_VERIFY_FRESH: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.verify_gitea_lfs == 'true' }}
GITHUB_TOKEN: ${{ github.token }}
run: ./ci/github_actions/fetch_lfs.sh --include="src/main/go/net/eagle0/admin_server/static/tiles/*"
+102
View File
@@ -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
+2
View File
@@ -154,6 +154,8 @@ jobs:
- name: Fetch LFS files
env:
GITEA_LFS_TOKEN: ${{ secrets.GITEA_LFS_TOKEN }}
GITEA_LFS_USERNAME: ${{ secrets.GITEA_LFS_USERNAME }}
GITHUB_TOKEN: ${{ github.token }}
run: ./ci/github_actions/fetch_lfs.sh
+3 -1
View File
@@ -159,6 +159,8 @@ jobs:
- name: Fetch LFS files
env:
GITEA_LFS_TOKEN: ${{ secrets.GITEA_LFS_TOKEN }}
GITEA_LFS_USERNAME: ${{ secrets.GITEA_LFS_USERNAME }}
GITHUB_TOKEN: ${{ github.token }}
run: ./ci/github_actions/fetch_lfs.sh
@@ -166,7 +168,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
+32 -2
View File
@@ -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 \
+6 -2
View File
@@ -148,12 +148,14 @@ jobs:
- name: Fetch LFS files
env:
GITEA_LFS_TOKEN: ${{ secrets.GITEA_LFS_TOKEN }}
GITEA_LFS_USERNAME: ${{ secrets.GITEA_LFS_USERNAME }}
GITHUB_TOKEN: ${{ github.token }}
run: ./ci/github_actions/fetch_lfs.sh
- name: Ensure Unity version installed
run: ./ci/github_actions/ensure_unity_installed.sh windows
- name: Ensure .NET SDK installed
uses: actions/setup-dotnet@v5
uses: actions/setup-dotnet@v6
with:
dotnet-version: '8.0.x'
- name: Ensure Bazel installed
@@ -265,12 +267,14 @@ jobs:
- name: Fetch LFS files
env:
GITEA_LFS_TOKEN: ${{ secrets.GITEA_LFS_TOKEN }}
GITEA_LFS_USERNAME: ${{ secrets.GITEA_LFS_USERNAME }}
GITHUB_TOKEN: ${{ github.token }}
run: ./ci/github_actions/fetch_lfs.sh
- name: Ensure Unity version installed
run: ./ci/github_actions/ensure_unity_installed.sh windows
- name: Ensure .NET SDK installed
uses: actions/setup-dotnet@v5
uses: actions/setup-dotnet@v6
with:
dotnet-version: '8.0.x'
- name: Ensure Bazel installed
+3
View File
@@ -0,0 +1,3 @@
[lfs]
url = https://gitea.eagle0.net/admin/eagle0-lfs.git/info/lfs
pushurl = https://gitea.eagle0.net/admin/eagle0-lfs.git/info/lfs
+25 -16
View File
@@ -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",
+66 -215
View File
@@ -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
View File
@@ -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",
+296
View File
@@ -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
+101 -20
View File
@@ -1,9 +1,9 @@
#!/bin/bash
# Fetch LFS files from Gitea mirror with retry.
# Fetch LFS files from the private Gitea LFS repository with retry.
#
# Gitea's mirror-sync API is async — the webhook fires on push but the
# actual sync may still be in progress when CI starts. Retry with backoff
# to bridge the gap.
# The primary endpoint is committed in .lfsconfig. CI supplies Gitea
# credentials through GITEA_LFS_USERNAME and GITEA_LFS_TOKEN. GitHub LFS
# remains a temporary fallback while the Gitea migration is verified.
#
# Usage:
# ./ci/github_actions/fetch_lfs.sh # full pull
@@ -16,6 +16,15 @@ export PATH="${COMMON_PATHS}:${PATH}"
MAX_ATTEMPTS=5
RETRY_DELAY=15
GITEA_LFS_URL="https://gitea.eagle0.net/admin/eagle0-lfs.git/info/lfs"
GITEA_CREDENTIAL_FILE=""
cleanup() {
if [ -n "$GITEA_CREDENTIAL_FILE" ]; then
rm -f -- "$GITEA_CREDENTIAL_FILE"
fi
}
trap cleanup EXIT
if [ -n "${GITHUB_PATH:-}" ]; then
for path in /opt/homebrew/bin /usr/local/bin; do
@@ -43,34 +52,106 @@ git lfs install --local --force
echo "LFS objects before pull:"
git lfs ls-files | wc -l
GIT_LFS_PULL=(git lfs pull)
GITEA_LFS_PULL=()
FALLBACK_GITHUB_LFS_PULL=()
GITEA_CREDENTIAL_CONFIG=()
GITEA_LFS_STORAGE_CONFIG=()
if [ "${GITEA_LFS_VERIFY_FRESH:-}" = "true" ]; then
if [ -z "${RUNNER_TEMP:-}" ]; then
echo "ERROR: RUNNER_TEMP must be set for fresh Gitea LFS verification"
exit 1
fi
MAX_ATTEMPTS=1
GITEA_LFS_STORAGE=$(mktemp -d "$RUNNER_TEMP/gitea-lfs-verification.XXXXXX")
GITEA_LFS_STORAGE_CONFIG=(-c "lfs.storage=$GITEA_LFS_STORAGE")
echo "Using empty temporary Git LFS storage for verification"
fi
if [ -n "${GITEA_LFS_USERNAME:-}" ]; then
if [ -z "${GITEA_LFS_TOKEN:-}" ]; then
echo "ERROR: GITEA_LFS_TOKEN must be set when GITEA_LFS_USERNAME is set"
exit 1
fi
elif [ -n "${GITEA_LFS_TOKEN:-}" ]; then
echo "ERROR: GITEA_LFS_USERNAME must be set when GITEA_LFS_TOKEN is set"
exit 1
fi
if [ -n "${GITEA_LFS_USERNAME:-}" ]; then
GITEA_CREDENTIAL_FILE=$(mktemp "${RUNNER_TEMP:-/tmp}/gitea-lfs-credentials.XXXXXX")
chmod 600 "$GITEA_CREDENTIAL_FILE"
git credential-store --file="$GITEA_CREDENTIAL_FILE" store <<EOF
protocol=https
host=gitea.eagle0.net
username=$GITEA_LFS_USERNAME
password=$GITEA_LFS_TOKEN
EOF
GITEA_CREDENTIAL_CONFIG=(
-c "credential.helper="
-c "credential.helper=store --file=$GITEA_CREDENTIAL_FILE"
)
GITEA_LFS_PULL=(
git
"${GITEA_CREDENTIAL_CONFIG[@]}"
-c "lfs.url=$GITEA_LFS_URL"
"${GITEA_LFS_STORAGE_CONFIG[@]}"
lfs
pull
)
elif [ "${GITHUB_ACTIONS:-}" = "true" ]; then
echo "Gitea LFS credentials are unavailable; skipping directly to the temporary GitHub fallback"
else
# Local callers can use their configured Git credential helper.
GITEA_LFS_PULL=(
git
-c "lfs.url=$GITEA_LFS_URL"
"${GITEA_LFS_STORAGE_CONFIG[@]}"
lfs
pull
)
fi
if [ -n "${GITHUB_TOKEN:-}" ]; then
BASIC_AUTH=$(printf 'x-access-token:%s' "$GITHUB_TOKEN" | base64 | tr -d '\n')
GIT_LFS_PULL=(git -c "http.https://github.com/.extraheader=AUTHORIZATION: basic $BASIC_AUTH" lfs pull)
GITHUB_BASIC_AUTH=$(printf 'x-access-token:%s' "$GITHUB_TOKEN" | base64 | tr -d '\n')
FALLBACK_GITHUB_LFS_PULL=(
git
-c "http.https://github.com/.extraheader=AUTHORIZATION: basic $BASIC_AUTH"
-c "http.https://github.com/.extraheader=AUTHORIZATION: basic $GITHUB_BASIC_AUTH"
-c "lfs.url=https://github.com/nolen777/eagle0.git/info/lfs"
lfs
pull
)
fi
for i in $(seq 1 $MAX_ATTEMPTS); do
if "${GIT_LFS_PULL[@]}" "$@"; then
echo "LFS objects after pull:"
git lfs ls-files | wc -l
exit 0
if [ "${#GITEA_LFS_PULL[@]}" -gt 0 ]; then
for i in $(seq 1 $MAX_ATTEMPTS); do
if "${GITEA_LFS_PULL[@]}" "$@"; then
echo "LFS objects after Gitea pull:"
git lfs ls-files | wc -l
exit 0
fi
if [ "$i" -lt "$MAX_ATTEMPTS" ]; then
echo "Gitea LFS pull attempt $i/$MAX_ATTEMPTS failed, retrying in ${RETRY_DELAY}s..."
sleep "$RETRY_DELAY"
fi
done
fi
if [ "${GITEA_LFS_VERIFY_FRESH:-}" = "true" ]; then
echo "Last Git LFS error log:"
if ! git lfs logs last; then
echo "No Git LFS error log was available"
fi
if [ "$i" -lt "$MAX_ATTEMPTS" ]; then
echo "LFS pull attempt $i/$MAX_ATTEMPTS failed, retrying in ${RETRY_DELAY}s..."
sleep $RETRY_DELAY
fi
done
fi
if [ "${GITEA_LFS_VERIFY_FRESH:-}" = "true" ]; then
echo "Fresh Gitea LFS verification failed; GitHub fallback is disabled"
exit 1
fi
if [ "${#FALLBACK_GITHUB_LFS_PULL[@]}" -gt 0 ]; then
echo "LFS mirror pull failed after $MAX_ATTEMPTS attempts; trying GitHub LFS fallback"
echo "Trying temporary GitHub LFS fallback"
if "${FALLBACK_GITHUB_LFS_PULL[@]}" "$@"; then
echo "LFS objects after GitHub fallback pull:"
git lfs ls-files | wc -l
@@ -78,5 +159,5 @@ if [ "${#FALLBACK_GITHUB_LFS_PULL[@]}" -gt 0 ]; then
fi
fi
echo "LFS pull failed after $MAX_ATTEMPTS attempts"
echo "LFS pull failed"
exit 1
File diff suppressed because it is too large Load Diff
+15
View File
@@ -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"
+8 -2
View File
@@ -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
+87
View File
@@ -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 -5
View File
@@ -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
)
+34
View File
@@ -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
View File
@@ -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
View File
@@ -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": [
+6
View File
@@ -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 \
"$@" \
@@ -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;
@@ -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 &currentCoords = 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;
@@ -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 {
@@ -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
@@ -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));
@@ -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(
@@ -279,6 +279,7 @@ namespace eagle {
Selector.CurrentLayoutMode = mode;
Selector.UpdateAvailableCommand(Model, AvailableCommand);
headerLabel.text = Selector.HeaderString;
RebuildHeaderLayout();
return;
}
}
@@ -298,6 +299,8 @@ namespace eagle {
if (AvailableCommand == null) {
Selector = null;
headerLabel.text = null;
UpdateCommandControls();
RebuildHeaderLayout();
return;
}
@@ -305,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(); }
@@ -314,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>
@@ -437,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);
@@ -795,11 +795,12 @@ namespace eagle {
}
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);
}
}
@@ -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 },
@@ -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 });
}
}
}
}
@@ -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 {
}
}
}
}
}
@@ -1,31 +0,0 @@
using System.Collections.Generic;
using Net.Eagle0.Eagle.Views;
namespace eagle.Notifications {
public static class ProvinceConqueredNotificationGenerator {
public static readonly NotificationGenerator Generator = GenerateNotification;
private static IEnumerable<Notification> GenerateNotification(
ActionResultView actionResultView,
IGameModel currentModel) {
var playerId = currentModel.PlayerId;
var provinceBefore = currentModel.Provinces[actionResultView.Province.Value];
if (provinceBefore.RulingFactionId.HasValue) {
var winner = currentModel.MaybeDestroyedFaction(actionResultView.Player.Value);
var loser =
currentModel.MaybeDestroyedFaction(provinceBefore.RulingFactionId.Value);
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 {provinceBefore.Name} from {loser.Name}.",
singleProvinceId: actionResultView.Province.Value,displayedHeroes: new List<HeroView> { winnerHead,loserHead });
}
}
}
}
}
@@ -5678,7 +5678,7 @@ MonoBehaviour:
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_TextWrappingMode: 1
m_TextWrappingMode: 0
m_wordWrappingRatios: 0.4
m_overflowMode: 0
m_linkedTextComponent: {fileID: 0}
@@ -187,14 +187,20 @@ namespace eagle {
var overHeroCap = fullInfo.HeroCap < heroes.Count;
var heroCountString = $"{heroes.Count}";
if (lowLoyaltyHeroes.Any() || overHeroCap) {
CommandersField.text = GUIUtils.ColoredString(Color.red, heroCountString);
if (lowLoyaltyHeroes.Any()) {
heroCountString = GUIUtils.ColoredString(Color.red, heroCountString);
} else if (heroes.Any(IsMediumLoyalty)) {
CommandersField.text = GUIUtils.ColoredString(Color.yellow, heroCountString);
} else {
CommandersField.text = heroCountString;
heroCountString = GUIUtils.ColoredString(Color.yellow, heroCountString);
}
var heroCapString = $"{fullInfo.HeroCap}";
if (overHeroCap) {
heroCapString = GUIUtils.ColoredString(Color.red, heroCapString);
} else if (heroes.Count == fullInfo.HeroCap) {
heroCapString = GUIUtils.ColoredString(Color.yellow, heroCapString);
}
CommandersField.text = $"{heroCountString} / {heroCapString}";
var tooltipReasons = new List<string>();
if (overHeroCap) { tooltipReasons.Add("Over hero cap"); }
if (lowLoyaltyHeroes.Any()) {
@@ -236,9 +236,9 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_IgnoreLayout: 0
m_MinWidth: -1
m_MinWidth: 0
m_MinHeight: -1
m_PreferredWidth: -1
m_PreferredWidth: 0
m_PreferredHeight: -1
m_FlexibleWidth: 1
m_FlexibleHeight: 1
@@ -1563,11 +1563,11 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_IgnoreLayout: 0
m_MinWidth: -1
m_MinWidth: 0
m_MinHeight: -1
m_PreferredWidth: -1
m_PreferredHeight: -1
m_FlexibleWidth: 1
m_FlexibleWidth: 0
m_FlexibleHeight: 1
m_LayoutPriority: 1
--- !u!1 &6969736396276542942
@@ -44,6 +44,77 @@ namespace eagle0.Tests {
Assert.DoesNotThrow(updater.Dispose);
}
[Test]
public void EndAftermathDeliversDeferredProvinceConqueredNotification() {
const long gameId = 7128;
const int observingFactionId = 3;
const int conqueringFactionId = 1;
const int defendingFactionId = 5;
const int conqueringHeroId = 10;
const int defendingHeroId = 50;
const int provinceId = 35;
var notifications = new List<Notification>();
var updater = new GameModelUpdater(gameId, observingFactionId, null) {
NoteRecipient = notifications.Add,
UpdateAction =
_ => {}
};
var startingState = new GameStateView { GameId = gameId };
startingState.Factions.Add(
conqueringFactionId,
new FactionView {
Id = conqueringFactionId,
FactionHeadId = conqueringHeroId,
Name = "The Eagle"
});
startingState.Factions.Add(
defendingFactionId,
new FactionView {
Id = defendingFactionId,
FactionHeadId = defendingHeroId,
Name = "Tumala's Defenders"
});
startingState.Heroes.Add(conqueringHeroId, new HeroView { Id = conqueringHeroId });
startingState.Heroes.Add(defendingHeroId, new HeroView { Id = defendingHeroId });
startingState.Provinces.Add(
provinceId,
new ProvinceView {
Id = provinceId,
Name = "Tumala",
RulingFactionId = conqueringFactionId
});
updater.ReceiveGameUpdate(new GameUpdate {
GameId = gameId,
StartingState = startingState,
ActionResultResponse = new ActionResultResponse()
});
var endAftermath = new ActionResultView { Type = ActionResultType.EndAftermathPhase };
endAftermath.Notifications.Add(new Net.Eagle0.Eagle.Common.Notification {
Details =
new Net.Eagle0.Eagle.Common.NotificationDetails {
ProvinceConqueredDetails =
new Net.Eagle0.Eagle.Common.ProvinceConqueredDetails {
ProvinceId = provinceId,
ConqueringFactionId = conqueringFactionId,
DefendingFactionId = defendingFactionId
}
}
});
var response = new ActionResultResponse { UnfilteredResultCountAfter = 1 };
response.ActionResultViews.Add(endAftermath);
updater.ReceiveGameUpdate(
new GameUpdate { GameId = gameId, ActionResultResponse = response });
Assert.AreEqual(1, notifications.Count);
Assert.AreEqual("Province Conquered", notifications[0].Title);
Assert.AreEqual(
"The Eagle has conquered Tumala from Tumala's Defenders.",
notifications[0].Text);
CollectionAssert.AreEqual(new[] { provinceId }, notifications[0].ProvinceIds);
}
[Test]
public void ResultBearingUpdateDefensivelyReplacesCommandsWhenTokenIsUnchanged() {
const long gameId = 7124;
@@ -123,7 +194,10 @@ namespace eagle0.Tests {
new Dictionary<int, string>(),
new Dictionary<int, string>());
shardokModel.UnitsById[fleeingUnitId] = new UnitViewWithName(
new Net.Eagle0.Shardok.Api.UnitView { UnitId = fleeingUnitId },
new Net.Eagle0.Shardok.Api.UnitView {
UnitId = fleeingUnitId,
Location = new Net.Eagle0.Shardok.Common.Coords { Row = 3, Column = 4 }
},
null,
null,
null);
@@ -167,6 +241,8 @@ namespace eagle0.Tests {
Assert.AreEqual(1, shardokModel.History.Count);
Assert.IsFalse(shardokModel.UnitsById.ContainsKey(fleeingUnitId));
Assert.AreEqual(3, shardokModel.ActionSourceCoords[0].Row);
Assert.AreEqual(4, shardokModel.ActionSourceCoords[0].Column);
}
private static AvailableCommands
@@ -895,16 +895,15 @@ namespace Shardok {
} else if (
isOtherPlayer && animationType != AnimationType.None &&
historyEntry.Actor != null) {
// Other player's action with animation
if (Model.UnitsById.TryGetValue(
historyEntry.Actor.Value,
out var actorUnit)) {
// For Move actions, use stored source coords (unit's pre-move location)
// For other actions, use current unit location
int sourceIndex =
Model.MoveSourceCoords.TryGetValue(i, out var sourceCoords)
? MapCoordsToGridIndex(sourceCoords)
: MapCoordsToGridIndex(actorUnit.Location);
// Other player's action with animation. Prefer the actor's position from
// before the result diff, since the result may move or remove the unit.
Model.UnitsById.TryGetValue(historyEntry.Actor.Value, out var actorUnit);
bool hasSourceCoords =
Model.ActionSourceCoords.TryGetValue(i, out var sourceCoords);
if (hasSourceCoords || actorUnit != null) {
int sourceIndex = hasSourceCoords
? MapCoordsToGridIndex(sourceCoords)
: MapCoordsToGridIndex(actorUnit.Location);
// Get target - either coords (archery) or unit location (melee)
int targetIndex = -1;
@@ -920,6 +919,14 @@ namespace Shardok {
targetIndex = MapCoordsToGridIndex(targetUnit.Location);
}
if (targetIndex < 0 && (animationType == AnimationType.FleeSuccess ||
animationType == AnimationType.FleeCaptured)) {
targetIndex = FindNearestOpponentGridIndex(
hasSourceCoords ? sourceCoords : actorUnit.Location,
historyEntry.Player.Value);
if (targetIndex < 0) { targetIndex = sourceIndex; }
}
if (targetIndex >= 0) {
if (UsesTwoStageSounds(animationType)) {
StartCoroutine(PlayTwoStageSequence(
@@ -939,8 +946,8 @@ namespace Shardok {
}
}
} else {
// Unit removed from model (e.g. fled off map) — play sound
// without animation since we can't determine coordinates
// The actor was already unavailable before this result, so its
// animation position cannot be reconstructed.
if (soundManager.IsTwoStageActionType(type)) {
StartCoroutine(PlayTwoStageSoundSequence(animationType, type));
} else {
@@ -2280,6 +2287,52 @@ namespace Shardok {
return bestIndex;
}
/// <summary>
/// Finds the nearest visible unit opposed to the acting player. Hostility values are
/// relative to the local player, so self and allied players oppose enemies and vice
/// versa. Used to give untargeted flee results a believable direction.
/// </summary>
int FindNearestOpponentGridIndex(Coords sourceCoords, PlayerId actorPlayerId) {
var actorPlayer = Model.players.Find(player => player.playerId == actorPlayerId);
bool actorPlayerKnown =
Model.players.Exists(player => player.playerId == actorPlayerId);
int bestIndex = -1;
int bestDistance = int.MaxValue;
foreach (var unit in Model.UnitsById.Values) {
if (unit.PlayerId == actorPlayerId) continue;
bool isOpponent;
if (actorPlayerKnown) {
isOpponent = AreOpposingHostilities(
actorPlayer.hostility,
(Net.Eagle0.Common.Hostility)unit.HostilityStatus);
} else {
isOpponent = true;
}
if (!isOpponent) continue;
int dist = HexDistance(sourceCoords, unit.Location);
if (dist < bestDistance) {
bestDistance = dist;
bestIndex = MapCoordsToGridIndex(unit.Location);
}
}
return bestIndex;
}
static bool AreOpposingHostilities(
Net.Eagle0.Common.Hostility first,
Net.Eagle0.Common.Hostility second) {
bool firstIsFriendly = first == Net.Eagle0.Common.Hostility.SelfHostility ||
first == Net.Eagle0.Common.Hostility.AlliedHostility;
bool secondIsFriendly = second == Net.Eagle0.Common.Hostility.SelfHostility ||
second == Net.Eagle0.Common.Hostility.AlliedHostility;
return (firstIsFriendly && second == Net.Eagle0.Common.Hostility.EnemyHostility) ||
(secondIsFriendly && first == Net.Eagle0.Common.Hostility.EnemyHostility);
}
/// <summary>
/// Public entry point for testing animations with their correct sound flow.
/// Calls PlayAnimationAndSound with no unit info, so animations that need
@@ -106,10 +106,11 @@ public class ShardokGameModel : eagle.IShardokGameStateProvider {
public List<ActionResultView> History { get; private set; }
/// <summary>
/// Tracks the source coordinates for Move actions, keyed by history index.
/// Used for animations since the model is fully updated before animations play.
/// Tracks each action actor's coordinates before its diff is applied, keyed by history index.
/// Used for animations since the model is fully updated before animations play and an action
/// may remove its actor (for example, when a unit flees or is captured).
/// </summary>
public Dictionary<int, Coords> MoveSourceCoords { get; } = new();
public Dictionary<int, Coords> ActionSourceCoords { get; } = new();
public PlayerId PlayerId { get; private set; }
public GameStatus GameStatus { get; private set; }
@@ -175,7 +176,7 @@ public class ShardokGameModel : eagle.IShardokGameStateProvider {
UnitsById.Clear();
ReserveUnitsById.Clear();
UnitPlayers.Clear();
MoveSourceCoords.Clear();
ActionSourceCoords.Clear();
AvailableCommands = new List<CommandDescriptor>();
_recommendedStartingPositionsApplied = false;
GameStatus = new GameStatus { State = GameStatus.Types.State.SetUp };
@@ -489,11 +490,14 @@ public class ShardokGameModel : eagle.IShardokGameStateProvider {
}
private void HandleNewHistoryEntry(ActionResultView entry) {
// For Move actions, record the source coordinates BEFORE applying the diff
// This is needed for animations since all diffs are applied before animations play
if (entry.Type == ActionType.Move && entry.Actor != null) {
// Record the actor's coordinates BEFORE applying the diff. All diffs are applied before
// animations play, and some results remove the actor from UnitsById entirely.
if (entry.Actor != null) {
if (UnitsById.TryGetValue(entry.Actor.Value, out var actorUnit)) {
MoveSourceCoords[History.Count] = actorUnit.Location;
ActionSourceCoords[History.Count] = new Coords {
Row = actorUnit.Location.Row,
Column = actorUnit.Location.Column
};
}
}
@@ -1,4 +1,4 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library", "go_test")
go_library(
name = "admin_server_lib",
@@ -6,6 +6,7 @@ go_library(
"admin_server.go",
"github_pr.go",
"map_editor.go",
"shardok_profile.go",
],
embedsrcs = glob([
"static/**",
@@ -15,6 +16,7 @@ go_library(
visibility = ["//visibility:private"],
deps = [
"//src/main/go/net/eagle0/util/aws",
"//src/main/protobuf/net/eagle0/common:shardok_internal_interface_go_grpc",
"//src/main/protobuf/net/eagle0/eagle/admin:game_admin_go_proto",
"//src/main/protobuf/net/eagle0/eagle/api:api_go_proto", # keep
"//src/main/protobuf/net/eagle0/eagle/api/admin:admin_go_proto",
@@ -49,3 +51,10 @@ go_binary(
"main.gitCommit": "{STABLE_GIT_COMMIT}",
},
)
go_test(
name = "admin_server_test",
srcs = ["shardok_profile_test.go"],
embed = [":admin_server_lib"],
deps = ["//src/main/protobuf/net/eagle0/common:shardok_internal_interface_go_grpc"],
)
@@ -234,6 +234,15 @@ func main() {
log.Printf("Connected to Auth server at %s", *authAddr)
}
shardokProfileConn, err := initializeShardokProfileClient()
if err != nil {
log.Fatalf("Failed to configure Shardok CPU profiling: %v", err)
}
if shardokProfileConn != nil {
defer shardokProfileConn.Close()
log.Printf("Shardok CPU profiling configured through %s", *shardokProfileAddr)
}
// Serve static files
staticContent, err := fs.Sub(staticFS, "static")
if err != nil {
@@ -279,6 +288,10 @@ func main() {
http.HandleFunc("/jfr/start", requireAuth(handleJfrStart))
http.HandleFunc("/jfr/stop", requireAuth(handleJfrStop))
http.HandleFunc("/jfr/download", requireAuth(handleJfrDownload))
http.HandleFunc("/shardok-profile/status", requireAuth(handleShardokProfileStatus))
http.HandleFunc("/shardok-profile/start", requireAuth(handleShardokProfileStart))
http.HandleFunc("/shardok-profile/stop", requireAuth(handleShardokProfileStop))
http.HandleFunc("/shardok-profile/download", requireAuth(handleShardokProfileDownload))
http.HandleFunc("/postgres-benchmark", requireAuth(handlePostgresBenchmark))
http.HandleFunc("/postgres-history-smoke", requireAuth(handlePostgresHistorySmoke))
http.HandleFunc("/server/restart", requireAuth(handleEagleRestart))
@@ -0,0 +1,266 @@
package main
import (
"archive/zip"
"context"
"encoding/json"
"flag"
"fmt"
"io"
"log"
"net/http"
"os"
"time"
shardokpb "github.com/nolen777/eagle0/src/main/protobuf/net/eagle0/common/shardok_internal_interface"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
)
var (
shardokProfileAddr = flag.String("shardok-profile-addr", "", "Shardok gRPC address for CPU profiling")
shardokProfileTLS = flag.Bool("shardok-profile-tls", true, "Use TLS for Shardok CPU profiling")
shardokProfileClient shardokpb.ShardokInternalInterfaceClient
)
type shardokProfileStatusResponse struct {
Recording bool `json:"recording"`
ProfileAvailable bool `json:"profile_available"`
StartedAtEpochMs int64 `json:"started_at_epoch_millis,omitempty"`
ProfileSizeBytes int64 `json:"profile_size_bytes,omitempty"`
SampleFrequencyHz int32 `json:"sample_frequency_hz,omitempty"`
Status string `json:"status,omitempty"`
Error string `json:"error,omitempty"`
}
var shardokProfileArtifacts = []struct {
filename string
artifactType shardokpb.CpuProfileArtifactType
}{
{
filename: "shardok.prof",
artifactType: shardokpb.CpuProfileArtifactType_CPU_PROFILE_ARTIFACT_TYPE_PROFILE,
},
{
filename: "shardok-server",
artifactType: shardokpb.CpuProfileArtifactType_CPU_PROFILE_ARTIFACT_TYPE_EXECUTABLE,
},
{
filename: "libc.so.6",
artifactType: shardokpb.CpuProfileArtifactType_CPU_PROFILE_ARTIFACT_TYPE_C_LIBRARY,
},
{
filename: "ld-linux-aarch64.so.1",
artifactType: shardokpb.CpuProfileArtifactType_CPU_PROFILE_ARTIFACT_TYPE_DYNAMIC_LINKER,
},
}
func initializeShardokProfileClient() (*grpc.ClientConn, error) {
if *shardokProfileAddr == "" {
return nil, nil
}
var transportCredentials credentials.TransportCredentials
if *shardokProfileTLS {
transportCredentials = credentials.NewClientTLSFromCert(nil, "")
} else {
transportCredentials = insecure.NewCredentials()
}
conn, err := grpc.NewClient(
*shardokProfileAddr,
grpc.WithTransportCredentials(transportCredentials),
)
if err != nil {
return nil, fmt.Errorf("connect to Shardok profiler: %w", err)
}
shardokProfileClient = shardokpb.NewShardokInternalInterfaceClient(conn)
return conn, nil
}
func shardokProfileDisabled() bool {
return shardokProfileClient == nil
}
func shardokProfileContext(parent context.Context, timeout time.Duration) (context.Context, context.CancelFunc) {
ctx, cancel := context.WithTimeout(parent, timeout)
if token := os.Getenv("SHARDOK_AUTH_TOKEN"); token != "" {
ctx = metadata.AppendToOutgoingContext(ctx, "authorization", "Bearer "+token)
}
return ctx, cancel
}
func writeShardokProfileJSON(w http.ResponseWriter, status int, response shardokProfileStatusResponse) {
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
if err := json.NewEncoder(w).Encode(response); err != nil {
log.Printf("Failed to write Shardok profile response: %v", err)
}
}
func profileStatusResponse(status *shardokpb.CpuProfileStatusResponse) shardokProfileStatusResponse {
return shardokProfileStatusResponse{
Recording: status.GetRecording(),
ProfileAvailable: status.GetProfileAvailable(),
StartedAtEpochMs: status.GetStartedAtEpochMillis(),
ProfileSizeBytes: status.GetProfileSizeBytes(),
SampleFrequencyHz: status.GetSampleFrequencyHz(),
}
}
func handleShardokProfileStatus(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
if shardokProfileDisabled() {
writeShardokProfileJSON(w, http.StatusOK, shardokProfileStatusResponse{
Status: "disabled",
})
return
}
ctx, cancel := shardokProfileContext(r.Context(), 5*time.Second)
defer cancel()
status, err := shardokProfileClient.GetCpuProfileStatus(ctx, &shardokpb.CpuProfileStatusRequest{})
if err != nil {
writeShardokProfileJSON(w, http.StatusBadGateway, shardokProfileStatusResponse{Error: err.Error()})
return
}
writeShardokProfileJSON(w, http.StatusOK, profileStatusResponse(status))
}
func handleShardokProfileStart(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
if shardokProfileDisabled() {
http.Error(w, "Shardok CPU profiling is not configured", http.StatusServiceUnavailable)
return
}
ctx, cancel := shardokProfileContext(r.Context(), 5*time.Second)
defer cancel()
status, err := shardokProfileClient.StartCpuProfile(ctx, &shardokpb.StartCpuProfileRequest{})
if err != nil {
writeShardokProfileJSON(w, http.StatusBadGateway, shardokProfileStatusResponse{Error: err.Error()})
return
}
writeShardokProfileJSON(w, http.StatusOK, profileStatusResponse(status))
}
func handleShardokProfileStop(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
if shardokProfileDisabled() {
http.Error(w, "Shardok CPU profiling is not configured", http.StatusServiceUnavailable)
return
}
ctx, cancel := shardokProfileContext(r.Context(), 5*time.Second)
defer cancel()
status, err := shardokProfileClient.StopCpuProfile(ctx, &shardokpb.StopCpuProfileRequest{})
if err != nil {
writeShardokProfileJSON(w, http.StatusBadGateway, shardokProfileStatusResponse{Error: err.Error()})
return
}
writeShardokProfileJSON(w, http.StatusOK, profileStatusResponse(status))
}
func copyShardokProfileArtifact(
ctx context.Context,
zipWriter *zip.Writer,
filename string,
artifactType shardokpb.CpuProfileArtifactType,
) error {
header := &zip.FileHeader{Name: filename, Method: zip.Store}
header.SetModTime(time.Now())
entry, err := zipWriter.CreateHeader(header)
if err != nil {
return fmt.Errorf("create %s zip entry: %w", filename, err)
}
stream, err := shardokProfileClient.DownloadCpuProfile(ctx, &shardokpb.CpuProfileDownloadRequest{
ArtifactType: artifactType,
})
if err != nil {
return fmt.Errorf("start %s download: %w", filename, err)
}
for {
chunk, receiveErr := stream.Recv()
if receiveErr == io.EOF {
return nil
}
if receiveErr != nil {
return fmt.Errorf("download %s: %w", filename, receiveErr)
}
if _, err := entry.Write(chunk.GetData()); err != nil {
return fmt.Errorf("write %s: %w", filename, err)
}
}
}
func handleShardokProfileDownload(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
if shardokProfileDisabled() {
http.Error(w, "Shardok CPU profiling is not configured", http.StatusServiceUnavailable)
return
}
ctx, cancel := shardokProfileContext(r.Context(), 5*time.Minute)
defer cancel()
status, err := shardokProfileClient.GetCpuProfileStatus(ctx, &shardokpb.CpuProfileStatusRequest{})
if err != nil {
http.Error(w, fmt.Sprintf("Failed to get Shardok profile status: %v", err), http.StatusBadGateway)
return
}
if !status.GetProfileAvailable() {
http.Error(w, "No Shardok CPU profile is available", http.StatusConflict)
return
}
timestamp := time.Now().Format("2006-01-02T15-04-05")
w.Header().Set("Content-Type", "application/zip")
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=shardok-profile-%s.zip", timestamp))
w.Header().Set("Cache-Control", "no-store")
zipWriter := zip.NewWriter(w)
defer func() {
if err := zipWriter.Close(); err != nil {
log.Printf("Failed to finish Shardok profile zip: %v", err)
}
}()
for _, artifact := range shardokProfileArtifacts {
if err := copyShardokProfileArtifact(
ctx,
zipWriter,
artifact.filename,
artifact.artifactType,
); err != nil {
log.Printf("Failed to stream Shardok CPU profile artifact %s: %v", artifact.filename, err)
return
}
}
readme, err := zipWriter.Create("README.txt")
if err != nil {
log.Printf("Failed to add Shardok profile README: %v", err)
return
}
if _, err := fmt.Fprintln(
readme,
"Analyze with: PPROF_BINARY_PATH=. go tool pprof shardok-server shardok.prof",
); err != nil {
log.Printf("Failed to write Shardok profile README: %v", err)
}
}
@@ -0,0 +1,141 @@
package main
import (
"encoding/json"
"html/template"
"net/http"
"net/http/httptest"
"strings"
"testing"
shardokpb "github.com/nolen777/eagle0/src/main/protobuf/net/eagle0/common/shardok_internal_interface"
)
func TestShardokProfileStatusResponse(t *testing.T) {
response := profileStatusResponse(&shardokpb.CpuProfileStatusResponse{
Recording: true,
ProfileAvailable: true,
StartedAtEpochMillis: 1234,
ProfileSizeBytes: 5678,
SampleFrequencyHz: 100,
})
if !response.Recording || !response.ProfileAvailable {
t.Fatalf("unexpected status flags: %+v", response)
}
if response.SampleFrequencyHz != 100 {
t.Fatalf("unexpected profile settings: %+v", response)
}
if response.StartedAtEpochMs != 1234 || response.ProfileSizeBytes != 5678 {
t.Fatalf("unexpected profile metadata: %+v", response)
}
}
func TestShardokProfileArtifactsIncludeRuntimeBinaries(t *testing.T) {
expected := map[string]shardokpb.CpuProfileArtifactType{
"shardok.prof": shardokpb.CpuProfileArtifactType_CPU_PROFILE_ARTIFACT_TYPE_PROFILE,
"shardok-server": shardokpb.CpuProfileArtifactType_CPU_PROFILE_ARTIFACT_TYPE_EXECUTABLE,
"libc.so.6": shardokpb.CpuProfileArtifactType_CPU_PROFILE_ARTIFACT_TYPE_C_LIBRARY,
"ld-linux-aarch64.so.1": shardokpb.CpuProfileArtifactType_CPU_PROFILE_ARTIFACT_TYPE_DYNAMIC_LINKER,
}
for _, artifact := range shardokProfileArtifacts {
expectedType, ok := expected[artifact.filename]
if !ok {
t.Fatalf("unexpected artifact %q", artifact.filename)
}
if artifact.artifactType != expectedType {
t.Fatalf(
"artifact %q has type %v, want %v",
artifact.filename,
artifact.artifactType,
expectedType,
)
}
delete(expected, artifact.filename)
}
if len(expected) != 0 {
t.Fatalf("missing artifacts: %v", expected)
}
}
func TestShardokProfileStatusDisabled(t *testing.T) {
previousClient := shardokProfileClient
shardokProfileClient = nil
defer func() { shardokProfileClient = previousClient }()
request := httptest.NewRequest(http.MethodGet, "/shardok-profile/status", nil)
recorder := httptest.NewRecorder()
handleShardokProfileStatus(recorder, request)
if recorder.Code != http.StatusOK {
t.Fatalf("unexpected HTTP status: %d", recorder.Code)
}
if recorder.Header().Get("Cache-Control") != "no-store" {
t.Fatalf("unexpected cache policy: %q", recorder.Header().Get("Cache-Control"))
}
var response shardokProfileStatusResponse
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
t.Fatalf("decode response: %v", err)
}
if response.Status != "disabled" {
t.Fatalf("unexpected response: %+v", response)
}
}
func TestShardokProfileEndpointsRequireExpectedMethod(t *testing.T) {
tests := []struct {
handler http.HandlerFunc
method string
}{
{handler: handleShardokProfileStatus, method: http.MethodPost},
{handler: handleShardokProfileStart, method: http.MethodGet},
{handler: handleShardokProfileStop, method: http.MethodGet},
{handler: handleShardokProfileDownload, method: http.MethodPost},
}
for _, test := range tests {
request := httptest.NewRequest(test.method, "/shardok-profile", nil)
recorder := httptest.NewRecorder()
test.handler(recorder, request)
if recorder.Code != http.StatusMethodNotAllowed {
t.Fatalf("unexpected HTTP status: %d", recorder.Code)
}
}
}
func TestLayoutTemplateIncludesShardokProfileControls(t *testing.T) {
parsed, err := template.ParseFS(templatesFS, "templates/layout.html", "templates/games.html")
if err != nil {
t.Fatalf("parse layout: %v", err)
}
if parsed.Lookup("layout.html") == nil {
t.Fatal("layout template was not parsed")
}
layout, err := templatesFS.ReadFile("templates/layout.html")
if err != nil {
t.Fatalf("read layout: %v", err)
}
for _, expected := range []string{
"shardok-profile-controls",
`onclick="startShardokProfile()"`,
"shardok-profile-stop-dialog",
"shardokProfileStopWithDownload",
"shardokProfileStopOnly",
"shardokProfileStopCancel",
} {
if !strings.Contains(string(layout), expected) {
t.Fatalf("layout does not include %q", expected)
}
}
for _, unwanted := range []string{
"shardok-profile-dialog",
"showShardokProfileDialog",
} {
if strings.Contains(string(layout), unwanted) {
t.Fatalf("layout unexpectedly includes %q", unwanted)
}
}
}
@@ -496,7 +496,8 @@ dialog .modal-actions button {
}
/* JFR controls in nav */
#jfr-controls {
#jfr-controls,
#shardok-profile-controls {
display: flex;
align-items: center;
gap: 0.5rem;
@@ -559,7 +560,8 @@ dialog .modal-actions button {
font-size: 0.875rem;
}
#jfr-controls .btn-small {
#jfr-controls .btn-small,
#shardok-profile-controls .btn-small {
padding: 0.25rem 0.5rem !important;
font-size: 0.75rem;
height: auto !important;
@@ -568,19 +570,22 @@ dialog .modal-actions button {
display: inline-block;
}
#jfr-controls button.btn-small {
#jfr-controls button.btn-small,
#shardok-profile-controls button.btn-small {
background: var(--pico-primary-background);
color: var(--pico-primary-inverse);
border: none;
cursor: pointer;
}
#jfr-controls button.btn-small.secondary {
#jfr-controls button.btn-small.secondary,
#shardok-profile-controls button.btn-small.secondary {
background: var(--pico-secondary-background);
color: var(--pico-secondary-inverse);
}
#jfr-controls a.btn-small {
#jfr-controls a.btn-small,
#shardok-profile-controls a.btn-small {
background: var(--pico-primary-background);
color: var(--pico-primary-inverse);
border-radius: var(--pico-border-radius);
@@ -24,6 +24,9 @@
<li id="jfr-controls" hx-get="/jfr/status" hx-trigger="load" hx-swap="innerHTML">
<span class="jfr-loading">JFR: ...</span>
</li>
<li id="shardok-profile-controls" hx-get="/shardok-profile/status" hx-trigger="load, every 5s" hx-swap="none">
<span class="jfr-loading">Shardok CPU: ...</span>
</li>
<li>
<button onclick="confirmEagleRestart()" class="btn-small secondary outline">Restart Eagle</button>
<span id="eagle-restart-result"></span>
@@ -47,6 +50,19 @@
</article>
</dialog>
<!-- Shardok CPU Profile Stop Dialog -->
<dialog id="shardok-profile-stop-dialog">
<article style="max-width: 400px;">
<h3>Stop Shardok CPU Profile</h3>
<p>Download the recording after stopping?</p>
<footer style="display: flex; gap: 0.5rem; justify-content: flex-end;">
<button class="secondary outline" onclick="shardokProfileStopCancel()">Cancel</button>
<button class="secondary" onclick="shardokProfileStopOnly()">Stop Only</button>
<button onclick="shardokProfileStopWithDownload()">Download & Stop</button>
</footer>
</article>
</dialog>
<!-- JFR Stop Dialog -->
<dialog id="jfr-stop-dialog">
<article style="max-width: 400px;">
@@ -87,6 +103,16 @@
// Refresh status on error
htmx.ajax('GET', '/jfr/status', '#jfr-controls');
}
} else if (evt.detail.pathInfo.requestPath.startsWith('/shardok-profile/')) {
if (evt.detail.pathInfo.requestPath === '/shardok-profile/download') {
return;
}
try {
const data = JSON.parse(evt.detail.xhr.responseText);
updateShardokProfileControls(data);
} catch(e) {
updateShardokProfileControls({error: evt.detail.xhr.responseText || e.message});
}
}
});
@@ -139,6 +165,80 @@
document.getElementById('jfr-stop-dialog').close();
}
function updateShardokProfileControls(data) {
const container = document.getElementById('shardok-profile-controls');
if (data.error) {
container.innerHTML = `<span class="jfr-error" title="${escapeHtml(data.error)}">Shardok CPU: Error</span>`;
return;
}
if (data.status === 'disabled') {
container.innerHTML = '<span class="jfr-status jfr-off">Shardok CPU: OFF</span>';
return;
}
if (data.recording) {
container.innerHTML = `
<span class="jfr-status jfr-on">Shardok CPU: ON</span>
<button onclick="stopShardokProfile()" class="btn-small secondary">Stop</button>
`;
} else {
const download = data.profile_available
? '<a href="/shardok-profile/download" class="btn-small">Download</a>'
: '';
container.innerHTML = `
<span class="jfr-status jfr-off">Shardok CPU: ${data.profile_available ? 'READY' : 'OFF'}</span>
<button onclick="startShardokProfile()" class="btn-small">Start</button>
${download}
`;
}
}
function escapeHtml(value) {
const element = document.createElement('span');
element.textContent = value || '';
return element.innerHTML;
}
function startShardokProfile() {
htmx.ajax('POST', '/shardok-profile/start', {
target: '#shardok-profile-controls',
swap: 'none'
});
}
function stopShardokProfile() {
document.getElementById('shardok-profile-stop-dialog').showModal();
}
function shardokProfileStopWithDownload() {
document.getElementById('shardok-profile-stop-dialog').close();
const downloadWindow = window.open('about:blank', '_blank');
htmx.ajax('POST', '/shardok-profile/stop', {
target: '#shardok-profile-controls',
swap: 'none'
}).then(function() {
if (downloadWindow) {
downloadWindow.location = '/shardok-profile/download';
}
}).catch(function() {
if (downloadWindow) {
downloadWindow.close();
}
});
}
function shardokProfileStopOnly() {
document.getElementById('shardok-profile-stop-dialog').close();
htmx.ajax('POST', '/shardok-profile/stop', {
target: '#shardok-profile-controls',
swap: 'none'
});
}
function shardokProfileStopCancel() {
document.getElementById('shardok-profile-stop-dialog').close();
}
function confirmEagleRestart() {
document.getElementById('eagle-restart-dialog').showModal();
}
@@ -29,6 +29,45 @@ service ShardokInternalInterface {
rpc GetHexMap(HexMapRequest) returns (HexMapResponse) {}
rpc GetHexMapNames(HexMapNamesRequest) returns (HexMapNamesResponse) {}
// Authenticated, on-demand CPU profiling for production diagnostics.
rpc GetCpuProfileStatus(CpuProfileStatusRequest) returns (CpuProfileStatusResponse) {}
rpc StartCpuProfile(StartCpuProfileRequest) returns (CpuProfileStatusResponse) {}
rpc StopCpuProfile(StopCpuProfileRequest) returns (CpuProfileStatusResponse) {}
rpc DownloadCpuProfile(CpuProfileDownloadRequest) returns (stream CpuProfileChunk) {}
}
message CpuProfileStatusRequest {}
message StartCpuProfileRequest {
reserved 1;
}
message StopCpuProfileRequest {}
message CpuProfileStatusResponse {
bool recording = 1;
bool profile_available = 2;
reserved 3;
int64 started_at_epoch_millis = 4;
int64 profile_size_bytes = 5;
int32 sample_frequency_hz = 6;
}
enum CpuProfileArtifactType {
CPU_PROFILE_ARTIFACT_TYPE_UNSPECIFIED = 0;
CPU_PROFILE_ARTIFACT_TYPE_PROFILE = 1;
CPU_PROFILE_ARTIFACT_TYPE_EXECUTABLE = 2;
CPU_PROFILE_ARTIFACT_TYPE_C_LIBRARY = 3;
CPU_PROFILE_ARTIFACT_TYPE_DYNAMIC_LINKER = 4;
}
message CpuProfileDownloadRequest {
CpuProfileArtifactType artifact_type = 1;
}
message CpuProfileChunk {
bytes data = 1;
}
message GameSubscriptionRequest {
@@ -18,6 +18,7 @@ message PrisonerManagementOption {
PrisonerManagementOptionExile exile = 3;
PrisonerManagementOptionReturn return = 4;
PrisonerManagementOptionMove move = 5;
PrisonerManagementOptionInterrogate interrogate = 6;
}
}
@@ -41,3 +42,6 @@ message PrisonerManagementOptionMove {
int32 to_province_id = 1;
}
message PrisonerManagementOptionInterrogate {
int32 target_province_id = 1;
}
@@ -34,6 +34,6 @@ message ShardokBattleView {
.google.protobuf.Int32Value winning_faction_id = 10; // set when battle ended and result is revealed
bool can_fight = 11; // true only for this viewer's highest fightable battle
bool can_observe = 12; // true if this viewer can currently observe this battle
bool defender_retreating = 13; // true once any defending unit has fled
bool attacker_retreating = 14; // true once any attacking unit has fled
bool defender_retreating = 13; // true for an army already retreating or once a defending unit has fled
bool attacker_retreating = 14; // true for an army already retreating or once an attacking unit has fled
}
@@ -235,6 +235,8 @@ minimumTrustToOfferTruce 0 Int
minimumTrustToInvite 20 Int
minimumTrustToOfferAlliance 30 Int
requiredPowerMultiplierForDismiss 1.5 Double
heroCapBaseMultiplier 0.8 Double
heroCapDivisor 100.0 Double
heroCapPerCastle 5 Int
overHeroCapLoyaltyDelta -1 Double
minimumAgilityForArchery 75 Double
1 actionVigorCost 15 Int
235 minimumTrustToInvite 20 Int
236 minimumTrustToOfferAlliance 30 Int
237 requiredPowerMultiplierForDismiss 1.5 Double
238 heroCapBaseMultiplier 0.8 Double
239 heroCapDivisor 100.0 Double
240 heroCapPerCastle 5 Int
241 overHeroCapLoyaltyDelta -1 Double
242 minimumAgilityForArchery 75 Double
+52 -41
View File
@@ -66,15 +66,15 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "bitflags"
version = "2.13.0"
version = "2.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
[[package]]
name = "bytemuck"
version = "1.25.1"
version = "1.25.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6aedf8ae72766347502cf3cb4f41cf5e9cc37d28bee90f1fdaaae15f9cf9424"
checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797"
[[package]]
name = "byteorder-lite"
@@ -101,9 +101,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.6.1"
version = "4.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
checksum = "d91e0c145792ef73a6ad36d27c75ac09f1832222a3c209689d90f534685ee5b7"
dependencies = [
"clap_builder",
"clap_derive",
@@ -111,9 +111,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.6.0"
version = "4.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b"
dependencies = [
"anstream",
"anstyle",
@@ -123,14 +123,14 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.6.1"
version = "4.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
"syn 3.0.3",
]
[[package]]
@@ -190,9 +190,9 @@ checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
[[package]]
name = "either"
version = "1.16.0"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d"
[[package]]
name = "fdeflate"
@@ -258,9 +258,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "libc"
version = "0.2.186"
version = "0.2.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
[[package]]
name = "map_generator"
@@ -279,9 +279,9 @@ dependencies = [
[[package]]
name = "matrixmultiply"
version = "0.3.10"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08"
checksum = "3f607c237553f086e7043417a51df26b2eb899d3caff94e6a67592ff992fedc7"
dependencies = [
"autocfg",
"rawpointer",
@@ -377,9 +377,9 @@ dependencies = [
[[package]]
name = "portable-atomic"
version = "1.13.1"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3"
[[package]]
name = "portable-atomic-util"
@@ -401,9 +401,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.106"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
@@ -416,9 +416,9 @@ checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea"
[[package]]
name = "quote"
version = "1.0.46"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
@@ -484,9 +484,9 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.228"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
@@ -494,29 +494,29 @@ dependencies = [
[[package]]
name = "serde_core"
version = "1.0.228"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 3.0.3",
]
[[package]]
name = "serde_json"
version = "1.0.150"
version = "1.0.151"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
dependencies = [
"itoa",
"memchr",
@@ -527,9 +527,9 @@ dependencies = [
[[package]]
name = "simd-adler32"
version = "0.3.9"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
[[package]]
name = "strsim"
@@ -539,9 +539,20 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.118"
version = "2.0.119"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
dependencies = [
"proc-macro2",
"quote",
@@ -577,22 +588,22 @@ dependencies = [
[[package]]
name = "zerocopy"
version = "0.8.54"
version = "0.8.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19"
checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.54"
version = "0.8.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5"
checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.119",
]
[[package]]
@@ -169,6 +169,117 @@ class EagleHistoryAppendEvent extends Event {
var succeeded: Boolean = false
}
@Name("net.eagle0.eagle.HistoryAppendPhase")
@Label("Eagle History Append Phase")
@Category(Array("Eagle0", "History"))
@Enabled(true)
@StackTrace(false)
@Threshold("0 ms")
class EagleHistoryAppendPhaseEvent extends Event {
@Label("Game ID")
var gameId: String = ""
@Label("Phase")
var phase: String = ""
@Label("Detail")
var detail: String = ""
@Label("Action Result Count")
var actionResultCount: Int = 0
@Label("Item Count")
var itemCount: Long = 0L
@Label("Succeeded")
var succeeded: Boolean = false
}
@Name("net.eagle0.eagle.PostActionPhase")
@Label("Eagle Post-Action Phase")
@Category(Array("Eagle0", "Game Processing"))
@Enabled(true)
@StackTrace(false)
@Threshold("0 ms")
class EaglePostActionPhaseEvent extends Event {
@Label("Game ID")
var gameId: String = ""
@Label("Phase")
var phase: String = ""
@Label("Faction ID")
var factionId: Int = -1
@Label("Action Result Count")
var actionResultCount: Int = 0
@Label("Human Client Count")
var humanClientCount: Int = 0
@Label("Item Count")
var itemCount: Long = 0L
@Label("Succeeded")
var succeeded: Boolean = false
}
@Name("net.eagle0.eagle.ShardokClientCommandReceived")
@Label("Eagle Shardok Client Command Received")
@Category(Array("Eagle0", "Shardok"))
@Enabled(true)
@StackTrace(false)
class EagleShardokClientCommandReceivedEvent extends Event {
@Label("Eagle Game ID")
var eagleGameId: String = ""
@Label("Shardok Game ID")
var shardokGameId: String = ""
@Label("Player ID")
var playerId: Int = 0
@Label("Token")
var token: Long = 0L
@Label("Command Index")
var commandIndex: Int = 0
@Label("Has Roll")
var hasRoll: Boolean = false
@Label("Roll")
var roll: Int = 0
@Label("Request Bytes")
var requestBytes: Int = 0
}
@Name("net.eagle0.eagle.ShardokPlacementCommandsReceived")
@Label("Eagle Shardok Placement Commands Received")
@Category(Array("Eagle0", "Shardok"))
@Enabled(true)
@StackTrace(false)
class EagleShardokPlacementCommandsReceivedEvent extends Event {
@Label("Eagle Game ID")
var eagleGameId: String = ""
@Label("Shardok Game ID")
var shardokGameId: String = ""
@Label("Player ID")
var playerId: Int = 0
@Label("Token")
var token: Long = 0L
@Label("Placement Count")
var placementCount: Int = 0
@Label("Request Bytes")
var requestBytes: Int = 0
}
object JfrEvents {
private val usePostgresQueryEvents = new ThreadLocal[Boolean]()
@@ -330,4 +441,91 @@ object JfrEvents {
}
} else f(event)
}
def historyAppendPhase[T](
gameId: String,
phase: String,
actionResultCount: Int
)(f: EagleHistoryAppendPhaseEvent => T): T = {
val event = new EagleHistoryAppendPhaseEvent()
event.gameId = gameId
event.phase = phase
event.actionResultCount = actionResultCount
if event.isEnabled then {
event.begin()
try {
val value = f(event)
event.succeeded = true
value
} finally {
event.end()
event.commit()
}
} else f(event)
}
def postActionPhase[T](
gameId: => String,
phase: String,
factionId: Int = -1
)(f: EaglePostActionPhaseEvent => T): T = {
val event = new EaglePostActionPhaseEvent()
if event.isEnabled then {
event.gameId = gameId
event.phase = phase
event.factionId = factionId
event.begin()
try {
val value = f(event)
event.succeeded = true
value
} finally {
event.end()
event.commit()
}
} else f(event)
}
def shardokClientCommandReceived(
eagleGameId: => String,
shardokGameId: => String,
playerId: Int,
token: Long,
commandIndex: Int,
roll: Option[Int],
requestBytes: => Int
): Unit = {
val event = new EagleShardokClientCommandReceivedEvent()
if event.isEnabled then {
event.eagleGameId = eagleGameId
event.shardokGameId = shardokGameId
event.playerId = playerId
event.token = token
event.commandIndex = commandIndex
event.hasRoll = roll.isDefined
event.roll = roll.getOrElse(0)
event.requestBytes = requestBytes
event.commit()
}
}
def shardokPlacementCommandsReceived(
eagleGameId: => String,
shardokGameId: => String,
playerId: Int,
token: Long,
placementCount: Int,
requestBytes: => Int
): Unit = {
val event = new EagleShardokPlacementCommandsReceivedEvent()
if event.isEnabled then {
event.eagleGameId = eagleGameId
event.shardokGameId = shardokGameId
event.playerId = playerId
event.token = token
event.placementCount = placementCount
event.requestBytes = requestBytes
event.commit()
}
}
}
@@ -13,6 +13,13 @@ case class ClientTextStoreWithUpdate(
copy(clientTextStore = clientTextStore.saved)
}
final case class ClientTextRequestToAdd(
id: ClientTextId,
accessibleTo: Vector[FactionId],
llmRequest: GeneratedTextRequestT,
requestedAfterHistoryCount: Int
)
trait ClientTextStore {
def completeTexts: Map[ClientTextId, CompleteClientText]
def incompleteTexts: Map[ClientTextId, IncompleteClientText]
@@ -48,6 +55,17 @@ trait ClientTextStore {
requestedAfterHistoryCount: Int
): ClientTextStore
/** Adds multiple text requests. Stores may override this to batch persistence. */
def withAddedTextRequests(requests: Vector[ClientTextRequestToAdd]): ClientTextStore =
requests.foldLeft(this) { (store, request) =>
store.withAddedTextRequest(
id = request.id,
accessibleTo = request.accessibleTo,
llmRequest = request.llmRequest,
requestedAfterHistoryCount = request.requestedAfterHistoryCount
)
}
def getText(id: ClientTextId): TextGenerationResult
/** Returns text-generation results for multiple text IDs. */
File diff suppressed because it is too large Load Diff
@@ -164,12 +164,14 @@ scala_library(
],
exports = [
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_applier",
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
"//src/main/scala/net/eagle0/eagle/model/state/date",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
],
deps = [
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_applier",
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
"//src/main/scala/net/eagle0/eagle/model/state/date",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
],
@@ -2,10 +2,19 @@ package net.eagle0.eagle.library
import scala.annotation.unused
import net.eagle0.eagle.{FactionId, RoundId}
import net.eagle0.eagle.library.actions.applier.ActionResultWithResultingState
import net.eagle0.eagle.model.action_result.types.ActionResultType
import net.eagle0.eagle.model.action_result.NotificationT
import net.eagle0.eagle.model.state.date.Date
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.RoundId
case class ChronicleEventSource(
date: Date,
actionResultType: ActionResultType,
newNotifications: Vector[NotificationT],
removedFactionIds: Vector[FactionId]
)
case class CappedResults(
results: Vector[ActionResultWithResultingState],
@@ -27,6 +36,15 @@ trait GameHistory {
)
def stateAfter(count: Int): GameState
def sinceDate(date: Date): Vector[ActionResultWithResultingState]
def chronicleEventSourcesSinceDate(date: Date): Vector[ChronicleEventSource] =
sinceDate(date).map { awrs =>
ChronicleEventSource(
date = awrs.resultingState.currentDate,
actionResultType = awrs.actionResult.actionResultType,
newNotifications = awrs.actionResult.newNotifications,
removedFactionIds = awrs.actionResult.removedFactionIds
)
}
def count: Int
def last: ActionResultWithResultingState
@@ -24,6 +24,7 @@ scala_library(
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/util:eagle_require",
"//src/main/scala/net/eagle0/eagle/library/util/hero:hero_utils",
"//src/main/scala/net/eagle0/eagle/library/util/province",
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province/concrete",
"//src/main/scala/net/eagle0/eagle/model/state:army",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
@@ -2,6 +2,7 @@ package net.eagle0.eagle.library.actions.applier
import net.eagle0.eagle.*
import net.eagle0.eagle.library.util.hero.HeroUtils
import net.eagle0.eagle.library.util.province.ProvinceUtils
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
import net.eagle0.eagle.model.action_result.changed_province.concrete.{ChangedProvinceC, HostileArmyStatusChange}
import net.eagle0.eagle.model.state.{HostileArmyGroup, MovingArmy}
@@ -74,6 +75,13 @@ object ProvinceUpdateHelpers {
val newFood = p.food + cp.foodDelta.getOrElse(0)
internalRequire(newFood >= 0, s"Got a foodDelta of ${cp.foodDelta} but only ${p.food} available")
val newInfrastructure =
(p.infrastructure + cp.infrastructureDelta.getOrElse(0.0)).max(0.0).min(100.0)
val newInfrastructureDevastation =
(p.infrastructureDevastation + cp.infrastructureDevastationDelta.getOrElse(0.0))
.max(0.0)
.min(p.infrastructure)
p.copy(
incomingShipments = incomingShipments,
defendingArmy = defendingArmy,
@@ -82,12 +90,12 @@ object ProvinceUpdateHelpers {
priceIndex = cp.newPriceIndex.getOrElse(p.priceIndex),
economy = (p.economy + cp.economyDelta.getOrElse(0.0)).max(0.0).min(100.0),
agriculture = (p.agriculture + cp.agricultureDelta.getOrElse(0.0)).max(0.0).min(100.0),
infrastructure = (p.infrastructure + cp.infrastructureDelta.getOrElse(0.0)).max(0.0).min(100.0),
infrastructure = newInfrastructure,
economyDevastation = (p.economyDevastation + cp.economyDevastationDelta.getOrElse(0.0)).max(0.0).min(p.economy),
agricultureDevastation =
(p.agricultureDevastation + cp.agricultureDevastationDelta.getOrElse(0.0)).max(0.0).min(p.agriculture),
infrastructureDevastation =
(p.infrastructureDevastation + cp.infrastructureDevastationDelta.getOrElse(0.0)).max(0.0).min(p.infrastructure),
infrastructureDevastation = newInfrastructureDevastation,
heroCap = ProvinceUtils.heroCap(p.castleCount, newInfrastructure - newInfrastructureDevastation),
support = (p.support + cp.supportDelta.getOrElse(0.0)).max(0.0).min(100.0)
)
}
@@ -255,9 +255,9 @@ object AvailableAttackDecisionCommandFactory extends ScalaAvailableCommandsFacto
/**
* Sequential attack decision logic:
* - Sort undecided army groups by troop count (largest first, tiebreak by factionId)
* - If no army has advanced yet: only the largest undecided army gets the command
* - If an army has advanced: only allied undecided armies get a choice to join; hostile undecided armies are
* auto-bounced at end of phase
* - If no army has committed to attack yet: only the largest undecided army gets the command
* - If an army has advanced or demanded tribute: only allied undecided armies get a choice to join; hostile
* undecided armies are auto-bounced at end of phase
*/
private def isMyTurnToDecide(
pid: ProvinceId,
@@ -267,16 +267,16 @@ object AvailableAttackDecisionCommandFactory extends ScalaAvailableCommandsFacto
val province = gs.provinces(pid)
val hostileArmies = province.hostileArmies
val advancedArmies = hostileArmies.filter(_.status == HostileArmyGroupStatus.Attacking)
val committedArmies = hostileArmies.filter(group => HostileArmyGroupStatus.isCommittedToAttack(group.status))
val undecidedArmies = hostileArmies.filter(_.status == HostileArmyGroupStatus.AwaitingDecision)
undecidedArmies.exists(_.factionId == fid) && {
if advancedArmies.nonEmpty then {
// Someone has already advanced - only allies can join
val advancedFids = advancedArmies.map(_.factionId)
advancedFids.forall(advFid => areAllied(fid, advFid, gs))
if committedArmies.nonEmpty then {
// Someone has already committed to the attack - only allies can join
val committedFids = committedArmies.map(_.factionId)
committedFids.forall(committedFid => areAllied(fid, committedFid, gs))
} else {
// No one has advanced yet - only the largest undecided army gets the command
// No one has committed yet - only the largest undecided army gets the command
val sortedUndecided = undecidedArmies
.sortBy(g => armyGroupSortKey(g, gs))
@@ -122,6 +122,7 @@ object AvailableMarchCommandFactory extends ScalaAvailableCommandsFactory {
): Option[AvailableCommand] =
HeroUtils
.consideredProvinces(gameState, factionId, provinceId)
.filter(p => p.id == provinceId || !p.hasActed)
.flatMap(p => oneOriginProvinceCommand(gameState = gameState, originProvince = p)) match {
case items if items.isEmpty => None
case opcs =>
@@ -574,6 +574,7 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_random_sequential_results_action",
"//src/main/scala/net/eagle0/eagle/library/actions/random_state_sequencer",
"//src/main/scala/net/eagle0/eagle/library/util:eagle_require",
"//src/main/scala/net/eagle0/eagle/library/util/province",
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_hero_trait",
@@ -1,7 +1,6 @@
package net.eagle0.eagle.library.actions.impl.action
import net.eagle0.eagle.library.actions.applier.ActionResultWithResultingState
import net.eagle0.eagle.library.GameHistory
import net.eagle0.eagle.library.{ChronicleEventSource, GameHistory}
import net.eagle0.eagle.model.action_result.generated_text_request.chronicle_event.*
import net.eagle0.eagle.model.action_result.types.ActionResultType
import net.eagle0.eagle.model.action_result.NotificationDetails
@@ -9,18 +8,18 @@ import net.eagle0.eagle.model.state.date.Date
object ChronicleEventGenerator {
private def relevantNotificationDetails(
awrs: ActionResultWithResultingState
source: ChronicleEventSource
): Vector[NotificationDetails] =
awrs.actionResult.newNotifications
source.newNotifications
.map(_.details)
.distinct
private def notificationEntries(
awrs: ActionResultWithResultingState
source: ChronicleEventSource
): Vector[ChronicleEvent] = {
// Chronicle events are only generated during active gameplay when currentDate is present
val date = awrs.resultingState.currentDate
relevantNotificationDetails(awrs).collect {
val date = source.date
relevantNotificationDetails(source).collect {
case NotificationDetails.AllianceAccepted(
offeringFid,
targetFid,
@@ -319,14 +318,13 @@ object ChronicleEventGenerator {
}
private def basicActionResultEntries(
awrs: ActionResultWithResultingState
source: ChronicleEventSource
): Vector[ChronicleEvent] =
awrs.actionResult.actionResultType match {
source.actionResultType match {
case ActionResultType.FactionDestroyed =>
val date = awrs.resultingState.currentDate
awrs.actionResult.removedFactionIds.map { factionId =>
source.removedFactionIds.map { factionId =>
FactionDestroyedChronicleEvent(
date = date,
date = source.date,
factionId = factionId
)
}
@@ -338,7 +336,7 @@ object ChronicleEventGenerator {
gameHistory: GameHistory,
since: Date
): Vector[ChronicleEvent] =
gameHistory.sinceDate(since).flatMap { (awrs: ActionResultWithResultingState) =>
notificationEntries(awrs) ++ basicActionResultEntries(awrs)
gameHistory.chronicleEventSourcesSinceDate(since).flatMap { source =>
notificationEntries(source) ++ basicActionResultEntries(source)
}
}
@@ -42,26 +42,24 @@ case class EndAttackDecisionPhaseAction(
}
/**
* Auto-bounce: For occupied provinces where an army has advanced, set remaining hostile AwaitingDecision armies to
* Withdrawing.
* Auto-bounce: For occupied provinces where an army has advanced or demanded tribute, set remaining hostile
* AwaitingDecision armies to Withdrawing.
*/
private def autoBounceResults: Vector[ActionResultT] =
provinces.flatMap { p =>
val hasAttacker = p.hostileArmies.exists(_.status == HostileArmyGroupStatus.Attacking)
val isOccupied = p.rulingFactionId.nonEmpty
val undecidedFids = p.hostileArmies
val committedAttackerFids = p.hostileArmies
.filter(group => HostileArmyGroupStatus.isCommittedToAttack(group.status))
.map(_.factionId)
val isOccupied = p.rulingFactionId.nonEmpty
val undecidedFids = p.hostileArmies
.filter(_.status == HostileArmyGroupStatus.AwaitingDecision)
.map(_.factionId)
if hasAttacker && isOccupied && undecidedFids.nonEmpty then {
// Check which undecided armies are hostile to the advancing army
val attackingFids = p.hostileArmies
.filter(_.status == HostileArmyGroupStatus.Attacking)
.map(_.factionId)
if committedAttackerFids.nonEmpty && isOccupied && undecidedFids.nonEmpty then {
// Check which undecided armies are hostile to the committed attackers
val hostileUndecided = undecidedFids.filter { fid =>
!attackingFids.exists(advFid =>
fid == advFid || FactionUtils.hasAlliance(fid, advFid, factions.values.toVector)
!committedAttackerFids.forall(committedFid =>
fid == committedFid || FactionUtils.hasAlliance(fid, committedFid, factions.values.toVector)
)
}
@@ -83,8 +81,8 @@ case class EndAttackDecisionPhaseAction(
)
}
.toVector
} else if !hasAttacker && undecidedFids.size > 1 && hasNonAlliedPair(undecidedFids) then {
// No army advanced but non-allied factions remain bounce all undecided
} else if committedAttackerFids.isEmpty && undecidedFids.size > 1 && hasNonAlliedPair(undecidedFids) then {
// No army committed but non-allied factions remain bounce all undecided
Vector(
ActionResultC(
actionResultType = EndAttackDecisionPhase,
@@ -4,7 +4,9 @@ import net.eagle0.common.{FunctionalRandom, RandomState}
import net.eagle0.eagle.library.actions.applier.ActionResultApplier
import net.eagle0.eagle.library.actions.impl.common.ProtolessRandomSequentialResultsAction
import net.eagle0.eagle.library.actions.random_state_sequencer.RandomStateSequencer
import net.eagle0.eagle.library.util.province.ProvinceUtils
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
import net.eagle0.eagle.model.action_result.changed_province.concrete.ChangedProvinceC
import net.eagle0.eagle.model.action_result.concrete.{ActionResultC, ChangedFactionC}
import net.eagle0.eagle.model.action_result.types.ActionResultType.EndVassalCommandsPhase
import net.eagle0.eagle.model.action_result.ActionResultT
@@ -111,6 +113,11 @@ case class EndVassalCommandsPhaseAction(
ActionResultC(
actionResultType = EndVassalCommandsPhase,
newRoundPhase = Some(RoundPhase.PlayerCommands),
changedProvinces = gs.provinces.values
.filter(_.hasActed)
.filterNot(ProvinceUtils.ruledByFactionLeader(_, gs))
.map(p => ChangedProvinceC(provinceId = p.id, setHasActed = Some(false)))
.toVector,
removedNotifications = gs.deferredNotifications.map(_.withDeferred(true)),
newNotifications = gs.deferredNotifications.map(_.withDeferred(false)),
changedFactions = staleFocusClears
@@ -3096,6 +3096,32 @@ scala_setting_library(
],
)
scala_setting_library(
name = "hero_cap_base_multiplier",
setting_name = "HeroCapBaseMultiplier",
setting_type = "Double",
setting_value = "0.8",
visibility = [
"//src/main/scala/net/eagle0/eagle:__subpackages__",
"//src/main/scala/net/eagle0/util:__subpackages__",
"//src/test/resources/net/eagle0/shardok/maps:__pkg__",
"//src/test/scala/net/eagle0/eagle:__subpackages__",
],
)
scala_setting_library(
name = "hero_cap_divisor",
setting_name = "HeroCapDivisor",
setting_type = "Double",
setting_value = "100.0",
visibility = [
"//src/main/scala/net/eagle0/eagle:__subpackages__",
"//src/main/scala/net/eagle0/util:__subpackages__",
"//src/test/resources/net/eagle0/shardok/maps:__pkg__",
"//src/test/scala/net/eagle0/eagle:__subpackages__",
],
)
scala_setting_library(
name = "hero_cap_per_castle",
setting_name = "HeroCapPerCastle",
@@ -124,6 +124,8 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/library/settings:gold_cost_for_divine",
"//src/main/scala/net/eagle0/eagle/library/settings:gold_increase_per_economy",
"//src/main/scala/net/eagle0/eagle/library/settings:gold_per_hero_held_back",
"//src/main/scala/net/eagle0/eagle/library/settings:hero_cap_base_multiplier",
"//src/main/scala/net/eagle0/eagle/library/settings:hero_cap_divisor",
"//src/main/scala/net/eagle0/eagle/library/settings:hero_cap_per_castle",
"//src/main/scala/net/eagle0/eagle/library/settings:history_backend",
"//src/main/scala/net/eagle0/eagle/library/settings:improvement_per_stat",
@@ -191,7 +191,6 @@ scala_library(
deps = [
"//src/main/scala/net/eagle0/common:tsv_utils",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/settings:hero_cap_per_castle",
"//src/main/scala/net/eagle0/eagle/library/util:eagle_require",
"//src/main/scala/net/eagle0/eagle/model/state/province",
"//src/main/scala/net/eagle0/eagle/model/state/province/concrete",
@@ -3,7 +3,6 @@ package net.eagle0.eagle.library.util
import java.net.URL
import net.eagle0.common.TsvUtils
import net.eagle0.eagle.library.settings.HeroCapPerCastle
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
import net.eagle0.eagle.model.state.province.{Neighbor, ProvinceT}
import net.eagle0.eagle.model.state.province.concrete.ProvinceC
@@ -23,8 +22,7 @@ object MapGenerator {
name = map("name"),
neighbors = neighborsFromMap(map),
hexMapName = map("hexMapName"),
castleCount = map("castleCount").toInt,
heroCap = map("castleCount").toInt * HeroCapPerCastle.intValue
castleCount = map("castleCount").toInt
)
def neighborsFromMap(map: Map[String, String]): Vector[Neighbor] = {
@@ -2,6 +2,7 @@ package net.eagle0.eagle.library.util.command_choice_helpers
import net.eagle0.eagle.{FactionId, ProvinceId}
import net.eagle0.eagle.library.settings.{
MaxCombatUnitCountPerSide,
MinVigorForMarch,
RequiredRatioToAttack,
RequiredRatioToAttackWithoutTakingCastles
@@ -115,12 +116,10 @@ object AttackCommandChooser {
heroesWithSufficientVigorForAttack(opmc.originProvinceId, gameState)
val unitCount = Vector(
Math.max(
(heroes.length + 1) / 2,
ShardokMapInfo.shardokMapInfos(destinationProvince.id).castleCount
),
heroes.length,
originProvince.rulingFactionHeroIds.length - 1, // Don't abandon the origin province
battalions.length
battalions.length,
MaxCombatUnitCountPerSide.intValue
).min
val selectedUnits = CombatUnitSelector.selectedCombatBattalions(
@@ -72,6 +72,7 @@ scala_library(
srcs = ["AttackCommandChooser.scala"],
visibility = [
"//src/main/scala/net/eagle0/eagle/ai:__pkg__",
"//src/test/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
"//src/test/scala/net/eagle0/eagle/library/util/command_choice_helpers:__pkg__",
],
exports = [
@@ -82,6 +83,7 @@ scala_library(
":combat_unit_selector",
":march_supplies_helpers",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/settings:max_combat_unit_count_per_side",
"//src/main/scala/net/eagle0/eagle/library/settings:min_vigor_for_march",
"//src/main/scala/net/eagle0/eagle/library/settings:required_ratio_to_attack",
"//src/main/scala/net/eagle0/eagle/library/settings:required_ratio_to_attack_without_taking_castles",

Some files were not shown because too many files have changed in this diff Show More