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
adminandGitHub f7d79639b5 Prioritize valuable safe upgrade quests (#8705) 2026-07-18 14:28:01 -07:00
adminandGitHub da0d1c04fd Handle incomplete Gemini streams as provider failures (#8706) 2026-07-18 12:28:43 -07:00
adminandGitHub 0c0ff85513 Generate focus province quests (#8704)
* Add focus province quest type

* Generate focus province quests
2026-07-18 09:09:18 -07:00
adminandGitHub c2c18d8a07 Display focus province quests (#8702)
* Add focus province quest type

* Display focus province quests
2026-07-18 09:08:44 -07:00
adminandGitHub 5e7223aa8e Add focus province quest type (#8701)
* Add focus province quest type

* Test focus quest without focus province
2026-07-18 08:56:10 -07:00
adminandGitHub 14a475032e Fix alliance offer cooldown tracking (#8703) 2026-07-18 08:44:42 -07:00
adminandGitHub 3dce099b2a Fix selected province fade flicker (#8700) 2026-07-18 08:28:05 -07:00
adminandGitHub 009a4ffe53 Clarify AI attack selection reason (#8699) 2026-07-18 08:24:41 -07:00
adminandGitHub 7781bbcc46 Remove dead AI client focus province state (#8698) 2026-07-18 08:22:11 -07:00
adminandGitHub e6438302e2 Resend mismatched completed client text (#8697) 2026-07-18 08:06:13 -07:00
adminandGitHub a919e8edf0 Repair stale generated text on reconnect (#8696)
* Recover missing recruit hero backstories

* Validate completed client text byte counts

* Revert "Recover missing recruit hero backstories"

This reverts commit 9cc349f8e9a477700d188323037bda5c6ebc30ec.
2026-07-18 07:56:09 -07:00
adminandGitHub a59b732b04 Require Rest for RestProvince quests (#8695)
* Require rest commands for RestProvince quests

* Centralize Rest command selection
2026-07-18 07:53:35 -07:00
adminandGitHub 077e0d2bc3 Retry empty generated text responses (#8693) 2026-07-18 05:25:47 -07:00
adminandGitHub 3ad7c3e567 Avoid resting in safe shallow provinces (#8694) 2026-07-18 05:24:42 -07:00
adminandGitHub 62fd16cec5 Show retreating armies in progress panels (#8688) 2026-07-17 20:41:06 -07:00
adminandGitHub 89d1d15979 Only warn when lone leader fix fails (#8692) 2026-07-17 19:55:28 -07:00
adminandGitHub 1bea610232 Fix lone faction leader validation (#8691) 2026-07-17 19:51:18 -07:00
adminandGitHub 8cce072614 Remove routine client cache success log (#8690) 2026-07-17 19:41:42 -07:00
adminandGitHub fb74935ad9 Keep eagle logs following through deployments (#8689) 2026-07-17 19:27:59 -07:00
adminandGitHub 36915b8184 Version changed available command sets (#8687)
* Version changed available command sets

* Preserve active command across model refreshes
2026-07-17 19:08:02 -07:00
adminandGitHub e07cdc8123 Expose retreating army state in server views (#8686)
* Expose retreating armies in progress panels

* Keep retreating army PR server-side
2026-07-17 18:58:57 -07:00
adminandGitHub fe4e205a07 Space attack ready tutorial guidance (#8685) 2026-07-17 15:59:09 -07:00
adminandGitHub 4506ea2672 Consolidate January tax guidance (#8684) 2026-07-17 15:42:21 -07:00
adminandGitHub ba69263f06 Delay ready hero tutorial guidance (#8683) 2026-07-17 15:39:26 -07:00
adminandGitHub 2a660f05d7 Pace post-battle tutorial guidance by month (#8682) 2026-07-17 15:18:42 -07:00
adminandGitHub 4dd30a9863 Delay Alms guidance after Improve dialogue (#8681) 2026-07-17 15:10:53 -07:00
adminandGitHub 1f18f3296d Name hero in low-vigor tutorial guidance (#8680) 2026-07-17 15:01:09 -07:00
adminandGitHub 467d2e2c82 Gate armament tutorial on available upgrades (#8679)
* Gate armament tutorial on available upgrades

* Delay attack-ready tutorial guidance
2026-07-17 14:38:54 -07:00
adminandGitHub 73b8087e07 Initialize tutorial faction trust (#8678) 2026-07-17 14:23:49 -07:00
adminandGitHub 76be4a49ea Clarify developed border tutorial dialogue (#8677) 2026-07-17 14:22:56 -07:00
adminandGitHub 27aae790ad Delay Elena alms tutorial guidance (#8676) 2026-07-17 14:03:21 -07:00
adminandGitHub fdf7f2a88d Track tutorial dialogue rounds and sequence attack guidance (#8675)
* Sequence attack tutorial with emitted dialogue state

* Track tutorial dialogue delivery rounds
2026-07-17 13:50:43 -07:00
adminandGitHub f1cf03e49e Track tutorial dialogue progress in game state (#8674)
* Track King departure tutorial by emitted month

* Store tutorial dialogue progress in game state
2026-07-17 13:21:17 -07:00
adminandGitHub 5213fb4805 Gate fire tutorial guidance on odds (#8673) 2026-07-17 11:56:54 -07:00
adminandGitHub a884b60991 Sequence tutorial attack preparation guidance (#8672) 2026-07-17 11:47:26 -07:00
adminandGitHub 1fdc679dfd Fix follow-up hero departure dialogue (#8671) 2026-07-17 11:26:56 -07:00
adminandGitHub afabe092c2 Make duel capture dialogue definitive (#8670) 2026-07-17 11:15:24 -07:00
adminandGitHub 3624279c4e Contextualize attack arm tutorial (#8669) 2026-07-17 09:16:56 -07:00
adminandGitHub eca0289a43 Make text client subscriptions resilient (#8668) 2026-07-17 09:08:00 -07:00
adminandGitHub 90bedd4be5 Require prisoner wait before recruitment (#8667) 2026-07-17 09:06:47 -07:00
adminandGitHub 2231cf1b9b Refresh expired OAuth sessions during gameplay (#8666)
* Refresh expired OAuth sessions during gameplay

* Refresh OAuth tokens on a five minute cadence
2026-07-17 09:05:28 -07:00
adminandGitHub 30a68bdca0 Include trust in invitation acceptance (#8665) 2026-07-17 08:43:04 -07:00
adminandGitHub 4f69ea79b4 Preserve trust when resolving tribute (#8664) 2026-07-17 08:37:57 -07:00
adminandGitHub 665d2291ef Add on-fire tutorial warning (#8663) 2026-07-17 07:34:55 -07:00
adminandGitHub a6ab25fcee Prioritize duel tutorial before melee (#8662) 2026-07-17 07:27:02 -07:00
adminandGitHub e2a82f7225 Allow configuring normal game player counts (#8661) 2026-07-17 06:04:46 -07:00
adminandGitHub 5e5244ead4 Ensure iOS platform before TestFlight archive (#8656) 2026-07-16 18:42:52 -07:00
adminandGitHub 74d3af589e Render tactical action names in text client (#8658) 2026-07-16 17:47:09 -07:00
adminandGitHub a6641d5eb1 Clarify hero and diplomacy campaign rules (#8660)
* Document hero recruitment eligibility

* Document low-loyalty hero departures

* Document diplomacy resolution outcomes
2026-07-16 17:46:48 -07:00
adminandGitHub 3ecb665709 Show focus province command payloads (#8659) 2026-07-16 17:45:41 -07:00
adminandGitHub 9b033230c9 Generate valid Defend command samples (#8657) 2026-07-16 17:15:28 -07:00
adminandGitHub bc813b8659 Clear retained tutorial focus after player action (#8655)
* Clear tutorial focus when dialogue closes

* Clear retained tutorial focus after player action
2026-07-16 16:43:36 -07:00
adminandGitHub 0fe61c71c9 Resolve tutorial speakers against the current game (#8654) 2026-07-16 13:48:20 -07:00
adminandGitHub 315c1daf82 Skip empty AI expansion marches (#8653) 2026-07-16 13:39:09 -07:00
adminandGitHub 45abe81540 Persist emitted tutorial dialogue IDs (#8652) 2026-07-16 13:22:16 -07:00
adminandGitHub 8141770268 Fix Visit Town selector project reference (#8651) 2026-07-16 11:03:40 -07:00
adminandGitHub fa7775a023 Track LLM stream attempts across retries (#8650) 2026-07-16 11:01:28 -07:00
adminandGitHub b3bf291a7d Reset client text before retrying LLM streams (#8649) 2026-07-16 10:46:06 -07:00
adminandGitHub 557669546b Persist displayed tutorial dialogue IDs (#8648) 2026-07-16 10:37:04 -07:00
adminandGitHub 42958cb34e Strengthen post-faction tutorial guidance (#8647) 2026-07-16 10:19:21 -07:00
adminandGitHub 91f02da9cf Contract Visit Town database column (#8646) 2026-07-16 10:00:45 -07:00
adminandGitHub 8d8b55e2c4 Cut over Visit Town database writes (#8645) 2026-07-16 09:23:24 -07:00
adminandGitHub 947cedc0f0 Explain hero units with zero troops (#8642) 2026-07-16 09:07:24 -07:00
adminandGitHub 4e5206220c Expand Visit Town database migration (#8644) 2026-07-16 09:07:02 -07:00
adminandGitHub acd299c346 Remove internal vigor ceiling from unit views (#8643) 2026-07-16 09:06:34 -07:00
adminandGitHub 74b816d3f4 Rename Travel command to Visit Town (#8641) 2026-07-16 08:47:06 -07:00
adminandGitHub 4abdefd40d Clarify vigor values in text client (#8640) 2026-07-16 08:45:58 -07:00
adminandGitHub 08f7e4ebfc Expose battle-start hero vigor correctly (#8639) 2026-07-16 08:45:05 -07:00
adminandGitHub afbf4c226c Teach tactical vigor thresholds contextually (#8638) 2026-07-16 08:44:27 -07:00
adminandGitHub 0afa842efe Clarify Travel versus March (#8635)
* Clarify Travel versus March

* Fix Travel command tooltip
2026-07-16 07:56:10 -07:00
adminandGitHub e78ae6ad75 Expand vigor and tactical unit documentation (#8637) 2026-07-16 07:15:49 -07:00
adminandGitHub 9631435dea Document tribute demand outcomes (#8636) 2026-07-16 07:08:55 -07:00
adminandGitHub 5ae910b19b Document refused tribute flow (#8634) 2026-07-16 07:00:08 -07:00
adminandGitHub dc9b1171fb Add tutorial defeat dialogue (#8633) 2026-07-16 05:34:29 -07:00
adminandGitHub 508eb64bfb Support spaces in text lookup IDs (#8632) 2026-07-15 22:27:10 -07:00
adminandGitHub 9aa7be4620 Fix text client command loss after reconnect (#8631)
* Fix text client command loss after reconnect

* Normalize text client game status from history
2026-07-15 22:20:01 -07:00
adminandGitHub f3d887e370 Move loyalty tutorial warning to October (#8630) 2026-07-15 22:12:11 -07:00
adminandGitHub ce4ed6a305 Register tutorial Info button target (#8629) 2026-07-15 22:04:54 -07:00
adminandGitHub 2b5e4aa96e Add strategic tutorial handoff dialogue (#8628) 2026-07-15 22:04:42 -07:00
adminandGitHub 41b9f5526c Hide ended games from active lobby (#8627) 2026-07-15 16:53:47 -07:00
adminandGitHub 6b24776e0a Handle terminal game state in text client (#8626) 2026-07-15 16:53:34 -07:00
adminandGitHub b1b44ebb6c Expose Eagle province graph in text client (#8625)
* Expose Eagle province graph in text client

* Limit province views to strategic adjacency

* Isolate queued battle result Unity test
2026-07-15 15:25:45 -07:00
adminandGitHub 45a2d50b57 Fix tutorial duel outcome after capture (#8624) 2026-07-15 14:06:32 -07:00
adminandGitHub 84f329ab80 Document tutorial playthrough notes (#8361)
* Document tutorial playthrough notes

* Record tutorial battle reset findings

* Clarify tutorial reset as probable defeat

* Record castle-hold tutorial attempt

* Record successful tutorial battle strategy

* Record failed Hedrick tutorial attack
2026-07-15 13:58:21 -07:00
adminandGitHub fcf9246562 Preserve final tactical battle action (#8623) 2026-07-15 13:57:36 -07:00
adminandGitHub 0d361f5ad7 Show army origins in tribute and defend panels (#8622)
* Show tribute-demand army origins

* Show army origins in defend panel
2026-07-15 13:42:16 -07:00
adminandGitHub 217d495747 Trust local GameAdmin service calls (#8621)
* Forward admin authentication to Eagle RPCs

* Revert "Forward admin authentication to Eagle RPCs"

This reverts commit feeff278c440be313cdb50902bff10a12c68379b.

* Trust local GameAdmin service calls
2026-07-15 13:36:49 -07:00
adminandGitHub ca80ad0057 Align tutorial dialogue with hero departures (#8619) 2026-07-15 13:23:38 -07:00
adminandGitHub c49c4d26e1 Include client texts in save downloads (#8617)
* Include client texts in save downloads

* Fix admin service client text dependencies
2026-07-15 13:16:58 -07:00
adminandGitHub 68906df5b9 Clarify diplomacy with departing heroes (#8618) 2026-07-15 13:15:52 -07:00
adminandGitHub dc54584fa3 Limit support deadline tutorial to first year (#8616)
* Limit support deadline tutorial to first year

* Use the actual tutorial start year
2026-07-15 13:11:37 -07:00
adminandGitHub d665eb53f3 Remove tutorial phase from game state view (#8615) 2026-07-15 13:06:01 -07:00
adminandGitHub 59143a2e04 Add tutorial dialogue for battle conditions (#8613)
* Add tutorial dialogue for battle conditions

* Mention castle morale recovery in tutorial

* Mention Holy Wave battle recovery
2026-07-15 13:01:58 -07:00
adminandGitHub f9825861e3 Order generated hero names before backstories (#8614) 2026-07-15 13:01:09 -07:00
adminandGitHub 9b5dcc40a0 Keep editor OAuth flow in polling mode (#8612) 2026-07-15 13:00:32 -07:00
adminandGitHub 44514fb2f2 Activate unused-hero warning in tutorial battle (#8609) 2026-07-15 12:08:40 -07:00
adminandGitHub 3014602331 Retry transient OAuth polling failures (#8611) 2026-07-15 12:08:10 -07:00
adminandGitHub bb182efc3e Add dormant tutorial end-turn warning support (#8608) 2026-07-15 11:57:29 -07:00
adminandGitHub 3a04d865b6 Add OAuth login to text client (#8610) 2026-07-15 11:56:42 -07:00
adminandGitHub f2f81d1cda Add tutorial unused-hero warning protocol (#8607) 2026-07-15 11:54:11 -07:00
adminandGitHub 72dc8ba9fb Fix command title vertical centering (#8605) 2026-07-15 11:38:51 -07:00
adminandGitHub 7adc38fe12 Report and isolate game loading failures (#8604) 2026-07-15 11:35:19 -07:00
adminandGitHub 67dbf928d6 Isolate running game serialization failures (#8603) 2026-07-15 11:26:58 -07:00
adminandGitHub efad456bef Add tutorial dialogue for duel outcomes (#8601)
* Add tutorial dialogue for duel outcomes

* Distinguish lost and inconclusive duels

* Simplify duel outcome consequences
2026-07-15 11:21:18 -07:00
adminandGitHub 9ebc517c9f Reject unauthenticated game requests (#8602)
* Preserve authentication context for game streams

* Reject unauthenticated game requests
2026-07-15 11:19:11 -07:00
adminandGitHub 49020af5e9 Serialize unit selector touch targets (#8600) 2026-07-15 11:04:15 -07:00
adminandGitHub e5e5dd9fdc Serialize March touch targets (#8598)
* Serialize March touch targets

* Preserve March slider handle aspect
2026-07-15 10:56:25 -07:00
adminandGitHub 0dd780fc63 Add tutorial attack readiness guidance (#8599) 2026-07-15 10:53:39 -07:00
adminandGitHub ae4c441427 Clarify province abandonment before tutorial march (#8597) 2026-07-15 10:38:06 -07:00
adminandGitHub f01a2ee378 Show game over state in text client (#8595) 2026-07-15 10:34:49 -07:00
adminandGitHub e4901b6414 Clarify Retreat and Flee outcomes (#8596) 2026-07-15 10:31:51 -07:00
adminandGitHub f0b2fd5f5e Add attack preparation tutorial sequence (#8591)
* Add attack preparation tutorial sequence

* Emphasize hiring troops in attack tutorial

* Recommend Champions for troop training
2026-07-15 10:27:46 -07:00
adminandGitHub a28a0ef8d5 Serialize Battle Aftermath touch targets (#8594) 2026-07-15 10:26:22 -07:00
adminandGitHub 88ff742bb1 Serialize Attack Decision touch targets (#8593) 2026-07-15 10:21:12 -07:00
adminandGitHub d549c0642b Prevent duplicate gRPC stream closure (#8592) 2026-07-15 10:19:39 -07:00
adminandGitHub 00c677d12a Serialize Improve selector touch targets (#8590) 2026-07-15 10:15:06 -07:00
adminandGitHub 6c47019e8e Explain hero-only March units (#8588) 2026-07-15 09:33:11 -07:00
adminandGitHub f7932c6681 Serialize organize troops touch targets (#8589) 2026-07-15 09:32:44 -07:00
adminandGitHub 13e734d9ae Serialize popup Next button touch sizes (#8587) 2026-07-15 08:28:03 -07:00
adminandGitHub a4f934105f Fix text consistency across game rewinds (#8585) 2026-07-15 07:59:14 -07:00
adminandGitHub 1300ec9ab9 Serialize Eagle play-area touch targets (#8586) 2026-07-15 07:58:36 -07:00
adminandGitHub bfba377c57 Serialize shared UI touch target sizes (#8584) 2026-07-15 07:40:09 -07:00
adminandGitHub 56564c1bee Prevent disposed connections from reconnecting (#8583)
* Prevent disposed connections from reconnecting

* Disambiguate lifecycle test timer
2026-07-15 07:32:29 -07:00
adminandGitHub 1728d719a4 Preserve tutorial games across Eagle deploys (#8578)
* Restore tutorial battle config after deploy

* Invalidate warmed game index after deploy
2026-07-15 07:31:55 -07:00
adminandGitHub 3314ef191f Show valid trade command payloads (#8580) 2026-07-15 07:17:05 -07:00
adminandGitHub 1fbd2e8438 Balance command panel header layout (#8581) 2026-07-15 07:16:40 -07:00
adminandGitHub 7f50b61419 Rewire status UI after connection replacement (#8579) 2026-07-15 07:04:55 -07:00
adminandGitHub c7dea02861 Defer strategic tutorial dialogue during battles (#8572) 2026-07-15 06:54:50 -07:00
adminandGitHub afd8028665 Report malformed command protos as command errors (#8577) 2026-07-15 06:53:41 -07:00
adminandGitHub 2263977668 Preserve pending tutorial dialogue on reconnect (#8574)
* Preserve pending tutorial dialogue on reconnect

* Fix hero departure regression test calls

* Reset tutorial state when switching games
2026-07-15 06:52:59 -07:00
adminandGitHub 4cfec68ef8 Fix stale tutorial battle reinforcements (#8576)
* Fix stale tutorial battle reinforcements

* Limit fix to future tutorial battles
2026-07-15 06:40:23 -07:00
adminandGitHub 432276f550 Show attack decision payloads in text client (#8575)
* Show attack decision command payloads

* Expand complex command templates
2026-07-15 05:31:17 -07:00
adminandGitHub ef179e67ef Improve organize troops text client template (#8573) 2026-07-14 22:28:59 -07:00
adminandGitHub 7ee9fbb8da Recommend tutorial battle starting positions (#8571) 2026-07-14 21:48:25 -07:00
adminandGitHub 3d46bee7dd Prevent game loading lock inversion (#8570) 2026-07-14 21:39:14 -07:00
adminandGitHub c6dc5477f5 Remove client-driven tutorial content (#8569) 2026-07-14 21:38:34 -07:00
adminandGitHub ab7499ca7a Migrate remaining strategic tutorial dialogue to server (#8564)
* Migrate remaining strategic tutorial dialogue

* Restore province replacement in tutorial dialogue
2026-07-14 19:08:44 -07:00
adminandGitHub 7d95c81a00 Resolve province tutorial replacements in text client (#8566) 2026-07-14 18:38:39 -07:00
adminandGitHub e523ebf96e Resolve province tutorial replacements in Unity (#8568) 2026-07-14 18:38:13 -07:00
adminandGitHub b740e1ad59 Refresh same-token strategic updates (#8567) 2026-07-14 17:01:44 -07:00
adminandGitHub 3a612426f6 Add province tutorial replacement schema (#8565) 2026-07-14 16:46:00 -07:00
adminandGitHub 452d0cac5a Show under attack alert in command panel (#8563) 2026-07-14 15:08:04 -07:00
adminandGitHub bdb7b324c4 Use layout groups for command panel (#8562) 2026-07-14 14:53:38 -07:00
adminandGitHub 145f7e9f9c Prepare game schema before client text (#8561) 2026-07-14 14:22:20 -07:00
adminandGitHub 09e5f4801d Explain Shardok command mechanics (#8560) 2026-07-14 14:20:55 -07:00
adminandGitHub 2aa5fa181a Use recommended Shardok starting positions (#8557)
* Use recommended Shardok starting positions

* Snapshot reserves before applying recommendations
2026-07-14 14:20:16 -07:00
adminandGitHub f438c4bbe2 Expose recommended Shardok placements (#8559) 2026-07-14 14:19:44 -07:00
adminandGitHub 66ecc25075 Add Shardok map queries to text client (#8558) 2026-07-14 14:15:20 -07:00
adminandGitHub 562d32d296 Recommend Shardok starting positions (#8556)
* Recommend Shardok starting positions

* Require explicit AI time limits
2026-07-14 14:09:03 -07:00
adminandGitHub bdf9b09b26 Persist creation history in one batch (#8551) 2026-07-14 13:37:29 -07:00
adminandGitHub d2559ff67c Show tactical conditions in text client (#8555) 2026-07-14 13:36:51 -07:00
adminandGitHub 88433fbc18 Batch decision logging off the game thread (#8548) 2026-07-14 13:36:27 -07:00
adminandGitHub 9426488913 Use server-driven tactical tutorial dialogue (#8554) 2026-07-14 13:35:35 -07:00
adminandGitHub 81395a9467 Document Holy Wave mechanics (#8553) 2026-07-14 13:15:10 -07:00
adminandGitHub fb4e0c1bac Validate tactical tutorial dialogues before delivery (#8552) 2026-07-14 11:12:44 -07:00
adminandGitHub 94b273ff60 Validate tutorial dialogue references before delivery (#8544)
* Validate tutorial dialogue references before delivery

* Allow supported tactical tutorial focus
2026-07-14 10:54:12 -07:00
adminandGitHub b179e250dd Render tactical tutorial dialogue in text client (#8550) 2026-07-14 10:51:54 -07:00
adminandGitHub b3e35b60c0 Dispose replaced game model updater (#8545) 2026-07-14 10:47:05 -07:00
adminandGitHub 3a0fa9e59c Instrument AI command phases (#8547) 2026-07-14 10:42:13 -07:00
adminandGitHub cdf7c5649b Serve tactical tutorial dialogue (#8549) 2026-07-14 10:40:27 -07:00
adminandGitHub 743737555e Test tutorial server dialogue queue behavior (#8543) 2026-07-14 10:38:28 -07:00
adminandGitHub 94f2366af3 Reuse AI command availability snapshot (#8546) 2026-07-14 10:36:25 -07:00
adminandGitHub 5d4399cad6 Prefer server copies of tutorial dialogue (#8518)
* Prefer server copies of tutorial dialogue

* Preserve tutorial dialogue model during battle transition

* Retry tutorial dialogue when dependencies arrive

* Pass tutorial model directly to dialogue manager

* Ignore stale game model callbacks

* Ignore incomplete tutorial game identity

* Initialize provisional model game ID

* Persist new game histories with their game ID

* Revert "Persist new game histories with their game ID"

This reverts commit e23610a7c3152b4f4cb5623f1c31b60d1ad23756.

* Remove redundant dialogue text listeners
2026-07-14 09:46:06 -07:00
adminandGitHub 011562a2d6 Avoid Postgres waits during Shardok reconnects (#8542) 2026-07-14 09:44:04 -07:00
adminandGitHub 781c7a8c3d Dispatch initial LLM requests asynchronously (#8541) 2026-07-14 09:26:33 -07:00
adminandGitHub bce1ddaf81 Persist new game histories with their game ID (#8540) 2026-07-14 08:39:30 -07:00
adminandGitHub d734c591a8 Clarify mixed-border AI decision reasons (#8539) 2026-07-14 08:34:45 -07:00
adminandGitHub c5aca2666c Tune AI Rest priority for hero depth (#8537)
* Tune AI rest priority for hero depth

* Raise shallow AI rest threshold

* Consider feast for RestProvince quests
2026-07-14 08:08:42 -07:00
adminandGitHub 6e261ef7b5 Omit unrestricted client text visibility rows (#8538) 2026-07-14 08:02:51 -07:00
adminandGitHub 71240cd9ed Keep one PostgreSQL connection warm (#8536) 2026-07-14 07:05:48 -07:00
adminandGitHub 15d7d30ccf Defer schema pool refill after game creation (#8533) 2026-07-14 06:37:21 -07:00
adminandGitHub f52d54b7c2 Sync Xcode before TestFlight Unity builds (#8534) 2026-07-14 06:35:58 -07:00
6314d3e4e3 Fix water crossing flee scoring (#8527)
* Fix water crossing flee scoring

* Strengthen water crossing creator penalty

* Score crossing creators on one saturating distance scale

Replace the flat unavailable-creator penalty with a single loop over
every crossing-capable unit. Unavailable (fled/captured/destroyed) and
unplaced creators sit at the max distance sentinel, and live distances
(including IMPOSSIBLE) are clamped to it, so losing a crossing creator
can never score better than any placed position. This also removes the
formula discontinuity between the creator-alive and creator-lost
branches that the flee transition crossed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Name water crossing distance sentinel

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 21:37:41 -07:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
3f54abc3c0 Update actions/setup-node action to v7 (#8532)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-13 21:12:54 -07:00
adminandGitHub 0e126faee2 Move navigation touch sizing into prefabs (#8529)
* Move navigation touch sizing into prefabs

* Increase Chronicle dismiss touch height
2026-07-13 21:10:10 -07:00
adminandGitHub 763ab63860 Preserve chronicle history on rejoin (#8531) 2026-07-13 18:18:08 -07:00
adminandGitHub 38a15fdf9f Enable PostgreSQL batched insert rewriting (#8530) 2026-07-13 15:11:34 -07:00
adminandGitHub b0446d99e4 Move command panel touch sizing into prefabs (#8528) 2026-07-13 14:32:26 -07:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2f6a58255b Update dependency bazel to v9.2.0 (#8526)
* Update dependency bazel to v9.2.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-13 14:21:52 -07:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
a4257efc0e Update aws-sdk-go-v2 monorepo (#8525)
* Update aws-sdk-go-v2 monorepo

* 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-13 14:21:38 -07:00
adminandGitHub b155494b4d Move Manage Prisoners touch sizing into prefab (#8459) 2026-07-13 14:19:51 -07:00
adminandGitHub 04a51c9fb6 Cap leader relocation marches (#8524) 2026-07-13 14:13:51 -07:00
adminandGitHub ee5cf07db3 Reserve incoming leader destinations (#8523) 2026-07-13 14:13:15 -07:00
adminandGitHub 5c4470b090 Allow left info panels to shrink for alerts (#8467)
* Allow left info panels to shrink for alerts

* Resize left panels only during attacks

* Preserve panel height under attack
2026-07-13 14:10:32 -07:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>admin
86171cc1ae Lock file maintenance (#8514)
* Lock file maintenance

* Refresh Bazel lockfile for Cargo maintenance

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Dan Crosby <dan@danielcrosby.net>
2026-07-13 11:41:44 -07:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>admin
d5da95c5e3 Update dependency com.zaxxer:HikariCP to v7 (#8513)
* Update dependency com.zaxxer:HikariCP to v7

* Regenerate Maven lockfile after rebase

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Dan Crosby <dan@danielcrosby.net>
2026-07-13 11:35:02 -07:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
23c7385d8f Update actions/checkout action to v7 (#8512)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-13 09:30:01 -07:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>admin
cc5b6dbc75 Update dependency rules_rust to v0.71.3 (#8508)
* Update dependency rules_rust to v0.71.3

* Update generated dependency lockfiles

* Fix protobuf Rust crates for rules_rust 0.71

* Refresh rules_rust lockfile after rebase

---------

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>
Co-authored-by: Dan Crosby <dan@danielcrosby.net>
2026-07-13 09:29:43 -07:00
adminandGitHub 025bcbbff6 Instrument create-game phases in JFR (#8521) 2026-07-13 08:56:33 -07:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
fa6133f3ee Update module google.golang.org/grpc to v1.82.0 (#8511)
* Update module google.golang.org/grpc to v1.82.0

* 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-13 08:54: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>
af35d42792 Update dependency rules_cc to v0.2.22 (#8499)
* Update dependency rules_cc to v0.2.22

* 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-13 08:53:59 -07:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
d3f99ccada Update dependency org.scalameta:scalafmt-cli_2.13 to v3.11.3 (#8498)
* Update dependency org.scalameta:scalafmt-cli_2.13 to v3.11.3

* 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-13 08:53: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>
f4e3944716 Update dependency org.postgresql:postgresql to v42.7.13 (#8497)
* Update dependency org.postgresql:postgresql to v42.7.13

* 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-13 08:53:13 -07:00
adminandGitHub c67dc76ed5 Upgrade LLVM toolchain to 22.1.8 (#8520) 2026-07-13 08:40:23 -07:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
8bd9342e68 Update module golang.org/x/sys to v0.47.0 (#8510)
* Update module golang.org/x/sys to v0.47.0

* 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-13 08:26:24 -07:00
adminandGitHub 55c55adf8a Automate Bazel and LLVM Renovate updates (#8519) 2026-07-13 08:23:23 -07:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
3f6fee72b5 Update dependency rules_dotnet to v0.22.1 (#8507)
* Update dependency rules_dotnet to v0.22.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-13 08:22:19 -07:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
35d441d7f6 Update dependency com.thesamet.scalapb:protoc-bridge_3 to v0.9.10 (#8496)
* Update dependency com.thesamet.scalapb:protoc-bridge_3 to v0.9.10

* 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-13 08:20:49 -07:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
ddb49f411b Update dependency io.sentry:sentry to v8.48.0 (#8506)
* Update dependency io.sentry:sentry to v8.48.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-13 08:20:27 -07:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
130c0a5f37 Update dependency grpc-java to v1.82.0 (#8505)
* Update dependency grpc-java to v1.82.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-13 08:19:30 -07:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
9c3eb2d6e7 Update dependency grpc to v1.82.0 (#8504)
* Update dependency grpc to v1.82.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-13 08:19:01 -07:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
a84154bc74 Update dependency apple_support to v2.7.0 (#8503)
* Update dependency apple_support to v2.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-13 08:18:01 -07:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
08a5dc6104 Update dependency rules_nodejs to v6.7.5 (#8500)
* Update dependency rules_nodejs to v6.7.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-13 08:11:25 -07:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
33d963488b Update go toolchain directive to v1.26.5 (#8501)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-13 08:06:28 -07:00
adminandGitHub 2c3135a493 Render server-driven tutorial dialogue in Unity (#8487)
* Render server-driven tutorial dialogue in Unity

* Keep startup dialogue fallback available

* Revert "Keep startup dialogue fallback available"

This reverts commit 40ca3b35b3f29c29428f8a08b899ebda13f50dc0.

* Backfill dialogue text for tutorial games

* Restore tutorial runtime for split scenes
2026-07-13 07:38:15 -07:00
adminandGitHub b3b69474e4 Preprovision PostgreSQL game schemas (#8515) 2026-07-13 07:37:37 -07:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
ee74455b57 Update aws-sdk-go-v2 monorepo (#8495)
* Update aws-sdk-go-v2 monorepo

* 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-13 07:30:58 -07:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
bd7378a971 Update eclipse_temurin_25_jre:26-jre Docker digest to a0053c4 (#8494)
* Update eclipse_temurin_25_jre:26-jre Docker digest to a0053c4

* 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-13 07:26:53 -07:00
adminandGitHub 588fd4217a Sync Xcode before Unity CI builds (#8517) 2026-07-13 07:18:10 -07:00
adminandGitHub 1a60af2cd2 Expand Renovate Monday update window (#8509) 2026-07-13 07:14:39 -07:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
8398da469d Update Rust crate rand to v0.10.2 (#8502)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-13 07:14:22 -07:00
adminandGitHub f7280d37a6 Reset create button after returning to lobby (#8491)
* Reset create button after game entry

* Initialize create button label
2026-07-13 06:40:29 -07:00
adminandGitHub 1e98f4e3e8 Flush game creation acknowledgment before initialization (#8492) 2026-07-13 06:39: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>
3b203ac1b2 Update eclipse_temurin_25_jdk:26-jdk Docker digest to 8e04698 (#8493)
* Update eclipse_temurin_25_jdk:26-jdk Docker digest to 8e04698

* 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-13 06:35:06 -07:00
adminandGitHub 0ac3bd1e6e Acknowledge game creation before initialization (#8490) 2026-07-12 16:48:31 -07:00
adminandGitHub b56df59977 Batch marking generated texts requested (#8488) 2026-07-12 15:41:49 -07:00
adminandGitHub 441841ec41 Batch PostgreSQL schema initialization (#8486) 2026-07-12 14:34:15 -07:00
adminandGitHub b8ebd1d234 Batch PostgreSQL per-entity history writes (#8485) 2026-07-12 14:25:58 -07:00
adminandGitHub c1d9c53b30 Render tutorial dialogue in text client (#8458)
* Render server-driven tutorial dialogue in text client

* Render already-applied tutorial dialogue
2026-07-12 14:21:54 -07:00
adminandGitHub b4fbc7770a Show unknown battle troop counts (#8484) 2026-07-12 14:14:20 -07:00
adminandGitHub 065d5b466a Cache completed PostgreSQL client texts (#8483) 2026-07-12 14:11:49 -07:00
adminandGitHub 59a28f75e5 Separate bootstrap events from state updates (#8479) 2026-07-12 14:05:04 -07:00
adminandGitHub 86dc430ebc Batch resolved text visibility lookups (#8482) 2026-07-12 13:04:02 -07:00
adminandGitHub a062d6e8b2 Batch initial client text persistence (#8481) 2026-07-12 13:03:31 -07:00
adminandGitHub 04425edc34 Log failed resource fetch URLs (#8477) 2026-07-12 11:28:07 -07:00
adminandGitHub 5a622b276a Show drop confirmation on first click (#8480) 2026-07-12 11:27:34 -07:00
adminandGitHub 9c809d6453 Consider global quests from vassal provinces (#8478) 2026-07-12 11:25:06 -07:00
adminandGitHub 2838e8b280 Revert "Deliver game start during initial sync (#8461)" (#8476)
This reverts commit 6c219a6d43.
2026-07-12 10:49:13 -07:00
adminandGitHub 3b03f3384a Prioritize specific expansion quest targets (#8475)
* Prioritize specific expansion quest targets

* Use faction province helper for quest targets
2026-07-12 10:39:43 -07:00
adminandGitHub 6c219a6d43 Deliver game start during initial sync (#8461) 2026-07-12 10:35:12 -07:00
adminandGitHub 062c19dacf Keep province title clear of header controls (#8473)
* Keep province title clear of header controls

* Prevent province title wrapping

* Move province status icons beside support
2026-07-12 10:32:53 -07:00
adminandGitHub 82b9c0f7df Clear live province intelligence dates (#8474) 2026-07-12 07:39:15 -07:00
adminandGitHub 13cc93345a Make AI rest decisions account for hero depth (#8472) 2026-07-11 21:41:18 -07:00
adminandGitHub a5652e43f2 Prefer training over resting in development fallbacks (#8470) 2026-07-11 21:40:52 -07:00
578 changed files with 30357 additions and 15165 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"
+1 -1
View File
@@ -1 +1 @@
9.1.1
9.2.0
+2 -2
View File
@@ -79,7 +79,7 @@ jobs:
rm -f .git/hooks/post-checkout .git/hooks/post-merge .git/hooks/pre-push
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7
env:
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig-no-lfs
GIT_CONFIG_NOSYSTEM: "1"
@@ -182,7 +182,7 @@ jobs:
FASTMAIL_FROM_NAME: ${{ secrets.FASTMAIL_FROM_NAME }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
persist-credentials: false
@@ -51,7 +51,7 @@ jobs:
brew install git-lfs
git-lfs version
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
persist-credentials: false
lfs: false
@@ -94,7 +94,7 @@ jobs:
brew install git-lfs
git-lfs version
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
persist-credentials: false
lfs: false
@@ -109,7 +109,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
persist-credentials: false
lfs: false
+3 -3
View File
@@ -74,14 +74,14 @@ jobs:
brew install git-lfs
git-lfs version
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
persist-credentials: false
lfs: false
- name: Check BUILD.bazel dependencies
run: ./scripts/check_build_deps.sh --strict
- name: Set up Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v7
with:
node-version: 'lts/*'
- name: Check JavaScript syntax
@@ -116,7 +116,7 @@ jobs:
brew install git-lfs
git-lfs version
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
persist-credentials: false
lfs: false
+1 -1
View File
@@ -31,7 +31,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
persist-credentials: false
+10 -2
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,
@@ -116,7 +121,7 @@ jobs:
- name: Checkout repository
if: steps.check-latest.outputs.skip != 'true'
uses: actions/checkout@v6
uses: actions/checkout@v7
env:
GIT_LFS_SKIP_SMUDGE: 1
with:
@@ -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/*"
@@ -325,7 +333,7 @@ jobs:
git-lfs --version
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7
env:
GIT_LFS_SKIP_SMUDGE: 1
with:
+1 -1
View File
@@ -56,7 +56,7 @@ jobs:
rm -f .git/hooks/post-checkout .git/hooks/post-merge .git/hooks/pre-push
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7
env:
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig-no-lfs
GIT_CONFIG_NOSYSTEM: "1"
+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
+1 -1
View File
@@ -44,7 +44,7 @@ jobs:
brew install git-lfs
git-lfs version
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
persist-credentials: false
lfs: false
+10 -2
View File
@@ -138,7 +138,7 @@ jobs:
fi
git-lfs --version
- uses: actions/checkout@v6
- uses: actions/checkout@v7
env:
GIT_LFS_SKIP_SMUDGE: 1
with:
@@ -154,12 +154,20 @@ 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 ios
- name: Ensure Bazel installed
uses: ./.github/actions/setup-bazel
- name: Sync Bazel Xcode config
run: ./scripts/sync_bazel_xcode.sh
- name: Build iOS Unity Project
id: build
run: |
@@ -229,7 +237,7 @@ jobs:
fi
git-lfs --version
- uses: actions/checkout@v6
- uses: actions/checkout@v7
env:
GIT_LFS_SKIP_SMUDGE: 1
with:
+5 -3
View File
@@ -113,7 +113,7 @@ jobs:
fi
git-lfs --version
- uses: actions/checkout@v6
- uses: actions/checkout@v7
env:
GIT_LFS_SKIP_SMUDGE: 1
with:
@@ -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'
@@ -287,7 +289,7 @@ jobs:
fi
git-lfs --version
- uses: actions/checkout@v6
- uses: actions/checkout@v7
env:
GIT_LFS_SKIP_SMUDGE: 1
with:
@@ -4,17 +4,20 @@ on:
pull_request:
types: [opened, reopened, synchronize, ready_for_review]
paths:
- '.bazelversion'
- 'MODULE.bazel'
- 'MODULE.bazel.lock'
- 'maven_install.json'
- 'renovate.json'
- 'ci/github_actions/ensure_bazel_installed.sh'
- 'ci/github_actions/update_llvm_distributions.py'
- '.github/actions/setup-bazel/**'
- '.github/workflows/renovate_dependency_artifacts.yml'
pull_request_target:
types: [opened, reopened, synchronize, ready_for_review]
paths:
- '.bazelversion'
- 'MODULE.bazel'
- 'MODULE.bazel.lock'
- 'maven_install.json'
@@ -22,6 +25,7 @@ on:
- 'go.sum'
- 'renovate.json'
- 'ci/github_actions/ensure_bazel_installed.sh'
- 'ci/github_actions/update_llvm_distributions.py'
- '.github/actions/setup-bazel/**'
- '.github/workflows/renovate_dependency_artifacts.yml'
@@ -58,11 +62,13 @@ jobs:
const changed = files.map((file) => file.filename);
const bazelPatterns = [
/^\.bazelversion$/,
/^MODULE\.bazel$/,
/^MODULE\.bazel\.lock$/,
/^maven_install\.json$/,
/^renovate\.json$/,
/^ci\/github_actions\/ensure_bazel_installed\.sh$/,
/^ci\/github_actions\/update_llvm_distributions\.py$/,
/^\.github\/actions\/setup-bazel\//,
/^\.github\/workflows\/renovate_dependency_artifacts\.yml$/,
];
@@ -87,7 +93,7 @@ jobs:
include.push({
artifact: 'bazel',
name: 'Bazel lockfiles',
changed_files: 'MODULE.bazel.lock maven_install.json',
changed_files: 'MODULE.bazel MODULE.bazel.lock maven_install.json',
commit_message: 'Update generated dependency lockfiles',
});
}
@@ -111,13 +117,18 @@ jobs:
steps:
- name: Checkout PR
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
lfs: false
- name: Ensure Bazel installed
uses: ./.github/actions/setup-bazel
- name: Refresh LLVM distribution metadata
env:
GITHUB_TOKEN: ${{ github.token }}
run: python3 ci/github_actions/update_llvm_distributions.py
- name: Update Maven lockfile
env:
REPIN: "1"
@@ -128,15 +139,16 @@ jobs:
- name: Verify generated lockfiles are current
run: |
if git diff --quiet -- MODULE.bazel.lock maven_install.json; then
if git diff --quiet -- MODULE.bazel MODULE.bazel.lock maven_install.json; then
echo "Generated dependency lockfiles are current"
exit 0
fi
echo "Generated dependency lockfiles are stale. Run the lockfile update commands and commit the result:"
echo " python3 ci/github_actions/update_llvm_distributions.py"
echo " REPIN=1 bazel run @maven//:pin"
echo " bazel build --nobuild //src/main/scala/net/eagle0/eagle:eagle_server //src/main/cpp/net/eagle0/shardok:shardok-server"
git diff -- MODULE.bazel.lock maven_install.json
git diff -- MODULE.bazel MODULE.bazel.lock maven_install.json
exit 1
update-artifacts:
@@ -173,13 +185,19 @@ jobs:
git-lfs version
- name: Checkout Renovate branch
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
ref: ${{ github.event.pull_request.head.ref }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
token: ${{ secrets.RENOVATE_LOCKFILE_TOKEN || github.token }}
lfs: false
- name: Refresh LLVM distribution metadata
if: matrix.artifact == 'bazel'
env:
GITHUB_TOKEN: ${{ github.token }}
run: python3 ci/github_actions/update_llvm_distributions.py
- name: Ensure Bazel installed
if: matrix.artifact == 'bazel'
uses: ./.github/actions/setup-bazel
@@ -196,7 +214,7 @@ jobs:
- name: Set up Go
if: matrix.artifact == 'go'
uses: actions/setup-go@v6
uses: actions/setup-go@v7
with:
go-version-file: go.mod
cache: false
+1 -1
View File
@@ -143,7 +143,7 @@ jobs:
brew install git-lfs
git-lfs version
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
persist-credentials: false
lfs: false
+33 -3
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'
@@ -57,7 +59,7 @@ jobs:
rm -f .git/hooks/post-checkout .git/hooks/post-merge .git/hooks/pre-push
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7
env:
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig-no-lfs
GIT_CONFIG_NOSYSTEM: "1"
@@ -68,12 +70,18 @@ jobs:
- name: Ensure Bazel installed
uses: ./.github/actions/setup-bazel
- name: Build Shardok ARM64 binary (cross-compile for Linux ARM64)
- name: Build optimized, profileable Shardok ARM64 binary
run: |
set -ex
echo "=== Building shardok-server binary for linux-aarch64 ==="
bazel build \
-c opt \
--strip=never \
--copt=-gline-tables-only \
--copt=-fno-omit-frame-pointer \
--copt=-mno-omit-leaf-frame-pointer \
--linkopt=-Wl,--build-id=sha1 \
--platforms=//:linux_arm64 \
--extra_toolchains=@llvm_toolchain_linux_arm64//:cc-toolchain-aarch64-linux \
//src/main/cpp/net/eagle0/shardok:shardok-server
@@ -103,6 +111,15 @@ jobs:
else
echo "WARNING: Binary e_machine is $E_MACHINE (expected 00b7 for aarch64)"
fi
BINARY_DESCRIPTION=$(file "$LINUX_BIN")
echo "Binary description: $BINARY_DESCRIPTION"
for REQUIRED_TEXT in "ARM aarch64" "BuildID" "with debug_info" "not stripped"; do
if [[ "$BINARY_DESCRIPTION" != *"$REQUIRED_TEXT"* ]]; then
echo "ERROR: Profileable release binary is missing '$REQUIRED_TEXT'"
exit 1
fi
done
elif [ "$MAGIC" = "cfaeedfe" ] || [ "$MAGIC" = "cffaedfe" ]; then
echo "ERROR: Binary is Mach-O format (macOS) - cross-compilation failed!"
exit 1
@@ -111,13 +128,19 @@ jobs:
file "$LINUX_BIN" || true
fi
- name: Build Shardok ARM64 Docker image
- name: Build optimized, profileable Shardok ARM64 Docker image
id: build-shardok
run: |
set -ex
# Keep this in sync with the standalone binary build above.
bazel build \
-c opt \
--strip=never \
--copt=-gline-tables-only \
--copt=-fno-omit-frame-pointer \
--copt=-mno-omit-leaf-frame-pointer \
--linkopt=-Wl,--build-id=sha1 \
--platforms=//:linux_arm64 \
--extra_toolchains=@llvm_toolchain_linux_arm64//:cc-toolchain-aarch64-linux \
//ci:shardok_server_image_arm64
@@ -262,6 +285,12 @@ jobs:
echo "Loaded image ID: $(docker image inspect --format '{{.Id}}' "$SHARDOK_IMAGE")"
echo "Shardok binary SHA256:"
docker run --rm --entrypoint sha256sum "$SHARDOK_IMAGE" /app/shardok-server
echo "Verifying perf capture permissions:"
docker run --rm \
--cap-add PERFMON \
--entrypoint perf \
"$SHARDOK_IMAGE" \
stat --event cpu-clock:u -- perf version
# Stop and remove any container using port 40042 or named shardok*
docker ps -q --filter "publish=40042" | xargs -r docker stop
@@ -272,6 +301,7 @@ jobs:
docker run -d \
--name shardok-ai \
--restart unless-stopped \
--cap-add PERFMON \
-p 40042:40042 \
-v /opt/eagle0/data:/data \
-v /etc/shardok:/etc/shardok:ro \
+1 -1
View File
@@ -72,7 +72,7 @@ jobs:
brew install git-lfs
git-lfs version
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
persist-credentials: false
lfs: false
+1 -1
View File
@@ -47,7 +47,7 @@ jobs:
fi
rm -f .git/hooks/post-checkout .git/hooks/post-merge .git/hooks/pre-push
- uses: actions/checkout@v6
- uses: actions/checkout@v7
env:
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig-no-lfs
GIT_CONFIG_NOSYSTEM: "1"
+14 -4
View File
@@ -14,6 +14,7 @@ on:
- "scripts/build_protos.sh"
- "scripts/build_plugins.sh"
- "scripts/build_windows_plugin.sh"
- "scripts/sync_bazel_xcode.sh"
- "ci/github_actions/build_unity.sh"
- "ci/github_actions/test_unity_editmode.sh"
- "ci/github_actions/ensure_bazel_installed.sh"
@@ -39,6 +40,7 @@ on:
- "scripts/build_protos.sh"
- "scripts/build_plugins.sh"
- "scripts/build_windows_plugin.sh"
- "scripts/sync_bazel_xcode.sh"
- "ci/github_actions/build_unity.sh"
- "ci/github_actions/test_unity_editmode.sh"
- "ci/github_actions/ensure_bazel_installed.sh"
@@ -94,7 +96,7 @@ jobs:
fi
git-lfs --version
- uses: actions/checkout@v6
- uses: actions/checkout@v7
env:
GIT_LFS_SKIP_SMUDGE: 1
with:
@@ -146,16 +148,20 @@ 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
uses: ./.github/actions/setup-bazel
- name: Sync Bazel Xcode configuration
run: ./scripts/sync_bazel_xcode.sh
- name: Run Unity EditMode tests
run: ./ci/github_actions/test_unity_editmode.sh
@@ -209,7 +215,7 @@ jobs:
fi
git-lfs --version
- uses: actions/checkout@v6
- uses: actions/checkout@v7
env:
GIT_LFS_SKIP_SMUDGE: 1
with:
@@ -261,16 +267,20 @@ 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
uses: ./.github/actions/setup-bazel
- name: Sync Bazel Xcode configuration
run: ./scripts/sync_bazel_xcode.sh
- name: Detect Addressables changes
id: addressables
+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
+39 -28
View File
@@ -17,15 +17,15 @@ SLF4J_VERSION = "2.0.18"
# Core Build Tools
#
bazel_dep(name = "bazel_skylib", version = "1.9.0")
bazel_dep(name = "bazel_skylib", version = "1.9.2")
bazel_dep(name = "aspect_rules_esbuild", version = "0.26.0")
bazel_dep(name = "aspect_rules_js", version = "3.2.2")
bazel_dep(name = "aspect_rules_js", version = "3.3.0")
bazel_dep(name = "bazel_jar_jar", version = "0.1.15", repo_name = "bazel_jar_jar")
bazel_dep(name = "platforms", version = "1.1.0")
bazel_dep(name = "rules_dotnet", version = "0.21.5")
bazel_dep(name = "rules_dotnet", version = "0.22.1")
bazel_dep(name = "rules_foreign_cc", version = "0.15.1")
bazel_dep(name = "rules_android", version = "0.7.3")
bazel_dep(name = "rules_nodejs", version = "6.7.4")
bazel_dep(name = "rules_nodejs", version = "6.7.5")
bazel_dep(name = "rules_pkg", version = "1.2.0")
bazel_dep(name = "rules_shell", version = "0.8.0")
@@ -33,7 +33,7 @@ multiple_version_override(
module_name = "aspect_rules_js",
versions = [
"2.3.8",
"3.2.2",
"3.3.0",
],
)
@@ -74,12 +74,12 @@ bazel_dep(name = "toolchains_llvm", version = "1.8.0")
llvm = use_extension("@toolchains_llvm//toolchain/extensions:llvm.bzl", "llvm")
LLVM_VERSION = "22.1.7"
LLVM_VERSION = "22.1.8"
LLVM_DISTRIBUTIONS = {
"LLVM-22.1.7-Linux-ARM64.tar.xz": "118ca2d3ad9da34367e05735317854e7977db45dc4c02a32af58da64c23b8789",
"LLVM-22.1.7-Linux-X64.tar.xz": "edb0522b41e261819c06ea437d249f9b8acfa413d3805bc9920eec6fb76ff830",
"LLVM-22.1.7-macOS-ARM64.tar.xz": "4177245188b0a30a6539c96b361dea56f253485756bfd8927a6a59e7301e7806",
"LLVM-22.1.8-Linux-ARM64.tar.xz": "805efad2bb91cb4967fa569e0881d10c0f69c04461cf671cccbae19f547acc34",
"LLVM-22.1.8-Linux-X64.tar.xz": "df0e1ecf16caf3489a272a5eea4eec9b0d82878f6477fa309504f918a0006384",
"LLVM-22.1.8-macOS-ARM64.tar.xz": "f260f4f7c0d430828a81ae8a3826a1d63fc0963ec2459489308cc23b1f7eab4f",
}
# Native toolchain (macOS -> macOS, Linux -> Linux)
@@ -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.70.0")
bazel_dep(name = "rules_rust", version = "0.72.0")
rust = use_extension("@rules_rust//rust:extensions.bzl", "rust")
rust.toolchain(edition = "2021")
@@ -177,7 +177,7 @@ use_repo(rust, "rust_toolchains")
register_toolchains("@rust_toolchains//:all")
crate = use_extension("@rules_rust//crate_universe:extension.bzl", "crate")
crate = use_extension("@rules_rust//crate_universe:extensions.bzl", "crate")
crate.from_cargo(
name = "map_generator_crates",
cargo_lockfile = "//src/main/rust/net/eagle0/eagle/map_generator:Cargo.lock",
@@ -189,7 +189,7 @@ use_repo(crate, "map_generator_crates")
# Platform Support - Apple/iOS
#
bazel_dep(name = "apple_support", version = "2.6.1", repo_name = "build_bazel_apple_support")
bazel_dep(name = "apple_support", version = "2.8.0", repo_name = "build_bazel_apple_support")
bazel_dep(name = "rules_apple", version = "4.5.3", repo_name = "build_bazel_rules_apple")
multiple_version_override(
@@ -214,6 +214,8 @@ use_repo(apple_cc_configure, "local_config_apple_cc", "local_config_apple_cc_too
bazel_dep(name = "protobuf", version = "35.1", repo_name = "com_google_protobuf")
single_version_override(
module_name = "protobuf",
patch_strip = 1,
patches = ["//third_party/protobuf/patches:rust_crates_dev_dependency.patch"],
version = "35.1",
)
@@ -234,10 +236,10 @@ use_repo(
)
bazel_dep(name = "rules_proto", version = "7.1.0")
bazel_dep(name = "rules_cc", version = "0.2.19")
bazel_dep(name = "grpc", version = "1.81.1")
bazel_dep(name = "rules_cc", version = "0.2.22")
bazel_dep(name = "grpc", version = "1.83.0")
# gRPC 1.81.1's generated upb module map trips Clang's layering check in the
# gRPC's generated upb module map trips Clang's layering check in the
# Linux ARM64 cross-toolchain even though the normal Bazel deps are present.
single_version_override(
module_name = "grpc",
@@ -245,7 +247,7 @@ single_version_override(
patches = ["//third_party/grpc/patches:disable_xds_client_layering_check.patch"],
)
bazel_dep(name = "grpc-java", version = "1.78.0.bcr.1")
bazel_dep(name = "grpc-java", version = "1.82.0")
bazel_dep(name = "rules_proto_grpc_csharp", version = "5.8.0")
bazel_dep(name = "flatbuffers", version = "25.12.19")
@@ -261,16 +263,25 @@ bazel_dep(name = "googletest", version = "1.17.0.bcr.2")
bazel_dep(name = "rules_oci", version = "2.3.0")
bazel_dep(name = "aspect_bazel_lib", version = "2.22.5")
bazel_dep(name = "rules_distroless", version = "0.8.0")
oci = use_extension("@rules_oci//oci:extensions.bzl", "oci")
apt = use_extension("@rules_distroless//apt:extensions.bzl", "apt")
apt.install(
lock = "//ci:shardok_perf.lock.json",
name = "shardok_perf",
manifest = "//ci:shardok_perf.yaml",
)
use_repo(apt, "shardok_perf")
# Base image for Eagle (Java 25 JRE - smaller runtime, no dev tools needed)
# Digest pinned for Bazel repository cache hit; update with:
# TOKEN=$(curl -s "https://auth.docker.io/token?service=registry.docker.io&scope=repository:library/eclipse-temurin:pull" | python3 -c "import json,sys; print(json.load(sys.stdin)['token'])")
# curl -s -H "Authorization: Bearer $TOKEN" -H "Accept: application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.list.v2+json" "https://registry-1.docker.io/v2/library/eclipse-temurin/manifests/25-jre" | python3 -c "import json,sys; m=json.load(sys.stdin); [print(p['digest']) for p in m.get('manifests',[]) if p['platform']['architecture']=='amd64']"
oci.pull(
name = "eclipse_temurin_25_jre",
digest = "sha256:0beb258e10ad28d2bae0f74349a170bdfa67da0ef87c028cbf3d95c3e1170120",
digest = "sha256:882787e465d6c0c4c08daa8d9e209d57ec052228eff8244c959f271af24ad27b",
image = "docker.io/library/eclipse-temurin",
platforms = ["linux/amd64"],
tag = "26-jre",
@@ -279,7 +290,7 @@ oci.pull(
# Base image for JFR sidecar (Java 25 JDK - includes jcmd for JFR dumps)
oci.pull(
name = "eclipse_temurin_25_jdk",
digest = "sha256:41c3a5b8a607d226c73e81538cd94d0e66f746f41f057561447eb47b40a91a65",
digest = "sha256:625889835aab66a096f999ee16cd7061ac846b155dc7adbed693686bbae5c59a",
image = "docker.io/library/eclipse-temurin",
platforms = ["linux/amd64"],
tag = "26-jdk",
@@ -310,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(
@@ -334,7 +345,7 @@ maven.install(
"com.thesamet.scalapb:scalapb-runtime_3:%s" % SCALAPB_VERSION,
"com.thesamet.scalapb:scalapb-runtime-grpc_3:%s" % SCALAPB_VERSION,
"com.thesamet.scalapb:compilerplugin_3:%s" % SCALAPB_VERSION,
"com.thesamet.scalapb:protoc-bridge_3:0.9.9",
"com.thesamet.scalapb:protoc-bridge_3:0.9.10",
# JSON
"org.json4s:json4s-ast_3:4.1.0-M8",
@@ -345,7 +356,7 @@ maven.install(
"org.scalamock:scalamock_3:7.5.5",
# Developer tools
"org.scalameta:scalafmt-cli_2.13:3.11.1",
"org.scalameta:scalafmt-cli_2.13:3.11.5",
# AWS SDK
"software.amazon.awssdk:s3-transfer-manager:%s" % AWS_SDK_VERSION,
@@ -376,14 +387,14 @@ maven.install(
"com.nimbusds:nimbus-jose-jwt:10.9.1",
# Error tracking
"io.sentry:sentry:8.43.2",
"io.sentry:sentry:8.50.1",
# SQLite for client text storage
"org.xerial:sqlite-jdbc:3.53.2.0",
"org.xerial:sqlite-jdbc:3.53.2.1",
# Postgres for production history benchmarking and migration work
"com.zaxxer:HikariCP:6.3.3",
"org.postgresql:postgresql:42.7.11",
"com.zaxxer:HikariCP:7.1.0",
"org.postgresql:postgresql:42.7.13",
],
duplicate_version_warning = "error",
fail_if_repin_required = True,
+102 -2299
View File
File diff suppressed because one or more lines are too long
+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",
+39
View File
@@ -46,6 +46,45 @@ fi
SCHEME="Unity-iPhone"
echo "Using scheme: $SCHEME"
# Xcode can be upgraded on a self-hosted runner before its matching iOS platform
# component is installed. In that state, xcrun may exist but the generic iOS
# destination is ineligible, and archive fails with exit code 70. Probe the exact
# destination used below and repair the Xcode installation before starting the
# expensive archive.
if ! xcodebuild \
-project "$XCODEPROJ" \
-scheme "$SCHEME" \
-showBuildSettings \
-destination "generic/platform=iOS" >/dev/null; then
echo "The generic iOS destination is unavailable; installing Xcode's iOS platform..."
if ! xcodebuild -checkFirstLaunchStatus; then
echo "Completing pending Xcode first-launch setup..."
xcodebuild -runFirstLaunch
fi
xcodebuild -downloadPlatform iOS
fi
echo "Installed Xcode SDKs:"
xcodebuild -showsdks
IOS_SDK_PATH=$(xcrun --sdk iphoneos --show-sdk-path)
if [ ! -d "$IOS_SDK_PATH" ]; then
echo "Error: Xcode reported an invalid iOS SDK path: $IOS_SDK_PATH"
exit 1
fi
echo "Using iOS SDK: $IOS_SDK_PATH"
if ! xcodebuild \
-project "$XCODEPROJ" \
-scheme "$SCHEME" \
-showBuildSettings \
-destination "generic/platform=iOS" >/dev/null; then
echo "Error: The generic iOS destination is still unavailable after platform setup."
xcodebuild -project "$XCODEPROJ" -scheme "$SCHEME" -showdestinations || true
exit 1
fi
# Archive without signing - we'll sign during export
# This avoids issues with provisioning profiles on framework targets
xcodebuild archive \
+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
@@ -0,0 +1,137 @@
#!/usr/bin/env python3
"""Update pinned LLVM release asset names and SHA-256 digests in MODULE.bazel."""
from __future__ import annotations
import argparse
import json
import os
import re
import sys
import urllib.request
from pathlib import Path
from typing import Any
LLVM_VERSION_PATTERN = re.compile(r'^LLVM_VERSION = "(?P<version>\d+\.\d+\.\d+)"$', re.MULTILINE)
LLVM_DISTRIBUTIONS_PATTERN = re.compile(
r"^LLVM_DISTRIBUTIONS = \{\n.*?^\}$",
re.MULTILINE | re.DOTALL,
)
LLVM_DISTRIBUTION_PLATFORMS = (
"Linux-ARM64",
"Linux-X64",
"macOS-ARM64",
)
def fetch_release(version: str, token: str | None) -> dict[str, Any]:
url = f"https://api.github.com/repos/llvm/llvm-project/releases/tags/llvmorg-{version}"
headers = {
"Accept": "application/vnd.github+json",
"User-Agent": "eagle0-llvm-distribution-updater",
"X-GitHub-Api-Version": "2022-11-28",
}
if token:
headers["Authorization"] = f"Bearer {token}"
request = urllib.request.Request(url, headers=headers)
with urllib.request.urlopen(request, timeout=30) as response:
return json.load(response)
def load_release(path: Path) -> dict[str, Any]:
with path.open(encoding="utf-8") as release_file:
return json.load(release_file)
def llvm_distributions(version: str, release: dict[str, Any]) -> dict[str, str]:
expected_tag = f"llvmorg-{version}"
if release.get("tag_name") != expected_tag:
raise ValueError(
f"Expected release tag {expected_tag}, got {release.get('tag_name')!r}"
)
assets = {asset["name"]: asset for asset in release.get("assets", [])}
distributions: dict[str, str] = {}
for platform in LLVM_DISTRIBUTION_PLATFORMS:
name = f"LLVM-{version}-{platform}.tar.xz"
asset = assets.get(name)
if asset is None:
raise ValueError(f"LLVM release {version} is missing required asset {name}")
digest = asset.get("digest")
if not isinstance(digest, str) or not digest.startswith("sha256:"):
raise ValueError(f"LLVM release asset {name} has no SHA-256 digest")
distributions[name] = digest.removeprefix("sha256:")
return distributions
def render_distributions(distributions: dict[str, str]) -> str:
lines = ["LLVM_DISTRIBUTIONS = {"]
lines.extend(f' "{name}": "{digest}",' for name, digest in distributions.items())
lines.append("}")
return "\n".join(lines)
def update_module(module_path: Path, release: dict[str, Any]) -> bool:
original = module_path.read_text(encoding="utf-8")
version_match = LLVM_VERSION_PATTERN.search(original)
if version_match is None:
raise ValueError(f"Could not find LLVM_VERSION in {module_path}")
distributions = llvm_distributions(version_match.group("version"), release)
replacement = render_distributions(distributions)
updated, replacements = LLVM_DISTRIBUTIONS_PATTERN.subn(replacement, original, count=1)
if replacements != 1:
raise ValueError(f"Could not find exactly one LLVM_DISTRIBUTIONS block in {module_path}")
if updated == original:
return False
module_path.write_text(updated, encoding="utf-8")
return True
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("--module", type=Path, default=Path("MODULE.bazel"))
parser.add_argument(
"--release-json",
type=Path,
help="Read GitHub release JSON from a file instead of the GitHub API",
)
return parser.parse_args()
def main() -> int:
args = parse_args()
module_text = args.module.read_text(encoding="utf-8")
version_match = LLVM_VERSION_PATTERN.search(module_text)
if version_match is None:
print(f"ERROR: Could not find LLVM_VERSION in {args.module}", file=sys.stderr)
return 1
version = version_match.group("version")
try:
release = (
load_release(args.release_json)
if args.release_json
else fetch_release(version, os.environ.get("GITHUB_TOKEN"))
)
changed = update_module(args.module, release)
except (OSError, ValueError) as error:
print(f"ERROR: {error}", file=sys.stderr)
return 1
if changed:
print(f"Updated LLVM {version} distribution metadata in {args.module}")
else:
print(f"LLVM {version} distribution metadata is current")
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,54 @@
#!/usr/bin/env python3
import tempfile
import unittest
from pathlib import Path
import update_llvm_distributions
class UpdateLlvmDistributionsTest(unittest.TestCase):
def release(self, *, missing_digest: bool = False) -> dict:
assets = []
for index, platform in enumerate(
update_llvm_distributions.LLVM_DISTRIBUTION_PLATFORMS, start=1
):
asset = {
"name": f"LLVM-22.1.8-{platform}.tar.xz",
"digest": f"sha256:{index:064x}",
}
assets.append(asset)
if missing_digest:
assets[0]["digest"] = None
return {"tag_name": "llvmorg-22.1.8", "assets": assets}
def test_updates_asset_versions_and_digests(self) -> None:
with tempfile.TemporaryDirectory() as directory:
module = Path(directory) / "MODULE.bazel"
module.write_text(
'LLVM_VERSION = "22.1.8"\n\n'
"LLVM_DISTRIBUTIONS = {\n"
' "LLVM-22.1.7-Linux-ARM64.tar.xz": "old-arm",\n'
' "LLVM-22.1.7-Linux-X64.tar.xz": "old-x64",\n'
' "LLVM-22.1.7-macOS-ARM64.tar.xz": "old-mac",\n'
"}\n",
encoding="utf-8",
)
changed = update_llvm_distributions.update_module(module, self.release())
self.assertTrue(changed)
updated = module.read_text(encoding="utf-8")
self.assertNotIn("22.1.7", updated)
self.assertIn('"LLVM-22.1.8-Linux-ARM64.tar.xz": "' + "1".zfill(64), updated)
self.assertIn('"LLVM-22.1.8-macOS-ARM64.tar.xz": "' + "3".zfill(64), updated)
def test_rejects_asset_without_sha256_digest(self) -> None:
with self.assertRaisesRegex(ValueError, "has no SHA-256 digest"):
update_llvm_distributions.llvm_distributions(
"22.1.8", self.release(missing_digest=True)
)
if __name__ == "__main__":
unittest.main()
File diff suppressed because it is too large Load Diff
+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
+1 -1
View File
@@ -156,7 +156,7 @@ CREATE TABLE action_changed_provinces (
support_delta REAL,
-- round properties
set_has_acted INTEGER, -- 0/1/NULL
set_ruler_is_traveling INTEGER,
set_ruler_is_visiting_town INTEGER,
-- ruling faction changes
clear_ruling_faction_id INTEGER NOT NULL DEFAULT 0,
new_ruling_faction_id INTEGER,
+45 -9
View File
@@ -4,20 +4,18 @@ This guide covers operating the Eagle text client against a running Eagle server
## Authentication And Startup
The current bearer-token file is:
```
/private/tmp/eagle0-text-client-token.txt
```
If that file is absent or authentication fails, stop and ask the user for a current bearer token. Do not invent a token or use another user's credentials.
The text client can request and refresh its own tokens. GitHub is the recommended OAuth provider:
Start the production client with:
```bash
bazel run //src/main/scala/net/eagle0/eagle/text_client:eagle_text_client -- --tls --host prod.eagle0.net --port 443 --bearer-token-file /private/tmp/eagle0-text-client-token.txt
bazel run //src/main/scala/net/eagle0/eagle/text_client:eagle_text_client -- --tls --host prod.eagle0.net --port 443 --oauth
```
On the first run, or when no usable refresh token remains, the client prints a GitHub login URL. Paste that URL to the user and wait for them to authenticate in their browser. The callback returns to a temporary loopback listener in the text client, not to the Unity client. Never open the URL or choose an account on the user's behalf.
The client stores the access and refresh tokens in `~/.eagle0/text-client/` with owner-only permissions and refreshes the access token automatically. For an existing token location, pass both `--bearer-token-file <path>` and `--refresh-token-file <path>`. Use `--oauth-provider <provider>` only if the account uses `google`, `discord`, `apple`, `steam`, or `twitch` instead of the recommended `github` provider.
At the `eagle0>` prompt, use `lobby` to list running games and available new-game leaders. To resume a game, use:
```text
@@ -74,7 +72,7 @@ Only post a selected command that is currently listed as available. A successful
Some actions enter a temporary subcommand state. For example, trading is operated as:
```text
post-json <province-id> TravelSelectedCommand {}
post-json <province-id> VisitTownSelectedCommand {}
post-json <province-id> TradeSelectedCommand {"tradeType":"BUY_FOOD","amount":<amount>}
post-return <province-id>
```
@@ -133,6 +131,44 @@ Shardok update with no available commands clears the previous command list.
Use `shardok-commands-json` or `shardok-units-json` when the concise output omits a needed field.
The initial Eagle state includes the complete strategic province graph. Inspect it with:
```text
eagle-map
eagle-map-json
```
Both forms list every province and all of its neighbors by ID and name. Use the JSON form when exact IDs or
machine-readable relationships are needed; unlike March destinations, this is the complete map graph and is not
filtered by the commands currently available.
During battle setup, `shardok-commands` prints any server-recommended unit placements in order and identifies the
ordinary current command index for the next recommendation:
```text
recommended-starting-positions (ordered):
[0] unit=12 target=(3,4)
[1] unit=15 target=(4,4)
use next: post-shardok <shardok-game-id> 17
```
The list is advice, not an automatic placement batch. Review it against `shardok-map`, choose whether to use the first
recommendation, and submit that one placement with the printed `post-shardok` command. After every placement, wait for
the updated state and run `shardok-commands` again; the legal command indices and remaining recommendations may change.
The client synchronizes Shardok map definitions when it connects. To inspect the
base terrain together with current tile modifiers and occupying units, use:
```text
shardok-map <shardok-game-id>
shardok-map-json <shardok-game-id>
```
The battle ID may be omitted when the client knows about exactly one Shardok
battle. The concise map prints every coordinate's terrain plus fire, castle,
bridge, ice, and snow state. The JSON form includes the complete base map,
dynamic tile modifiers, and current active units.
## Reconnects And Shutdown
The client automatically recreates a failed gRPC stream and re-subscribes to the active game. It retains its in-process `ClientTextStore`, so a reconnect should announce:
+210
View File
@@ -0,0 +1,210 @@
# Tutorial Playthrough Notes
This is a working guide for playing through the Eagle0 tutorial without losing. It is written from live text-client
testing, so treat it as a practical checklist rather than a complete design reference.
## Core Rule
Do not risk Sadar Rakon unless the outcome is overwhelmingly favorable. In tutorial games, Sadar is the human faction
leader; if he is captured, the server treats the human player as defeated and the game ends.
## Strategic Goals
- Survive the opening tutorial battle.
- Rebuild Onmaa enough to stabilize support and loyalty.
- Expand carefully from Onmaa/Nikemi with enough force to avoid leader capture.
- Trigger the major strategic tutorial event where The Eagle appears. In code, this happens when Sadar controls at
least four provinces or Hedrick's faction has no provinces.
## Tactical Safety Rules
- Avoid sending Sadar alone into a tactical battle.
- If a battle turns into a long chase, reassess instead of blindly sweeping the map.
- Prefer preserving Sadar over forcing a marginal province capture.
- When the text client shows no attack command, do not assume the enemy is helpless; they may still be able to win by
holding, hiding, or outlasting the attacker.
## Known Failure
In one previous Kojaria attempt, Sadar attacked with one battalion, chased Hedrick across the map, failed to force a
decisive win, and was captured. The client did not clearly announce game over at the time; the save later showed Kojaria
held by Hedrick and Sadar listed under `captured_heroes`.
## Live Run Log
### Fresh Tutorial Run
- Started: game `-3568010721410464682`.
- Leader: Sadar Rakon.
- Current objective: start the tutorial from the opening battle and record safe choices.
- Opening battle setup:
- Rakon's Loyalists at `(8,10)`.
- Onmaa Defenders at `(7,10)`.
- Hunters of the Steppe at `(8,11)`.
- Rationale: keep the two infantry units together on the west-facing line and keep the longbowmen directly behind
them. Tarn's force begins west/southwest of the defender setup area, and Elena/John arrive later, so the opening
goal is to preserve the core army rather than overextend.
- Opening battle, day 1: no attacks were available. Ended the turn rather than starting fires or moving forward.
- Opening battle, day 2:
- Used Hunters of the Steppe for an archery attack against the visible unit at `(7,9)`.
- Used Rakon's Loyalists for an archery attack against the unit at `(6,10)`.
- Used Onmaa Defenders for melee against the unit at `(6,10)`.
- Avoided the offered duel from Rakon's Loyalists against the unit at `(7,9)`. If that unit is Tarn, the duel risks
Sadar directly before reinforcements have arrived.
- Opening battle, days 3-5:
- On day 3, only Rakon's Loyalists and Hunters of the Steppe had commands. Used both for archery against the unit at
`(6,10)`.
- On days 4 and 5, only Hunters of the Steppe had commands. Used archery against the nearest non-Tarn pressure unit
rather than retreating or entering melee.
- This is a danger signal: if Sadar's unit disappears from the command list this early, the opening formation may have
absorbed too much damage. Keep future attempts more defensive and consider moving Sadar back instead of using every
available attack.
- Elena Fyar arrived on day 6 as actor `7`, confirming the `elena_reinforcement_round_5` tutorial event. She spawned
far southwest of the fight with Holy Wave available. Moved her toward the battle first instead of spending Holy Wave
while isolated; the move consumed her action for the turn.
- John Ranil arrived on day 8 as actor `8`, confirming the `ranil_reinforcement_round_7` tutorial event.
- After John arrived:
- Moved Hunters of the Steppe back to `(10,11)` instead of taking a weak melee attack.
- Moved Elena to `(8,7)` to close with the enemy line.
- Moved John to `(6,4)` to approach from the west.
- Do not expect reinforcements to appear in immediate melee range. They need one or more turns of marching before they
can rescue the original defender units.
- Day 10 Holy Wave timing:
- Moved John adjacent to Elena first.
- Used Elena's Holy Wave only after John was adjacent, so the action could inspire more than Elena herself.
- If Holy Wave is available immediately when Elena appears, do not spend it unless she is already adjacent to a
friendly unit or there is a specific emergency reason.
- Day 11 reinforcement attack:
- Elena got melee commands against `(7,9)` and `(8,10)`. Chose `(8,10)` first because `(7,9)` had repeatedly looked
like the Tarn/front-leader unit.
- Moved John to `(9,9)`, which opened a charge against `(8,10)`.
- Used John's charge and then his follow-up melee against `(8,10)`.
- Practical rule: once Elena and John arrive, pair their attacks on the same non-Tarn unit before taking leader-risky
engagements.
- Probable defeat/reset signal:
- Continued John melee attacks against `(8,10)` through day 14.
- After the day-14 melee, the Shardok stream emitted `shardok-reset` for the same battle id
`ce7be1272b8b4456_1_df540967`.
- The battle token reset to `6` and commands returned to the original opening placement commands, while Eagle status
still reported `BATTLE_IN_PROGRESS`.
- This most likely means the battle was lost, but the text client did not surface the loss explicitly. Treat a reset
after a long battle as a probable defeat signal unless server state proves otherwise.
- For the live run, replay the reset battle with a more conservative setup and keep Sadar out of the first contact
line.
- Replay setup after reset:
- Rakon's Loyalists at `(10,11)`.
- Onmaa Defenders at `(8,10)`.
- Hunters of the Steppe at `(9,11)`.
- Rationale: put Sadar deep, use Onmaa Defenders as the first contact screen, and keep archers close enough to shoot
without becoming the front line.
- Replay tactical adjustment:
- Day 2: enemy units still reached Sadar quickly. Moved Rakon's Loyalists away instead of dueling or meleeing.
- Day 3: moved Sadar again, then moved Hunters of the Steppe out of melee contact.
- Day 4-5: continued withdrawing Sadar north/east and pulled the archer back instead of trading melee.
- Elena arrived with Sadar still alive and mobile. Move Elena east immediately; do not spend Holy Wave while isolated.
- John arrived after Elena. Use Elena for contact, march John east, and let Sadar contribute only with archery while
he remains out of melee.
- Second probable defeat/reset:
- The replay kept Sadar alive through both reinforcements and reached day 20.
- After repeated John melee against the remaining contact at `(6,7)`, Shardok emitted another `shardok-reset` for the
same battle id and returned to the original placement command list.
- This reproduced the reset behavior from the first attempt and likely means the replay was also lost. Stop treating
the current live game as a successful tutorial source until the loss condition is visible in the text client or
confirmed from server state.
- Failed castle-hold variant:
- Onmaa castle tiles are `(6,12)`, `(8,11)`, and `(10,11)`.
- Rakon's Loyalists at `(10,11)`.
- Onmaa Defenders at `(8,11)`.
- Hunters of the Steppe at `(6,12)`.
- Early result: this was much stronger than the previous formations. When no attacks were available, ended turn
instead of moving off castles. When longbow shots were offered, used Hunters of the Steppe from `(6,12)`. When
enemies stepped adjacent, used ordinary castle melee, but continued to avoid duels.
- What failed:
- The battle still reset after day 22, returning the same battle id to token `6`.
- The late phase degraded into too much Holy Wave/Reduce and too little direct pressure. Treat that as the wrong
plan for this tutorial battle.
### Successful Opening Battle
- Won in game `-3568010721410464682`, battle `ce7be1272b8b4456_1_df540967`.
- Winning setup:
- Sadar/Rakon's Loyalists at `(10,11)`.
- Onmaa Defenders at `(8,11)`.
- Hunters of the Steppe at `(6,12)`.
- Main rule: hold the three castle tiles with the starting units until the enemy gives useful attacks. Do not walk the
starting units off the castles just because movement commands are available.
- Early turns:
- End the first defender turn when only movement/fire commands are available.
- Use Hunters of the Steppe for archery whenever possible, usually into the non-Ikhaan targets at `(5,10)`,
`(5,11)`, or `(7,10)`.
- Use ordinary melee from castle tiles when the enemy steps adjacent.
- Sadar may duel non-Ikhaan targets. In the winning run, actor `4` used `CHALLENGE_DUEL` against targetUnit `2`.
- Avoid Sadar's duel against targetUnit `3` at `(9,10)`; that target behaved like Ikhaan Tarn. Ordinary melee into
that adjacent target was still used later, but the duel was skipped every time.
- Reinforcements:
- Elena arrived as actor `7`. March her directly toward the castle line instead of spending Holy Wave while isolated:
`(11,3)`, `(8,5)`, `(8,8)`, then `(8,11)`.
- John arrived as actor `8`. Rush him toward the castles: `(6,4)`, `(10,7)`, then `(8,10)`.
- Once John and Elena reach the line, use their melee attacks immediately. In the winning run both repeatedly attacked
the non-Ikhaan target at `(7,10)`.
- After that target dropped, chase the remaining left-side contact with John and Elena together. John moved to
`(4,11)`, Elena to `(5,10)`, and John finished the battle with melee at `(5,11)`.
- Do not make Holy Wave or Reduce the plan. The successful run won without using Holy Wave or Reduce in the decisive
phase; direct archery, castle melee, and reinforcement melee were enough.
- Post-battle aftermath:
- The battle resolved to Eagle commands and offered three captured heroes: `201`, `202`, and `203`.
- Recruit attempts were refused for all three, then each was imprisoned.
- After the captured-hero prompts cleared, normal strategic commands returned at province `14`, starting with
`ImproveAvailableCommand`.
### Strategic Play After Opening Battle
- Immediate recovery priorities:
- Improve Onmaa's economy and agriculture before starting a war.
- Use Alms sparingly if support is low, but watch food and treasury.
- Train existing heroes/units before marching. The opening win leaves enough time to build before attacking.
- Troop organization notes:
- Onmaa initially exposed only `LightInfantry` and `Longbowmen` as constructible in the command surface.
- An invalid organize attempt tried to top up preexisting heavy units and hit the server error
`HeavyInfantry was not among constructible battalion types Vector(LightInfantry, Longbowmen)`.
- Safe commands in the live run were:
- Top up the existing light infantry battalion.
- Create a new longbow battalion. The text-client payload that worked used numeric enum value `4` for Longbowmen.
- Do not assume existing heavy battalions can be reinforced just because they already exist in the province.
- Marching rule:
- Do not march Sadar in the first expansion army. Leave him in Onmaa and send the non-leader army instead.
- The live run sent Elena, John, Agamemnon, and Old Marek from Onmaa to Nikemi, then on to Kojaria.
- Send supplies forward after the army reaches Nikemi; do not strand the field army without food.
### Failed Hedrick Attack
- Target: Kojaria, held by Hedrick the Hedge-Merchant's faction.
- Army: four non-Sadar heroes, about 1954 troops, including the new untrained longbow battalion.
- Strategic safety result:
- This was better than marching Sadar alone. A reset here did not immediately imply player defeat because Sadar
stayed home.
- It still appears to be a losing tactical plan.
- Tactical setup:
- Placed the army on the west edge and advanced toward Hedrick's single 1000-troop stack.
- A first attempt surrounded Hedrick and used repeated melee. It reset after an infantry melee, likely a loss.
- A second attempt used more spacing:
- Move John/engineer to the firing lane first.
- Use archery whenever it appears.
- Move cavalry into contact only after ranged damage is available.
- Avoid actor 0 infantry melee and avoid making Reduce/Holy Wave the core plan.
- What worked better:
- The stable pattern was archery from actor 0 when available, archery from the new longbows when available, and
cavalry melee from Old Marek.
- Extinguishing fire with the longbows was useful when the enemy lit the cavalry contact square.
- What failed:
- Even with repeated archery and cavalry melee, the battle dragged to day 12 and then reset after another cavalry
melee.
- A single late Reduce did not visibly break the stalemate.
- The new longbow battalion had `0.0` training and `0.0` armament in the stale strategic snapshot, so it may have
contributed much less than its troop count suggested.
- Practical conclusion:
- Do not attack Hedrick immediately after creating a raw longbow battalion.
- Build a stronger army first: train and arm the new longbows, improve the economy/agriculture enough to support
better reinforcements, and consider taking easier provinces or waiting until the army has more developed units.
- If attacking Hedrick, treat his 1000-stack as a boss fight. The current safe-looking four-hero army was not enough
to finish it reliably.
+2 -2
View File
@@ -287,8 +287,8 @@ You play **Sadar Rakon**, formerly Ikhaan Tarn's most trusted lieutenant. Tarn's
| 5 | `tutorial_support_deadline` | December nag if support < 40: have Elena give alms now |
| 6 | `guidance_expand` (script `tutorial_expansion`) | Once support is stable: keep developing, taxes coming in January |
| 7 | `tutorial_taxes_collected` | January: gold/food collected. Now expand — March your warlord + most heroes to a neighbor, leave 12 behind |
| 8 | `tutorial_ready_to_join` | A free hero in `{provinceName}` is ready to recruit → Travel + Recruit |
| 9 | `tutorial_in_town` | When you Travel: explains Trade / Arm Troops / Divine / Recruit / Manage Prisoners; *Return* ends the turn |
| 8 | `tutorial_ready_to_join` | A free hero in `{provinceName}` is ready to recruit → Visit Town + Recruit |
| 9 | `tutorial_in_town` | When you Visit Town: explains Trade / Arm Troops / Divine / Recruit / Manage Prisoners; *Return* ends the turn |
| 10 | `tutorial_faction_appears` | **The twist**: Fracture Covenant lands at Ingia and Soria. *The Eagle* commands them. Tarn may be with them |
| 11 | `tutorial_hero_departed` | First King's hero abandons service — "something deeper is at work" |
| 12 | `tutorial_hero_departed_again` | Second one in a different month — "something is wrong, I can feel it" (foreshadowing) |
+1 -1
View File
@@ -78,7 +78,7 @@ This section lists quests with direct `QuestCommandChooser` handlers in `Fulfill
|-------|---------|-------|
| `GrandArmyQuest` | `GrandArmyQuestCommandChooser` | Uses OrganizeTroops to top off existing battalions and hire Light Infantry. Only executes if resources are sufficient to fully meet the troop target. |
| `BattalionDiversityQuest` | `BattalionDiversityQuestCommandChooser` | Hires one battalion of a missing type to reach 3+ distinct types. Only attempts if the province already has 2+ existing types, has sufficient development (`meetsRequirements`), and gold/food surplus. |
| `UpgradeBattalionQuest` | `UpgradeBattalionQuestCommandChooser` | Walks a priority tree per turn: arm-completes (with optional travel and/or food sale setup) → train-completes → march in a pre-qualified neighbor battalion → organize/top-off of the target type → march in an unqualified neighbor battalion → develop Economy/Agriculture → train progress → develop Infrastructure → arm progress. Only toggles Travel when it directly enables an arm-completes finisher. |
| `UpgradeBattalionQuest` | `UpgradeBattalionQuestCommandChooser` | Walks a priority tree per turn: arm-completes (with optional Visit Town and/or food sale setup) → train-completes → march in a pre-qualified neighbor battalion → organize/top-off of the target type → march in an unqualified neighbor battalion → develop Economy/Agriculture → train progress → develop Infrastructure → arm progress. Only chooses Visit Town when it directly enables an arm-completes finisher. |
### Reconnaissance Quests
@@ -20,7 +20,7 @@ These values do different jobs; they are not interchangeable ratings that simply
- **Training** scales the battalion type's melee and missile damage bonuses. Higher training therefore means more damage per troop. It also affects some command-specific odds, including the chance to stun on a [Charge](/shardok-commands/#charge).
- **Armament** scales the battalion type's resistances and reduces incoming normal damage. Each type and damage category has its own resistance profile, so the same ARM value does not protect every battalion equally. Penetrating damage bypasses this normal armament reduction. If the unit can use [Archery](/shardok-commands/#archery), armament also determines its starting volleys: currently one volley per 10 ARM, rounded down, plus any bonus from its battalion type.
- **Morale** changes the battalion's damage swing. Most units also need at least 10 morale to use normal tactical commands.
- **Hero vigor** is an action constraint and current energy reserve, not battalion armor. A hero-bearing unit normally needs at least 10 vigor to act, and special effects can spend or reduce it.
- **Hero vigor** is an action constraint and current energy reserve, not battalion armor. It affects maximum AP and command access; at zero vigor, the hero-led unit is captured and removed from active battle. See [Vigor](/heroes/#vigor).
- **Troop count** multiplies battalion damage: more surviving troops deliver more total battalion damage. It also determines how many soldiers remain available to absorb casualties.
- **Battalion type** supplies the base damage, training bonus, resistance profile, armament benefit, movement costs, charge multiplier, and special capabilities.
- **Hero stats** contribute separately. Charisma and bravery affect battalion combat swings, while a hero also contributes personal damage and enables stat- or profession-based commands.
@@ -55,13 +55,15 @@ An exceptional formation created through necromantic effects rather than ordinar
## Heroes and units
A Shardok unit normally combines one hero and one battalion. The hero supplies stats, vigor, profession commands, and special capabilities; the battalion supplies troops, type, training, armament, movement, morale, and damage profile. A profession alone may not be enough: casting requires a casting-capable battalion, and Ranger stealth requires a stealth-capable one.
A Shardok unit normally combines one hero and one battalion. The hero supplies stats, [vigor](/heroes/#vigor), profession commands, personal damage, and special capabilities; the battalion supplies troops, type, training, armament, movement, morale, and its damage profile. The hero's Charisma and Bravery also affect the battalion's combat swing. A profession alone may not be enough: casting requires a casting-capable battalion, and Ranger stealth requires a stealth-capable one.
The two parts remain one tactical unit. If the battalion reaches zero troops, it is destroyed. If the attached hero reaches zero vigor, the hero-led unit is captured and removed from active battle; the surviving battalion does not detach and continue fighting independently.
## Creating and changing battalions
[**Organize Troops**](/commands/#organize-troops) creates, resizes, merges, transfers, or dismisses battalions. New troops cost gold and begin without training or armament. Transfers preserve weighted training and armament. Empty battalions are removed.
Use [**Train**](/commands/#train) to improve eligible battalions in the province and [**Arm Troops**](/commands/#arm-troops) while traveling to buy higher armament. Province economy and agriculture affect which types can be raised; effective infrastructure limits equipment.
Use [**Train**](/commands/#train) to improve eligible battalions in the province and [**Arm Troops**](/commands/#arm-troops) while visiting town to buy higher armament. Province economy and agriculture affect which types can be raised; effective infrastructure limits equipment.
## Movement, food, and battle
@@ -72,7 +72,9 @@ Most command phase actions spend the acting province's turn.
<b>Available when:</b> a diplomatic option is valid against another faction and an eligible messenger hero can be sent.
<b>Effect:</b> Send a hero to another faction with a diplomatic proposal. The target faction receives the offer at the end of the current round, after any battles this round have taken place. Truce, invitation, alliance, break-alliance, and ransom options create [incoming diplomacy offers](/factions/#incoming-diplomacy-offers) for the target faction and story-generation requests. Most diplomacy options spend gold, remove the messenger hero from the acting province, and cost the messenger vigor.
<b>Effect:</b> Send a hero to another faction with a diplomatic proposal. The target faction receives the offer at the end of the current round, after any battles this round have taken place. Truce, invitation, alliance, break-alliance, and ransom options create [incoming diplomacy offers](/factions/#incoming-diplomacy-offers) for the target faction and story-generation requests. Most diplomacy options spend gold and cost the messenger vigor.
For truce, invitation, alliance, and break-alliance missions, the envoy leaves the acting province immediately and is unavailable for the rest of the current round, including its battles. The envoy normally returns during **Diplomacy Resolution** near the end of that same round and can act again next round. If the origin province has been lost, the envoy returns elsewhere in the faction when possible. An imprisoned envoy does not return.
<b>Selection:</b> choose the diplomacy option, target faction, and sent hero.
@@ -164,7 +166,7 @@ Look for growth or attack opportunities. The vassal first tries to march into ne
<b>Available when:</b> the acting province has movable heroes, battalions, gold, or food and at least one valid destination.
<b>Effect:</b> Move heroes, battalions, gold, and food from one province to another. The selected units and supplies leave the origin province immediately, each marching hero spends vigor and gains wisdom XP, and the destination receives an [incoming army](/provinces/#incoming-armies) scheduled for the next round.
<b>Effect:</b> Move heroes, battalions, gold, and food from one province to another. March is inter-province movement and always has a destination; [Visit Town](#visit-town) is only a local visit within the current province. The selected units and supplies leave the origin province immediately, each marching hero spends vigor and gains wisdom XP, and the destination receives an [incoming army](/provinces/#incoming-armies) scheduled for the next round.
If the destination is already ruled by your faction when the army arrives, the heroes, battalions, gold, and food enter the province during the next round's Province Move Resolution phase, before riots, vassal commands, and player commands.
@@ -279,21 +281,23 @@ Shipments arrive during the next round's Province Move Resolution phase, before
<b>Selection:</b> choose the acting hero.
### Travel
### Visit Town
<b>Hero requirements:</b> None.
<b>Hero requirements:</b> None. The acting province's ruling hero makes the visit.
<b>Vigor cost:</b> 15.
<b>Available when:</b> the acting province can enter the traveling command state.
<b>Available when:</b> the acting province's ruling hero has at least 40 vigor, the hero is not already visiting town, and there is no blizzard.
<b>Effect:</b> Unlock the traveling commands below. Once you are traveling, you can use as many traveling commands as you want in the same round before using Return to come back to camp.
<b>Effect:</b> Have the ruling hero leave camp to visit the town inside the acting province. Visit Town does not move any hero, battalion, or supplies to another province, so it has no destination; use [March](#march) for inter-province movement.
<b>Selection:</b> no additional choices.
Visit Town spends the ruling hero's vigor and unlocks whichever town commands below are currently eligible. You can use as many eligible town commands as you want in the same round before using Return to go back to camp. If no town activity currently qualifies, Return may be the only command available.
## Traveling Commands
<b>Selection:</b> no additional choices. Visit Town always uses the current province and never asks you to select a destination.
You must use Travel before these commands appear. Once you are traveling, you can use as many traveling commands as you want in one round before using Return to come back to camp.
## Town Commands
You must use Visit Town before these local commands appear. The ruling hero remains in the current province; only the available command set changes from camp duties to town activities. While visiting town, you can use as many eligible town commands as you want in one round before using Return to come back to camp.
### Arm Troops
@@ -301,7 +305,7 @@ You must use Travel before these commands appear. Once you are traveling, you ca
<b>Vigor cost:</b> None.
<b>Available when:</b> you are traveling, the province has gold, and at least one battalion can raise ARM without exceeding effective infrastructure.
<b>Available when:</b> the ruling hero is visiting town, the province has gold, and at least one battalion can raise ARM without exceeding effective infrastructure.
<b>Effect:</b> Spend gold to raise armament on existing battalions. New armament must be higher than the battalion's current armament and cannot exceed the province's effective infrastructure. The province spends gold, the price index may change, and each armed battalion records a backstory event.
@@ -313,7 +317,7 @@ You must use Travel before these commands appear. Once you are traveling, you ca
<b>Vigor cost:</b> None.
<b>Available when:</b> you are traveling and an [unaffiliated hero](/heroes/#unaffiliated-heroes) quest is available in the province.
<b>Available when:</b> the ruling hero is visiting town and an [unaffiliated hero](/heroes/#unaffiliated-heroes) quest is available in the province.
<b>Effect:</b> Decline one of the unaffiliated hero quests available in the acting province.
@@ -327,7 +331,7 @@ You must use Travel before these commands appear. Once you are traveling, you ca
<b>Gold cost:</b> 100 gold per selected hero, multiplied by the province [price index](/provinces/#price-index) and rounded to the nearest whole gold for each hero. The total cost is that per-hero cost multiplied by the number of selected heroes. For example, divining 3 heroes at a 1.25 price index costs 375 gold.
<b>Available when:</b> you are traveling, the province can pay the cost for at least one hero, and there are free heroes who are not yet ready to join your faction but might be won over.
<b>Available when:</b> the ruling hero is visiting town, the province can pay the cost for at least one hero, and there are free heroes who are not yet ready to join your faction but might be won over.
<b>Effect:</b> Spend gold to divine one or more [free heroes](/factions/#free-heroes). For each divined hero, you receive details about what could impress them enough to become recruitable. Fulfill that ask, and the hero can later appear under Recruit Heroes. The province spends gold, and the price index may change.
@@ -339,7 +343,7 @@ You must use Travel before these commands appear. Once you are traveling, you ca
<b>Vigor cost:</b> None.
<b>Available when:</b> you are traveling and the province has prisoner heroes with available management options.
<b>Available when:</b> the ruling hero is visiting town and the province has prisoner heroes with available management options.
<b>Effect:</b> Choose what happens to ordinary prisoners in the province. These are heroes who are already in the prisoner state, not newly captured heroes still waiting for a battle aftermath decision.
@@ -383,21 +387,23 @@ Ordinary prisoners cannot be returned this way. Faction leader prisoners cannot
<b>Vigor cost:</b> None.
<b>Available when:</b> you are traveling and unaffiliated heroes in the province are willing to join.
<b>Available when:</b> the province is ruled by one of the acting faction's faction leaders, that ruling faction leader is visiting town, and at least one unaffiliated hero in the province currently qualifies to join that faction. The visitor may be any faction leader; it does not have to be the faction head. Vassals cannot use this command.
<b>Effect:</b> Recruit available unaffiliated heroes in the acting province. Recruited heroes join the faction at starting loyalty, move from the province's unaffiliated hero list into its ruling faction heroes, and receive a recruitment backstory event.
<b>Selection:</b> choose the heroes to recruit.
See [**What `WOULD_JOIN` Means**](/factions/#what-would_join-means) for the recruitment status, traveler exception, and separate **Please Recruit Me** behavior in vassal-ruled provinces.
### Return
<b>Hero requirements:</b> None.
<b>Vigor cost:</b> None.
<b>Available when:</b> you are traveling.
<b>Available when:</b> the ruling hero is visiting town.
<b>Effect:</b> Return from the traveling command state to the normal strategic flow.
<b>Effect:</b> Return the ruling hero from the current province's town to camp and end that province's turn.
<b>Selection:</b> no additional choices.
@@ -407,7 +413,7 @@ Ordinary prisoners cannot be returned this way. Faction leader prisoners cannot
<b>Vigor cost:</b> None.
<b>Available when:</b> you are traveling and the province has enough gold or food for at least one trade.
<b>Available when:</b> the ruling hero is visiting town and the province has enough gold or food for at least one trade.
<b>Effect:</b> Buy or sell food using the acting province's gold and food stores. Food buy and sell prices are based on the province price index. Buying spends gold and adds food; selling removes food and adds gold. The price index may change after the trade.
@@ -425,7 +431,7 @@ These commands appear only when the game is resolving a special situation, respo
<b>Available when:</b> an attack decision is pending.
<b>Effect:</b> Choose how to respond to a pending attack opportunity or battle decision. The command shows involved armies, acting units, and the available attack decision types. See [Attack and Defense Decisions](/concepts/eagle-to-shardok/#attack-and-defense-decisions) for how this fits into battle creation.
<b>Effect:</b> Choose how to respond to a pending attack opportunity or battle decision. The command shows involved armies, acting units, and the available attack decision types. **Advance** commits the army to the attack. **Withdraw** sends it back without fighting. **Demand Tribute** lets you request a specific amount of the province's gold and food, up to the displayed maximum; the demand cannot be zero. The defender later chooses whether to pay the full demand or refuse it. See [Attack and Defense Decisions](/concepts/eagle-to-shardok/#attack-and-defense-decisions) and [Resolving a tribute demand](/concepts/march-timeline/#resolving-a-tribute-demand).
<b>Selection:</b> choose one available attack decision.
@@ -567,7 +573,9 @@ Gift success is probabilistic. The chance is based on the province leader's Char
<b>Available when:</b> another faction tries to break an alliance.
<b>Effect:</b> Respond to a faction attempting to break an alliance.
<b>Effect:</b> Respond to a faction attempting to break an alliance. Accepting the break replaces the alliance with a 12-month truce; it does not make the factions immediately hostile. The truce remains active through its reset month and changes to **Hostile** when a later **New Round** advances the date past that month. For example, a break accepted in January has a January reset date the following year and becomes hostile when February begins.
Imprisoning the envoy ends the alliance without a truce and makes both relationships **Hostile**. See [Diplomacy Outcomes](/factions/#diplomacy-outcomes) for the other imprisonment consequences.
<b>Selection:</b> choose the originating faction and the resolution.
@@ -615,7 +623,7 @@ Gift success is probabilistic. The chance is based on the province leader's Char
<b>Available when:</b> another faction demands tribute.
<b>Effect:</b> Decide whether to pay demanded tribute. The command shows the demanding factions, requested tribute, their rough strength, and the acting province's available food and gold.
<b>Effect:</b> Decide whether to pay demanded tribute. The command shows the demanding factions, requested tribute, their rough strength, and the acting province's available food and gold. A demand must be accepted or refused in full; partial payment and counteroffers are not available. Paying requires the province to have the full requested amount, creates a 12-month truce between the factions, and turns back the demanding faction's hostile armies against the payer's provinces. Refusing transfers no resources and returns that faction's army to **Attacking**, after which normal defense selection and battle resolution continue. Neither response changes prestige directly, although a battle caused by refusal can later affect prestige. See [Resolving a tribute demand](/concepts/march-timeline/#resolving-a-tribute-demand).
<b>Selection:</b> choose the demanding faction and whether tribute is paid.
@@ -106,7 +106,7 @@ Moving armies arrive at their destinations, supplies move, and incoming hostile
### Hero Departures
Heroes who are ready to leave unaffiliated states or temporary situations may depart.
The game checks affiliated heroes for [low-loyalty departure](/heroes/#loyalty-departure). Any hero with loyalty below 50 may leave at any time, and lower loyalty means a greater risk. A departing hero becomes an unaffiliated traveler in the same province and does not take the province's supplies.
### Unaffiliated Hero Actions
@@ -13,7 +13,7 @@ March is a delayed strategic order. Selected heroes, battalions, gold, and food
| Province Move Resolution, round N+1 | Arrival checked | An army already friendly to the destination enters it. A hostile arrival waits outside rather than entering. |
| After Player Commands, round N+1 | Hostile Army Setup | Hostile arrivals become attacking groups, one per faction, preserving their approach direction and carried supplies. |
| Uncontested Conquest | No tactical defense needed | An unruled destination can be claimed without Shardok. Competing groups are compared by size, power, and a deterministic tie-breaker. |
| Attack and Defense Decisions | A ruled province is contested | Attackers decide whether to press the attack; defenders may choose an army and fallback destination. Truces can turn armies back. |
| Attack and Defense Decisions | A ruled province is contested | Attackers decide whether to press the attack or demand tribute; defenders answer tribute demands and may choose an army and fallback destination. Truces can turn armies back. |
| Battle Request and Resolution | Shardok battle | Eagle sends armies, map, limited battle food, and victory conditions to Shardok, then waits for a result. |
| Battle Aftermath | Consequences applied | Eagle resolves ownership, survivors, prisoners, supplies, devastation, prestige, and any allied-claimant decisions. |
@@ -27,6 +27,16 @@ A marching army does not occupy a neutral or enemy province during movement reso
An unruled province may be conquered without a tactical battle. A ruled and defended province can require attack and defense decisions followed by Shardok.
## Resolving a tribute demand
A tribute demand pauses that attacking faction during Defense Decision. The defender resolves each demand by either paying it in full or refusing it; there is no partial-payment or counteroffer option.
If the defender pays, the full requested gold and food are deducted from the province. The two factions enter a 12-month truce, and the demanding faction's hostile armies against any province ruled by the payer turn back. The returning armies arrive at their origin provinces in the next round.
If the defender refuses, no food or gold is transferred and the demanding army returns to **Attacking** status. The province remains in Defense Decision, so an eligible defender can then choose a defending army and fallback destination. Once the remaining attack and defense decisions are resolved, the ordinary battle-request rules apply: opposed armies proceed to battle, while an undefended attack may instead become an uncontested conquest.
Paying or refusing tribute has no direct prestige effect. If refusal leads to battle, the eventual battle result can change prestige normally.
## Food and gold
Carried supplies travel with the army. Moving armies consume their own food while incoming. If food is insufficient, their battalions can suffer starvation losses.
@@ -17,7 +17,7 @@ Movement cost depends on terrain and battalion type. Water, mountains, bridges,
Melee and ranged attacks require valid targets and battlefield geometry. Line of fire can be blocked. Damage and resistance depend on the units, heroes, damage profiles, and terrain involved.
Many major actions require a minimum AP and then consume all remaining AP. The command reference states the exact rule. Unless an entry says otherwise, a morale-using unit needs at least 10 morale, and a unit with a hero needs at least 10 hero vigor.
Many major actions require a minimum AP and then consume all remaining AP. The command reference states the exact rule. Unless an entry says otherwise, a morale-using unit needs at least 10 morale, and a unit with a hero needs at least 10 [hero vigor](/heroes/#vigor). Current vigor also contributes to maximum AP. Reaching zero vigor resolves the entire hero-led unit as captured, even if troops remain.
## Heroes and battalions
@@ -60,6 +60,22 @@ The common states are:
Some free heroes are effectively unavailable until circumstances change. For example, a province without one of your faction leaders cannot recruit from the Free Hero panel, and a hero may refuse forever if they have a permanent reason not to join your faction.
### What `WOULD_JOIN` Means
`WOULD_JOIN` means the hero meets the normal recruitment threshold for the faction that rules their current province. It is faction-specific: another faction may not qualify to recruit the same hero. The calculation uses the ruling faction's prestige, its faction head's charisma, the free hero's power and ambition, the hero's current state, and any personal bias toward or against that faction. A hero who is permanently unwilling to join that faction does not qualify.
The status means **willing, not recruited**. To recruit the hero with [**Recruit Heroes**](/commands/#recruit-heroes):
1. The province must be ruled by one of your faction leaders.
2. That ruling faction leader must personally use [**Visit Town**](/commands/#visit-town).
3. Use **Recruit Heroes** while the leader is visiting town and select the hero.
The faction head does not have to visit personally. Any of your faction leaders can recruit in a province they rule, although the willingness calculation still uses the faction head's charisma.
Vassals may visit town for their own purposes, but they cannot use **Recruit Heroes**, so they do not automatically recruit a hero marked `WOULD_JOIN`. However, an eligible free hero in a vassal-ruled province with enough support to provide taxes may independently ask to join during the **Please Recruit Me** phase. That is a separate faction-level decision: a human player accepts or declines the request rather than the vassal deciding. AI-controlled factions accept such requests automatically.
`WOULD_JOIN` is not the only status that can lead to recruitment. Travelers continue to display `TRAVELER` even when they would accept, because their willingness remains unknown until a faction leader visits town. Once the leader visits, the authoritative **Recruit Heroes** list shows which free heroes that faction can recruit under the current circumstances.
## Relationships
Faction relationships have a visible diplomatic state and an internal trust value.
@@ -82,6 +98,20 @@ Trust is a hidden number used by AI diplomacy. Higher trust makes a faction more
Incoming offers are unresolved diplomatic proposals from other factions. They become [special and conditional commands](/commands/#special-and-conditional-commands) such as resolving truce offers, alliance offers, invitations, ransom offers, or alliance breaks.
## Diplomacy Outcomes
An envoy sent with a truce, invitation, alliance, or break-alliance proposal leaves the acting province during **Player Commands**. The hero is unavailable for the rest of that round, including any battles, and normally returns during **Diplomacy Resolution** after the battles are complete. If the origin province is still controlled, the envoy returns there; otherwise the game tries another faction province and then a moving faction army. If none exists, the envoy becomes an outlaw in the origin province.
Accepting another faction's alliance break replaces the alliance with a truce. Its reset date is 12 months after acceptance. The truce remains in force through that reset month, then becomes **Hostile** when **New Round** advances the date beyond it. Accepting a break therefore provides a protected transition instead of permitting immediate war.
Imprisoning a messenger has concrete consequences:
- The messenger does not return. They leave their original faction and become a prisoner held in the imprisoning faction's senior faction leader province.
- The messenger receives a -100 faction bias toward the imprisoning faction, making later recruitment much harder.
- The messenger's original faction loses 150 trust toward the imprisoning faction.
- Every third-party faction loses 40 trust toward the imprisoning faction.
- The incoming offer is removed. If the message was an alliance break, the alliance ends immediately, both sides become **Hostile**, and no transition truce is created.
## Focus Province
A faction can have a focus province. The focus province helps direct [province orders](/provinces/#province-orders) and AI attention toward a particular province.
@@ -28,7 +28,37 @@ These values help drive hero flavor, AI decisions, and story generation.
## Vigor
Vigor is a hero's current energy. Many commands require an acting hero and spend vigor. Resting restores vigor up to the hero's constitution.
Vigor is a hero's current energy reserve. **Constitution is the hero's normal strategic vigor cap**; vigor and Constitution are related, but they are not the same stat.
### Strategic vigor
Many Eagle commands require an acting hero and spend vigor. The command reference lists each cost. A hero with too little vigor is not offered as an actor for that command.
Vigor recovers over time and through deliberate recovery:
- Each new month restores 5 vigor.
- [Rest](/commands/#rest) restores up to 15 vigor to each faction hero in the province.
- [Feast](/commands/#feast) restores up to 10 vigor to each faction hero in the province.
- Recovery cannot raise strategic vigor above Constitution.
Strategic actions that spend vigor also grant Constitution XP. Constitution can rise through normal stat advancement, increasing the hero's future strategic vigor cap.
### Vigor in a Shardok battle
A hero enters battle with the vigor they currently have after strategic costs such as marching. That value becomes the hero's **battle-start vigor** and the recovery ceiling for that battle. A tired hero can therefore enter with a battle recovery ceiling below their Constitution. The ceiling does not fall as the battle proceeds; current vigor falls and rises beneath it.
Tactical vigor affects what a hero-led unit can do:
- Higher current vigor contributes to the unit's maximum Action Points. A normal unit's maximum is 5 plus rounded `vigor / 25` plus rounded `morale / 25`.
- Spending Action Points normally spends 0.5 vigor per AP, and some profession abilities have additional vigor costs.
- Combat damage and battlefield effects can reduce vigor.
- Ending an activation with unused AP, [Rest](/shardok-commands/#stop-or-rest), and effects such as [Holy Wave](/shardok-commands/#holy-wave) can restore vigor, but not above battle-start vigor.
- Below 10 vigor, most ordinary tactical commands are unavailable. A few cleanup or required commands are exceptions.
- At zero vigor, the hero-led unit is resolved as captured. Its surviving battalion does not remain on the map as an independently acting unit.
While the hero remains active, their stats affect the battalion's combat swing, they add personal damage, and their profession can unlock special commands. Low vigor does not directly scale those contributions down point by point; it reduces AP and command access, with the decisive removal occurring at zero.
When a tactical display shows `current / battle start`, the first number is current vigor and the second is the battle-start recovery ceiling—not Constitution. Enemy details may be hidden by battlefield knowledge. An absent or hidden enemy value must not be interpreted as an actual zero.
## Loyalty
@@ -36,6 +66,12 @@ Loyalty measures a hero's attachment to their faction. Feasts, events, captures,
Loyalty is not faction bias. Loyalty describes an affiliated hero's current attachment; bias helps determine whether an unaffiliated or imprisoned hero will join a particular faction.
### Loyalty Departure
Any hero with low loyalty (below 50) may leave at any time. The game checks for departures automatically during the **Hero Departures** phase of each strategic round, before players and vassals issue commands. Lower loyalty means a greater chance of departure.
When a hero leaves, they stop belonging to the faction and become an unaffiliated traveler in the same province. They do not take gold or food with them. If other faction heroes remain, the province and its supplies remain with the faction. If every affiliated hero leaves, the province becomes unowned and its stored gold and food are lost.
## Professions
Professions give heroes special command access, bonuses, and tactical identity. Some effects are Eagle strategic effects, while others apply only when the hero is attached to a Shardok battle unit.
@@ -15,7 +15,7 @@ Source map for Shardok command requirements/costs:
- Charge: terrain-entry cost from BattalionType::GetCostToEnterTerrain(...); allowsMoveAfterCharge is true for Light Cavalry and Heavy Cavalry in src/main/resources/net/eagle0/shardok/battalionTypes.tsv; stun settings are baseStunChanceOnCharge and trainingDifferenceMultiplierForStunChanceOnCharge in the same file; src/main/cpp/net/eagle0/shardok/library/command_factories/ChargeCommandFactory.cpp.
- Move: per-hex terrain-entry costs, plus extraCostForZocMovement=1; src/main/cpp/net/eagle0/shardok/library/command_factories/MoveCommandFactory.cpp and src/main/cpp/net/eagle0/shardok/library/commands/MoveCommand.cpp.
- Lightning Bolt: lightningActionPointCost=5 and lightningRange=3; src/main/cpp/net/eagle0/shardok/library/command_factories/LightningBoltCommandFactory.cpp and src/main/cpp/net/eagle0/shardok/library/commands/LightningBoltCommand.cpp.
- Meteor: meteorStartActionPointCost=5, meteorTargetActionPointCost=0, meteorCancelActionPointCost=0, meteorCastActionPointCost=1, meteorCastVigorCost=20, and meteorRange=3; src/main/cpp/net/eagle0/shardok/library/command_factories/MeteorStartCommandFactory.cpp, MeteorTargetCommandFactory.cpp, src/main/cpp/net/eagle0/shardok/library/util/MeteorRangeCoords.cpp, src/main/cpp/net/eagle0/shardok/library/commands/MeteorStartCommand.cpp, MeteorTargetCommand.cpp, MeteorCancelCommand.cpp, and src/main/cpp/net/eagle0/shardok/library/actions/MeteorCastAction.cpp.
- Meteor: minimumVigorForMeteor=30, meteorStartActionPointCost=5, meteorTargetActionPointCost=0, meteorCancelActionPointCost=0, meteorCastActionPointCost=1, meteorCastVigorCost=20, meteorRange=3, meteorSplashFactor=0.4, and meteorCastWisdomXp=5; src/main/cpp/net/eagle0/shardok/library/command_factories/MeteorStartCommandFactory.cpp, MeteorTargetCommandFactory.cpp, src/main/cpp/net/eagle0/shardok/library/util/MeteorRangeCoords.cpp, src/main/cpp/net/eagle0/shardok/library/commands/MeteorStartCommand.cpp, MeteorTargetCommand.cpp, MeteorCancelCommand.cpp, and src/main/cpp/net/eagle0/shardok/library/actions/MeteorCastAction.cpp.
- Start Fire: startFireActionPointCost=5; hero start-fire thresholds are Mage or agility >= 70 and wisdom >= 70 in src/main/cpp/net/eagle0/shardok/ai_battle_simulator/AiBattleSimulator.cpp; src/main/cpp/net/eagle0/shardok/library/command_factories/StartFireCommandFactory.cpp and src/main/cpp/net/eagle0/shardok/library/commands/StartFireCommand.cpp.
- Extinguish Fire: extinguishActionPointCost=5; src/main/cpp/net/eagle0/shardok/library/command_factories/ExtinguishFireCommandFactory.cpp and src/main/cpp/net/eagle0/shardok/library/commands/ExtinguishFireCommand.cpp.
- Freeze Water: freezeWaterActionPointCost=5; src/main/cpp/net/eagle0/shardok/library/command_factories/FreezeWaterCommandFactory.cpp.
@@ -23,22 +23,22 @@ Source map for Shardok command requirements/costs:
- Blow Bridge: blowBridgeActionPointCost=5; src/main/cpp/net/eagle0/shardok/library/command_factories/BlowBridgeCommandFactory.cpp.
- Placement: no AP cost in setup commands; src/main/cpp/net/eagle0/shardok/library/actions/PlaceUnitCommand.cpp and PlaceHiddenUnitCommand.cpp.
- Scout: scoutActionPointCost=5 and scoutRange=4; src/main/cpp/net/eagle0/shardok/library/command_factories/ScoutCommandFactory.cpp.
- Raise Dead: raiseDeadActionPointCost=5 and raiseDeadRange=2; src/main/cpp/net/eagle0/shardok/library/command_factories/RaiseDeadCommandFactory.cpp.
- Dismiss Unit: requires remainingActionPoints > 0 in src/main/cpp/net/eagle0/shardok/library/command_factories/ControlCommandFactory.cpp; execution zeroes both units in src/main/cpp/net/eagle0/shardok/library/commands/DismissUnitCommand.cpp.
- Holy Wave: holyWaveActionPointCost=5; src/main/cpp/net/eagle0/shardok/library/command_factories/HolyWaveCommandFactory.cpp.
- Stop and Rest: stop posts no AP cost; rest zeroes AP in src/main/cpp/net/eagle0/shardok/library/commands/UnitRestCommand.cpp; stop implementation in UnitStopCommand.cpp.
- Challenge Duel: challengeDuelActionPointCost=5; src/main/cpp/net/eagle0/shardok/library/command_factories/ChallengeDuelCommandFactory.cpp and src/main/cpp/net/eagle0/shardok/library/commands/challenge_duel_command/ChallengeDuelCommand.cpp.
- Fear: fearActionPointCost=5 and fearRange=3; src/main/cpp/net/eagle0/shardok/library/command_factories/FearCommandFactory.cpp.
- Repair: repairActionPointCost=5; src/main/cpp/net/eagle0/shardok/library/command_factories/RepairCommandFactory.cpp.
- Reduce: reduceActionPointCost=5 and reduceRange=3; src/main/cpp/net/eagle0/shardok/library/command_factories/ReduceCommandFactory.cpp.
- Raise Dead: raiseDeadActionPointCost=5, raiseDeadRange=2, raiseDeadStartingSize=100, raiseDeadWisdomXp=1, and raiseDeadCharismaXp=2; src/main/cpp/net/eagle0/shardok/library/command_factories/RaiseDeadCommandFactory.cpp and src/main/cpp/net/eagle0/shardok/library/commands/RaiseDeadCommand.cpp.
- Dismiss Unit: dismissBaseOdds=60 and requires remainingActionPoints > 0 in src/main/cpp/net/eagle0/shardok/library/command_factories/ControlCommandFactory.cpp; execution zeroes both units in src/main/cpp/net/eagle0/shardok/library/commands/DismissUnitCommand.cpp.
- Holy Wave: holyWaveActionPointCost=5, holyWaveVigorCost=10, holyWaveInspireBonus=25, maxInspireOverBase=10, holyWaveVigorBuf=5, holyWaveDamagePercentage=0.7, and holyWaveCharismaXp=5; src/main/cpp/net/eagle0/shardok/library/command_factories/HolyWaveCommandFactory.cpp and src/main/cpp/net/eagle0/shardok/library/commands/HolyWaveCommand.cpp.
- Stop and Rest: stop posts no AP cost; actionCostToVigorCost=0.5 and rest zeroes AP in src/main/cpp/net/eagle0/shardok/library/commands/UnitRestCommand.cpp; availability and stop implementation are in ChargeCommandFactory.cpp and UnitStopCommand.cpp.
- Challenge Duel: challengeDuelActionPointCost=5, minimumVigorToAcceptDuel=30, duelDeclinedMoraleAdjustment=-15, duelWonMoraleBoost=15, and duelWinnerCharismaXp=20; src/main/cpp/net/eagle0/shardok/library/command_factories/ChallengeDuelCommandFactory.cpp and src/main/cpp/net/eagle0/shardok/library/commands/challenge_duel_command/ChallengeDuelCommand.cpp.
- Fear: fearActionPointCost=5, fearRange=3, fearDebuf=-25, fearFailedBuf=10, and fearCharismaXp=2; src/main/cpp/net/eagle0/shardok/library/command_factories/FearCommandFactory.cpp and src/main/cpp/net/eagle0/shardok/library/commands/FearCommand.cpp.
- Repair: repairActionPointCost=5 and repairBuf=25; src/main/cpp/net/eagle0/shardok/library/command_factories/RepairCommandFactory.cpp and src/main/cpp/net/eagle0/shardok/library/commands/RepairCommand.cpp.
- Reduce: reduceActionPointCost=5, reduceRange=3, and reduceForestBonus=50; src/main/cpp/net/eagle0/shardok/library/command_factories/ReduceCommandFactory.cpp and src/main/cpp/net/eagle0/shardok/library/commands/ReduceCommand.cpp.
- Flee and Retreat: fleeActionPointCost=5 and retreatActionPointCost=5; src/main/cpp/net/eagle0/shardok/library/command_factories/FleeCommandFactory.cpp and RetreatCommandFactory.cpp.
- Control: requires remainingActionPoints > 0 in ControlCommandFactory.cpp; execution zeroes acting unit in src/main/cpp/net/eagle0/shardok/library/commands/ControlCommand.cpp.
- Reinforce: reinforceActionPointCost=5; src/main/cpp/net/eagle0/shardok/library/command_factories/ReinforceCommandFactory.cpp.
- Brave Water: braveWaterActionPointCost=8; src/main/cpp/net/eagle0/shardok/library/command_factories/BraveWaterCommandFactory.cpp and src/main/cpp/net/eagle0/shardok/library/commands/BraveWaterCommand.cpp.
- Brave Water: braveWaterActionPointCost=8, braveWaterRangerBonus=20, and braveWaterMaxLoss=0.2; src/main/cpp/net/eagle0/shardok/library/command_factories/BraveWaterCommandFactory.cpp and src/main/cpp/net/eagle0/shardok/library/commands/BraveWaterCommand.cpp.
- Release Unit: requires remainingActionPoints > 0 in ControlCommandFactory.cpp; execution zeroes both units in src/main/cpp/net/eagle0/shardok/library/commands/ReleaseUnitCommand.cpp.
- End Turn and Setup: no command AP cost; end turn zeroes player unit AP in src/main/cpp/net/eagle0/shardok/library/commands/EndTurnCommand.cpp; setup end in src/main/cpp/net/eagle0/shardok/library/actions/EndPlayerSetupCommand.cpp.
- Hide: hideActionPointCost=5; src/main/cpp/net/eagle0/shardok/library/command_factories/HideCommandFactory.cpp.
- Fortify: fortifyActionPointCost=5; src/main/cpp/net/eagle0/shardok/library/command_factories/FortifyCommandFactory.cpp.
- Fortify: fortifyActionPointCost=5 and fortifyDamageTakenMultiplier=0.9; src/main/cpp/net/eagle0/shardok/library/command_factories/FortifyCommandFactory.cpp, src/main/cpp/net/eagle0/shardok/library/commands/FortifyCommand.cpp, and the commands/actions that clear fortified status.
- Become Outlaw: uses fleeActionPointCost=5 through src/main/cpp/net/eagle0/shardok/library/command_factories/FleeCommandFactory.cpp and src/main/cpp/net/eagle0/shardok/library/commands/BecomeOutlawCommand.cpp.
- Evacuate Prisoners: evacuatePrisonersActionPointCost=5; src/main/cpp/net/eagle0/shardok/library/command_factories/EvacuatePrisonersCommandFactory.cpp.
-->
@@ -127,13 +127,13 @@ The follow-up list is a planning aid, not a complete list of everything the unit
### Meteor
<b>Unit requirements:</b> [Mage](/heroes/#mage) attached to Light Infantry, Light Cavalry, or Longbowmen, with enough vigor to start the spell. Heavy units and Undead cannot cast.
<b>Unit requirements:</b> [Mage](/heroes/#mage) attached to Light Infantry, Light Cavalry, or Longbowmen, with at least 30 vigor. Heavy units and Undead cannot cast.
<b>Action Point cost:</b> start costs 5 AP and consumes all remaining AP; target and cancel cost 0 AP; the automatic cast step is configured as 1 AP but resolves by setting the caster to 0 AP.
<b>Available when:</b> the caster can begin the spell, choose or change a target within 3 hexes, cast the pending meteor, or cancel the pending meteor.
<b>Effect:</b> Begin a delayed meteor spell, choose where it will land, then resolve the strike when the cast step becomes available. You can cancel while the meteor is still pending.
<b>Effect:</b> Begin a delayed meteor spell, then choose a target within 3 hexes on the following round. When the meteor resolves, a unit on the target hex takes a fire attack based on the Mage's Wisdom. Units on the six adjacent hexes take 40% as much. The impact damages castles, bridges, ice, and snow; may start fires on the target and adjacent hexes; and removes fortification from units it hits. Destroying supporting ice or a bridge can also drop a unit into the water. The Mage spends 20 vigor, gains 5 Wisdom XP, and is stunned for 2 rounds after the strike. You can cancel while the meteor is still pending.
<b>Selection:</b> choose the caster, spell target, or cancel option depending on the current meteor step.
@@ -229,21 +229,21 @@ The follow-up list is a planning aid, not a complete list of everything the unit
<b>Available when:</b> there is an empty, undead-traversable target hex within 2 hexes.
<b>Effect:</b> Create or restore undead forces through necromantic action.
<b>Effect:</b> Make the displayed success roll. On success, create a battalion of 100 Undead on the target hex, controlled by the Necromancer. The new battalion starts with 50 morale, 0 training, and 0 armament. On failure, no unit is created. Either result spends the action; the Necromancer gains 1 Wisdom XP and 2 Charisma XP.
<b>Selection:</b> choose the necromancer unit and target.
### Dismiss Unit
<b>Unit requirements:</b> a unit that is allowed to be dismissed.
<b>Unit requirements:</b> a hero who currently controls a summoned unit, such as Undead raised by a Necromancer.
<b>Action Point cost:</b> requires the controlling unit to have more than 0 AP; execution sets both the controlling unit and dismissed unit to 0 AP.
<b>Available when:</b> dismissal is valid for the current tactical state.
<b>Available when:</b> the controlling hero has more than 0 AP and still has a controlled unit.
<b>Effect:</b> Remove the unit from the tactical battle.
<b>Effect:</b> Make the displayed success roll, based on the controlling hero's Charisma and Wisdom. Success disbands the controlled unit. Failure still ends the control relationship, but the unit remains on the battlefield uncontrolled and targets its former controller.
<b>Selection:</b> choose the unit to dismiss.
<b>Selection:</b> choose Dismiss Unit on the controlling hero; the controlled unit is the target.
### Holy Wave
@@ -251,33 +251,35 @@ The follow-up list is a planning aid, not a complete list of everything the unit
<b>Action Point cost:</b> 5 AP; consumes all remaining AP.
<b>Available when:</b> the paladin has a valid holy-wave action.
<b>Vigor cost:</b> 10 from the Paladin under normal circumstances.
<b>Effect:</b> Use a holy-area effect from the capable unit.
<b>Available when:</b> the Paladin unit has enough AP and meets the shared morale and hero-vigor requirements.
<b>Selection:</b> choose the paladin unit and command target, if one is required.
<b>Effect:</b> Emit a wave centered on the Paladin. The Paladin and every adjacent non-Undead unit controlled by the same player gain 25 morale, capped at 10 above base morale and at 100 overall; attached heroes regain 5 vigor up to starting vigor; and stun is removed. Every adjacent Undead unit, regardless of allegiance, loses 70% of its troops, becomes visible, and targets the Paladin. The Paladin gains 5 Charisma XP. The effect does not make a success roll.
<b>Selection:</b> choose the Paladin unit. Holy Wave does not require a target.
### Stop and Rest
<b>Unit requirements:</b> a unit that can stop or rest.
<b>Unit requirements:</b> a unit with an available charge.
<b>Action Point cost:</b> Stop costs 0 AP; Rest consumes all remaining AP.
<b>Available when:</b> the unit can end movement or spend its action resting.
<b>Available when:</b> Stop is offered outside an enemy zone of control; Rest is offered inside an enemy zone of control.
<b>Effect:</b> `UNIT_STOP_COMMAND` stops a unit. `UNIT_REST_COMMAND` has a unit spend its action resting instead of acting aggressively.
<b>Effect:</b> Stop declines the available charge without spending the unit's remaining AP. Rest declines the charge, ends the activation, and restores an attached hero's vigor by half of the AP given up. For example, resting with 4 AP restores 2 vigor.
<b>Selection:</b> choose stop or rest for the acting unit.
### Challenge Duel
<b>Unit requirements:</b> [Champion](/heroes/#champion).
<b>Unit requirements:</b> [Champion](/heroes/#champion) challenging an adjacent known enemy unit with an attached hero. A challenge cannot enter a castle.
<b>Action Point cost:</b> 5 AP; consumes all remaining AP.
<b>Available when:</b> an opposing unit can be challenged.
<b>Available when:</b> the target hero has at least 30 vigor, or the Champion does not know enough about the target to know otherwise.
<b>Effect:</b> Challenge an opposing unit to a duel. The tactical action emits accepted or declined outcomes directly.
<b>Effect:</b> The target's chance to accept depends on Bravery and its assessment of the two battalions and heroes. A target that declines normally loses 15 morale; a target known to have less than 30 vigor declines without that penalty. If accepted, the heroes exchange attacks until one falls or 30 exchanges pass. A winner's battalion gains 15 morale and the winning hero gains 20 Charisma XP. Participating heroes can also gain Strength and Agility XP.
<b>Selection:</b> choose the champion unit and duel target.
@@ -289,7 +291,7 @@ The follow-up list is a planning aid, not a complete list of everything the unit
<b>Available when:</b> a known enemy that uses morale is within 3 hexes.
<b>Effect:</b> Use a fear effect against the target.
<b>Effect:</b> Make the displayed success roll. The Necromancer's Charisma and Wisdom improve the chance, while a target hero's Charisma resists it. Success reduces the target battalion's morale by 25 and stuns it for 1 round. Failure instead increases its morale by 10. The Necromancer gains 2 Charisma XP either way.
<b>Selection:</b> choose the necromancer unit and target.
@@ -299,11 +301,11 @@ The follow-up list is a planning aid, not a complete list of everything the unit
<b>Action Point cost:</b> 5 AP; consumes all remaining AP.
<b>Available when:</b> a damaged unit or battlefield object can be repaired.
<b>Available when:</b> a damaged friendly or unoccupied bridge or castle is on the Engineer's hex or an adjacent hex. The Engineer cannot repair from water or repair a structure occupied by a known enemy.
<b>Effect:</b> Repair the damaged target.
<b>Effect:</b> Restore 25 integrity to the bridge or castle, up to its maximum of 100. Repair does not restore troops or hero vigor.
<b>Selection:</b> choose the engineer unit and repair target.
<b>Selection:</b> choose the Engineer unit and the bridge or castle to repair.
### Reduce
@@ -311,71 +313,83 @@ The follow-up list is a planning aid, not a complete list of everything the unit
<b>Action Point cost:</b> 5 AP; consumes all remaining AP.
<b>Available when:</b> an enemy or damaged castle is within 3 hexes and the line is not blocked by mountains.
<b>Available when:</b> a known enemy unit or a castle with remaining integrity is within 3 hexes and the line is not blocked by mountains. Friendly-occupied hexes and the Engineer's own hex cannot be targeted.
<b>Effect:</b> Reduce the target's defensive or structural value.
<b>Effect:</b> Bombard the target. The damage roll improves with the Engineer's Agility, Strength, and Wisdom, and gains a bonus when friendly forces have access to a forest. The result reduces castle or bridge integrity on the hex, damages any occupying enemy unit, and removes that unit's fortified status.
<b>Selection:</b> choose the engineer unit and target.
### Flee and Retreat
### Flee
<b>Unit requirements:</b> a unit that is allowed to leave or withdraw.
<b>Unit requirements:</b> a unit with an attached hero that is allowed to flee.
<b>Action Point cost:</b> 5 AP; consumes all remaining AP.
<b>Available when:</b> flight or retreat is valid for the current battlefield state.
<b>Available when:</b> the hero can leave the battle and has enough AP.
<b>Effect:</b> `FLEE_COMMAND` handles immediate flight from a tactical situation. `RETREAT_COMMAND` handles a more formal withdrawal command.
<b>Effect:</b> Attempt to withdraw from the province entirely. Make the displayed success roll: nearby visible friendly units improve the chance to escape, while nearby visible enemies and impassable terrain reduce it. Units 2 hexes away have half as much effect as adjacent units. VIP heroes escape automatically. On success, the unit leaves both the battle and the province and will escape even if the province is later captured. On failure, the unit is captured during the attempt.
<b>Selection:</b> choose the fleeing or retreating unit.
<b>Selection:</b> choose the fleeing unit.
### Retreat
<b>Unit requirements:</b> a defending unit with an attached hero.
<b>Action Point cost:</b> 5 AP; consumes all remaining AP.
<b>Available when:</b> the defending hero has enough AP. Attackers cannot use Retreat.
<b>Effect:</b> Withdraw the unit from the battle while remaining in the province. Retreat succeeds automatically and does not make the immediate capture roll used by Flee, but the hero will still be captured if their side loses the battle and the province is captured.
<b>Selection:</b> choose the retreating unit.
### Control
<b>Unit requirements:</b> a unit with access to a control effect.
<b>Unit requirements:</b> a hero who already has a controlled summoned unit, such as Undead raised by a Necromancer.
<b>Action Point cost:</b> requires more than 0 AP; execution sets the acting unit to 0 AP.
<b>Available when:</b> a valid target can be controlled.
<b>Available when:</b> the controlling hero has more than 0 AP and still has a controlled unit.
<b>Effect:</b> Take control of the target when the command is available.
<b>Effect:</b> Spend the controlling hero's remaining AP to command the linked unit this round. Control does not create a new controlled unit; Raise Dead establishes that relationship.
<b>Selection:</b> choose the acting unit and control target.
<b>Selection:</b> choose Control on the controlling hero; the linked unit is the target.
### Reinforce
<b>Unit requirements:</b> reinforcements available to enter the battle.
<b>Unit requirements:</b> an on-map unit with enough AP and a friendly reserve unit available to enter the battle.
<b>Action Point cost:</b> 5 AP; consumes all remaining AP.
<b>Available when:</b> the battle state allows reinforcement and there is a valid entry option.
<b>Available when:</b> fewer than 10 friendly on-map units have attached heroes, and the reserve unit has an empty, non-burning hex in its designated starting area.
<b>Effect:</b> Bring reinforcements into the battle.
<b>Effect:</b> Spend the acting unit's activation and move the selected reserve unit onto the chosen entry hex as a normal active unit.
<b>Selection:</b> choose the reinforcement option and entry placement.
### Brave Water
<b>Unit requirements:</b> a unit that can attempt brave-water movement. [Ranger](/heroes/#ranger) is not required, but Rangers get an odds bonus.
<b>Unit requirements:</b> a unit with an attached hero whose battalion type permits Brave Water. The hero needs at least 10 vigor. [Ranger](/heroes/#ranger) is not required, but Rangers receive a 20-point odds bonus.
<b>Action Point cost:</b> 8 AP; consumes all remaining AP.
<b>Available when:</b> the unit has a valid water path or water target.
<b>Available when:</b> the unit is on land and can reach a believed-empty, traversable land hex across water.
<b>Effect:</b> Move through water despite the usual danger or restriction.
<b>Effect:</b> Make the displayed crossing roll. Agility and Wisdom improve the chance; armament and bad weather reduce it. Success moves the unit across the water, unless a hidden occupant triggers an ambush on arrival. Failure leaves the unit in place. The attempt always stuns the unit for 1 round and can cost up to 20% of its troops, armament, and hero vigor; safer crossings reduce the possible losses.
<b>Selection:</b> choose the unit and water movement target.
### Release Unit
<b>Unit requirements:</b> a unit that can release a constrained target.
<b>Unit requirements:</b> a hero who currently controls a summoned unit, such as Undead raised by a Necromancer.
<b>Action Point cost:</b> requires more than 0 AP; execution sets both the controlling unit and released unit to 0 AP.
<b>Available when:</b> a controlled, held, or otherwise constrained unit can be released.
<b>Available when:</b> the controlling hero has more than 0 AP and still has a controlled unit.
<b>Effect:</b> Release the constrained unit.
<b>Effect:</b> End the control relationship without a roll. The released unit becomes uncontrolled, targets its former controller, and remains on the battlefield. Both units end their activations.
<b>Selection:</b> choose the acting unit and unit to release.
<b>Selection:</b> choose Release Unit on the controlling hero; the controlled unit is the target.
### End Turn and Setup
@@ -407,21 +421,21 @@ The follow-up list is a planning aid, not a complete list of everything the unit
<b>Action Point cost:</b> 5 AP; consumes all remaining AP.
<b>Available when:</b> the engineer can fortify its current position.
<b>Available when:</b> the Engineer is not already fortified, is standing on fortifiable terrain, and has enough AP.
<b>Effect:</b> Have the unit fortify its current position.
<b>Effect:</b> Mark the unit as fortified, reducing damage taken by 10%. Moving removes fortification; being hit by Reduce or Meteor also removes it, as does taking fire damage at the end of the unit's turn.
<b>Selection:</b> choose the engineer unit.
### Become Outlaw
<b>Unit requirements:</b> a unit or hero eligible for the outlaw consequence.
<b>Unit requirements:</b> a unit with an attached hero that is not allowed to flee normally.
<b>Action Point cost:</b> 5 AP through the flee/outlaw action.
<b>Available when:</b> the battle result or tactical state allows the outlaw transition.
<b>Available when:</b> the hero has enough AP, cannot flee, and the battle permits the outlaw outcome.
<b>Effect:</b> Mark the unit or hero as becoming an outlaw as a battle consequence.
<b>Effect:</b> Use the same displayed escape roll as Flee. Success removes the unit from battle and records the hero as an outlaw. Failure removes the unit as captured.
<b>Selection:</b> choose the outlaw command when offered.
@@ -431,8 +445,8 @@ The follow-up list is a planning aid, not a complete list of everything the unit
<b>Action Point cost:</b> 5 AP; consumes all remaining AP.
<b>Available when:</b> prisoners can be moved out of danger.
<b>Available when:</b> at least one hostile hero has already been captured, and the Warden is allowed to flee and has enough AP.
<b>Effect:</b> Move prisoners out of danger when the battlefield state allows prisoner evacuation.
<b>Effect:</b> Remove the Warden's unit from battle as evacuated and secure every captured hostile hero for the Warden's player. The evacuation succeeds automatically.
<b>Selection:</b> choose the warden unit and evacuation command.
+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.
+7 -7
View File
@@ -2,18 +2,18 @@ module github.com/nolen777/eagle0
go 1.25.0
toolchain go1.26.4
toolchain go1.26.5
require (
github.com/aws/aws-sdk-go-v2 v1.42.0
github.com/aws/aws-sdk-go-v2/config v1.32.25
github.com/aws/aws-sdk-go-v2/credentials v1.19.24
github.com/aws/aws-sdk-go-v2/service/s3 v1.103.3
github.com/aws/aws-sdk-go-v2 v1.43.0
github.com/aws/aws-sdk-go-v2/config v1.32.31
github.com/aws/aws-sdk-go-v2/credentials v1.19.30
github.com/aws/aws-sdk-go-v2/service/s3 v1.106.0
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/google/uuid v1.6.0
github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6
golang.org/x/sys v0.46.0
google.golang.org/grpc v1.81.1
golang.org/x/sys v0.47.0
google.golang.org/grpc v1.82.1
google.golang.org/protobuf v1.36.11
)
+92
View File
@@ -4,34 +4,66 @@ cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdB
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 h1:rIkQfkCOVKc1OiRCNcSDD8ml5RJlZbH/Xsq7lbpynwc=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0/go.mod h1:RD2SsorTmYhF6HkTmDw7KmPYQk8OBYwTkuasChwv7R4=
github.com/aws/aws-sdk-go-v2 v1.32.8 h1:cZV+NUS/eGxKXMtmyhtYPJ7Z4YLoI/V8bkTdRZfYhGo=
github.com/aws/aws-sdk-go-v2 v1.32.8/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U=
github.com/aws/aws-sdk-go-v2 v1.42.0 h1:XvXMJTkFQtpBKIWZnmr9ZEOc2InWM2yldjXEJ/bymhA=
github.com/aws/aws-sdk-go-v2 v1.42.0/go.mod h1:27+ACypSLljLAEKsCYOmrjKh83vuTRkuAe9Uv/3A4bg=
github.com/aws/aws-sdk-go-v2 v1.42.1 h1:9eOTgu1z/dVtYpNZ3/8/XbbaX0x/BqE3HUzAzs6K0ek=
github.com/aws/aws-sdk-go-v2 v1.42.1/go.mod h1:5pKeft2eJj+gElQ38Jqg4ibCqh+/AK33/0X3hip7IjM=
github.com/aws/aws-sdk-go-v2 v1.43.0 h1:fharf/WhbRAVZ1du0QL7roNFxZ6T/sWr+4Ni617bwSI=
github.com/aws/aws-sdk-go-v2 v1.43.0/go.mod h1:5pKeft2eJj+gElQ38Jqg4ibCqh+/AK33/0X3hip7IjM=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 h1:lL7IfaFzngfx0ZwUGOZdsFFnQ5uLvR0hWqqhyE7Q9M8=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7/go.mod h1:QraP0UcVlQJsmHfioCrveWOC1nbiWUl3ej08h4mXWoc=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13 h1:p1BBrg/Hhp6uK7zpejeI8QFXHJeC/mynzi04Sl03k9g=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13/go.mod h1:8cIfkE9MDhkRZGpQ22aV6/lkYeYSozpz16Smrs5x4Ls=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.14 h1:3IZY0XAJquT3aHzbkHfPzy4ACPcEjVG0x87KOwtpqGY=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.14/go.mod h1:zwM6veDkhGgQFqkBy+uT28AAYpLu+uFMlPl+rCg/73E=
github.com/aws/aws-sdk-go-v2/config v1.28.10 h1:fKODZHfqQu06pCzR69KJ3GuttraRJkhlC8g80RZ0Dfg=
github.com/aws/aws-sdk-go-v2/config v1.28.10/go.mod h1:PvdxRYZ5Um9QMq9PQ0zHHNdtKK+he2NHtFCUFMXWXeg=
github.com/aws/aws-sdk-go-v2/config v1.32.25 h1:ACCejvStYoilgwrfegSt5ZntCbPrk52qfwyNcnl3omM=
github.com/aws/aws-sdk-go-v2/config v1.32.25/go.mod h1:LJyU8sDRbXUxFn8xMJIGP+v9QYYwveNLI8a/giAOiAs=
github.com/aws/aws-sdk-go-v2/config v1.32.29 h1:BcMHHnpiWKogf+gGfpj3K1w+Sktz29XDo/cPSAPO3FU=
github.com/aws/aws-sdk-go-v2/config v1.32.29/go.mod h1:+Kbhn8Es4kPUph3F/0W7avykytc+Jh2Ld9/msv9ljV4=
github.com/aws/aws-sdk-go-v2/config v1.32.30 h1:XwsEzpTJfQYJbFicz/QMLwAZdyeNVVoOEkbF7R3gPJk=
github.com/aws/aws-sdk-go-v2/config v1.32.30/go.mod h1:Ud32SuMc+/9BGxfpSVld7HrE2o05JwKmXY4M3jOQNZU=
github.com/aws/aws-sdk-go-v2/config v1.32.31 h1:n4nY9O3QKoHIkL85EX+V8RcMFtOhlpTFhGArg915PXk=
github.com/aws/aws-sdk-go-v2/config v1.32.31/go.mod h1:PN0NYDCCoOpGGsZ2+elDUidmHfQBPyYzN2GCgl8HEBs=
github.com/aws/aws-sdk-go-v2/credentials v1.17.51 h1:F/9Sm6Y6k4LqDesZDPJCLxQGXNNHd/ZtJiWd0lCZKRk=
github.com/aws/aws-sdk-go-v2/credentials v1.17.51/go.mod h1:TKbzCHm43AoPyA+iLGGcruXd4AFhF8tOmLex2R9jWNQ=
github.com/aws/aws-sdk-go-v2/credentials v1.19.24 h1:2hQqYCV9yqyePQ9o6dCrZc/zO8U3TwPr9mIKlZnPu/I=
github.com/aws/aws-sdk-go-v2/credentials v1.19.24/go.mod h1:IDwpACtwqHLISdzfwUUNq4P9DsB/h5BLg4FwJPNfqFY=
github.com/aws/aws-sdk-go-v2/credentials v1.19.28 h1:zTXJSsNcoO91/mTXsZoYf0AK8dvNPiA58/VtyGXR+wM=
github.com/aws/aws-sdk-go-v2/credentials v1.19.28/go.mod h1:Kd9E0JzDBW/q1xbsHFrev/GnbAf5J0Ng8xoyc7HZ91Q=
github.com/aws/aws-sdk-go-v2/credentials v1.19.29 h1:WHZGssHH887cO0ox07SIQZsFx3MKD4ps6w0xUEmnKYQ=
github.com/aws/aws-sdk-go-v2/credentials v1.19.29/go.mod h1:Mhl0xR6zjguiuj00XRx2wMx22sAltk7oya39sT7fdg8=
github.com/aws/aws-sdk-go-v2/credentials v1.19.30 h1:TTCvvzFU6gXa4iJecNG/0F/B0oYTiazoRECr2XyLHrY=
github.com/aws/aws-sdk-go-v2/credentials v1.19.30/go.mod h1:jKxAp2AEncnliinzpgOSZDFv6+VjvWhjw/AtbfsWT9U=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.23 h1:IBAoD/1d8A8/1aA8g4MBVtTRHhXRiNAgwdbo/xRM2DI=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.23/go.mod h1:vfENuCM7dofkgKpYzuzf1VT1UKkA/YL3qanfBn7HCaA=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29 h1:r6qZHbT+wxgWO/e9vYNUEtg7lv5+UN3pRqKhLXvnArg=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29/go.mod h1:QRnaRcTVGKPGRy8w78HMQtKUGRYcnMZAANATkeVA6Mo=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.30 h1:/hi1JADLEW9YYryEz1w4GQu0EtP23pP553Cf9KgsDV4=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.30/go.mod h1:/3AOgy4K17Dm4ucMZVC/MJkzy5kmfKUcINRHZyo0koQ=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.31 h1:kfVL5wAunCJycL6MOQ6aNh6PlAYEymflcjuKmrWUA0o=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.31/go.mod h1:nWfRNDAppujCQgOUd43lKT4yeLv9z3nJ3bw1G3BgQKo=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.27 h1:jSJjSBzw8VDIbWv+mmvBSP8ezsztMYJGH+eKqi9AmNs=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.27/go.mod h1:/DAhLbFRgwhmvJdOfSm+WwikZrCuUJiA4WgJG0fTNSw=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 h1:f3vKqSo13fhTYb+JEcXwXefZQE26I1FB5eTSniU67ko=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29/go.mod h1:MzoLFUArKGpGD+ukmPiTPG1X5x4o6M2kq4v2dr1FiEc=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.30 h1:xM/Is9cKMHa8Jj8zkvWhvrFkZsXJV9E+BB4g0HW0duQ=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.30/go.mod h1:WueJeNDZvK1fMYEWJIkcivBfEzUkTpBhzlrUKKY8EuA=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.31 h1:Z8F3hfCY33IGpJjFAnv0wvtv1FIKj1GHmRDEYqy64tw=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.31/go.mod h1:aVyUoytEyOViR6jhq6jula0xkc5NfBE2hgeF6BvOrao=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.27 h1:l+X4K77Dui85pIj5foXDhPlnqcNRG2QUyvca300lXh8=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.27/go.mod h1:KvZXSFEXm6x84yE8qffKvT3x8J5clWnVFXphpohhzJ8=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 h1:RdwIf/CuUsvJX3RgJagbOyotl/cxoLY4xviKuE7p2GY=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29/go.mod h1:71wt8W2EgswdZy9Mf9KNnzxZ3TiZlv4caKghPktDOkA=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.30 h1:jn46zC9LdsVR/ZpMIJqMqb8hHv31BlLx3ulVqNspUOk=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.30/go.mod h1:1hTMsAgbdS/AtUi4bw8+gUuh1pceo+eXRLfpSuSQj3M=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.31 h1:hyOxUyXdh3AyjE93gBgsfziJag9ACwcs+ZpDBLzi8mw=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.31/go.mod h1:OERqI9k0draSLB8O8woxY3q25ZWTELRK4RRoLMuMZFo=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw=
@@ -40,46 +72,98 @@ github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.27 h1:AmB5QxnD+fBFrg9LcqzkgF/CaYv
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.27/go.mod h1:Sai7P3xTiyv9ZUYO3IFxMnmiIP759/67iQbU4kdmkyU=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30 h1:VTGy885W5DKBxWRUJbym9hytNaYzsyaPkCHGRRMAOhU=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30/go.mod h1:AS0HycUvJRFvTt613AYDOgO2jzw+00cVSMny8XB3yMY=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.31 h1:3GUprIsfmGcC5SACIyB0e7E0BM1O1b3Erl5CePYIAeQ=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.31/go.mod h1:7PuV1yl5e2xnUbm+RqvVg5i2iBM8EyijZNoI9wsOoOc=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.32 h1:0MrUL35H/Y4kdFfItoR5jCgtDQ4Z/8LudAoIHRfA4hE=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.32/go.mod h1:2tNZkuWz54arj8mHVf+8Y7cKkcD8Wr/fBpENgEXpjLc=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 h1:iXtILhvDxB6kPvEXgsDhGaZCSC6LQET5ZHSdJozeI0Y=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12 h1:ZD2+BSw9vFsNlKYIasSNt3uDbjqqXIBcM13UJv/Lx2k=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12/go.mod h1:Ms4zlcVBbXbiP7EVLhl+lgjvA/a7YphqQ3Ih3174EmI=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.13 h1:mbRIur/BiHK6SKPjoBIXSE/hJ6g6JGRLuxQy1jGjlN4=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.13/go.mod h1:ITg9em2KbJx1s0y4aqRX5OYWG6HBZ5TVR//OdpEZ2CQ=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.8 h1:iwYS40JnrBeA9e9aI5S6KKN4EB2zR4iUVYN0nwVivz4=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.8/go.mod h1:Fm9Mi+ApqmFiknZtGpohVcBGvpTu542VC4XO9YudRi0=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.22 h1:V51LGlOq/1VsDsHUdoklAQi7rMmx4qQubvFYAlP2254=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.22/go.mod h1:4Pzhyz8hJOm2bepgl+NjvRx8vlUFAIIvJnZ/MkcNPpU=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.23 h1:9Fjh6fi/U5JEStVZijmaMpUwE/gvBJj7x2B/PjbO9To=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.23/go.mod h1:iMoT2f1tClxrWAAnKCXjZQ6LOmfLrMG14wmnWpM+F14=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.24 h1:mdPwDQPqxlw9Sc62Nt15yjEcARaDbPXkjRYtXsUripo=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.24/go.mod h1:ls5ytnwLTcQaUu32fMYXFI3MjpKuTwL840PAm9iqyEg=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.8 h1:cWno7lefSH6Pp+mSznagKCgfDGeZRin66UvYUqAkyeA=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.8/go.mod h1:tPD+VjU3ABTBoEJ3nctu5Nyg4P4yjqSH5bJGGkY4+XE=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29 h1:DRebniUGZ2MqiiIVmQJ04vIXr918hubdHMnarSLEWyU=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29/go.mod h1:LfRkPCD8YHDM2E5eTkos2UpwYeZnBcVarTa8L59bJHA=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.30 h1:/Z5jmNrKsSD7EmDjzAPsm/3L9IuOkzaynklJZ1qX7S4=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.30/go.mod h1:lEzEZnOosE7zi8Z6royW1cFJTD9fpab4Ul1SBrllewk=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.31 h1:w2SIhW92DZPFrSL4ksVCr8IYff5OZwIcxg8+95tzvAI=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.31/go.mod h1:wAhpCQbkov+IcvjozJbd2xRCoZybUEHNkcFunssNACg=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.8 h1:/Mn7gTedG86nbpjT4QEKsN1D/fThiYe1qvq7WsBGNHg=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.8/go.mod h1:Ae3va9LPmvjj231ukHB6UeT8nS7wTPfC3tMZSZMwNYg=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.29 h1:hiME6pBzC7OTl9LMtlyTWBuEl1f4QBcUmFDKC7MLXtc=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.29/go.mod h1:G7RP+uhagpKtKhd1BM9N6JQqjCcGEU47K5lBVZQyRQw=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.31 h1:uao4A3QZ5UmB326V6KF+qRpv9Tjz7IlnlnTbbANntlU=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.31/go.mod h1:I/1+z0VwL1GhQyLgkoHDlygpUZ+iTAwOQ/NsftiUL2I=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.32 h1:jWXtZdCnhXa9sGFixRaU2AxT4DIVse9HS4E2f+/KwV0=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.32/go.mod h1:9JS1UpfVvyD/ZPX8GsKb/Pq8scEM+7GP5fqh9SwH7po=
github.com/aws/aws-sdk-go-v2/service/s3 v1.72.2 h1:a7aQ3RW+ug4IbhoQp29NZdc7vqrzKZZfWZSaQAXOZvQ=
github.com/aws/aws-sdk-go-v2/service/s3 v1.72.2/go.mod h1:xMekrnhmJ5aqmyxtmALs7mlvXw5xRh+eYjOjvrIIFJ4=
github.com/aws/aws-sdk-go-v2/service/s3 v1.103.3 h1:JRseEu/vIDMaWis4bSw0QbXL+cvIGc1XnX076H5ZXLE=
github.com/aws/aws-sdk-go-v2/service/s3 v1.103.3/go.mod h1:77ZAgynvx1txMvDG8gGWoWkO1augYDxkp9JElWFgjQU=
github.com/aws/aws-sdk-go-v2/service/s3 v1.105.0 h1:XptwLL+UHXgafYMIHTy59IRovLbhz3znkxY2uS/pbXU=
github.com/aws/aws-sdk-go-v2/service/s3 v1.105.0/go.mod h1:zdmCoFO/dSI7GlrwsPqFJI+WlFnSU4Tc8TJnlXrM1Do=
github.com/aws/aws-sdk-go-v2/service/s3 v1.105.1 h1:LkBKxAOE5WXjlFuFZqPG1rREnl6I6QCMElcXFDEidos=
github.com/aws/aws-sdk-go-v2/service/s3 v1.105.1/go.mod h1:zdmCoFO/dSI7GlrwsPqFJI+WlFnSU4Tc8TJnlXrM1Do=
github.com/aws/aws-sdk-go-v2/service/s3 v1.105.2 h1:5C00eQYpTrgQXnp6V3P6P7zPElna3AXvlukbANE6nJI=
github.com/aws/aws-sdk-go-v2/service/s3 v1.105.2/go.mod h1:zdmCoFO/dSI7GlrwsPqFJI+WlFnSU4Tc8TJnlXrM1Do=
github.com/aws/aws-sdk-go-v2/service/s3 v1.106.0 h1:7QZWVJZWzHivHWIa+5TELLaBBkbuoj0GPwQtMlJ0sqk=
github.com/aws/aws-sdk-go-v2/service/s3 v1.106.0/go.mod h1:fcvq5L7dK+5cQFicEJwpI6e6Wn8NY2i6yT5wRLYVc7s=
github.com/aws/aws-sdk-go-v2/service/signin v1.2.0 h1:3nXpRcFwRCW8n7HgO2QGy0Dc20eQNfBuUemGQhpF8m8=
github.com/aws/aws-sdk-go-v2/service/signin v1.2.0/go.mod h1:LxYujSTLPRlp2vTtcUO/+1ilrew8ytt6SvQyOgejzFQ=
github.com/aws/aws-sdk-go-v2/service/signin v1.4.0 h1:sLzmJGCMv+C8KqiJgEqDLB6vxaJGmobRh4rr//ZpA3w=
github.com/aws/aws-sdk-go-v2/service/signin v1.4.0/go.mod h1:mxC0nT/C8wMMS97DemZPzvUZxvIt+2Iq+eS3JdFZGgg=
github.com/aws/aws-sdk-go-v2/service/signin v1.4.1 h1:V7ZZ300WPXGjvkyore5DGe0ljVPOxCXie/thWdtSBXE=
github.com/aws/aws-sdk-go-v2/service/signin v1.4.1/go.mod h1:mxC0nT/C8wMMS97DemZPzvUZxvIt+2Iq+eS3JdFZGgg=
github.com/aws/aws-sdk-go-v2/service/signin v1.5.0 h1:OHH5iTQvVGmfHjX/5Q+vFuA/Rf2x6/95aJ/75QCQSm4=
github.com/aws/aws-sdk-go-v2/service/signin v1.5.0/go.mod h1:mCF3AK9PpL49oOrhniUXWAfhVBVQ/XbytoE5eccZUIs=
github.com/aws/aws-sdk-go-v2/service/sso v1.24.9 h1:YqtxripbjWb2QLyzRK9pByfEDvgg95gpC2AyDq4hFE8=
github.com/aws/aws-sdk-go-v2/service/sso v1.24.9/go.mod h1:lV8iQpg6OLOfBnqbGMBKYjilBlf633qwHnBEiMSPoHY=
github.com/aws/aws-sdk-go-v2/service/sso v1.31.3 h1:ey1XLTYXb9PcLt4535632o5kCGXNXEhNb620Dqwuylo=
github.com/aws/aws-sdk-go-v2/service/sso v1.31.3/go.mod h1:Lk7PlmoTYryQmyBG0EXqj5BcUbj3whXdU2s3yGI3EAc=
github.com/aws/aws-sdk-go-v2/service/sso v1.32.0 h1:qjMmry/cBDee1E/2gyvel0uRYCi3mwRZ2hf6N+GAodo=
github.com/aws/aws-sdk-go-v2/service/sso v1.32.0/go.mod h1:u8af9Nqkmqnr96f7v9nHqzZT9XBwbXEkTiqT4ROuJSE=
github.com/aws/aws-sdk-go-v2/service/sso v1.32.1 h1:gYFYh4iLLcAOJRLNPY2aD2g9DIhKn4eof8UkIrr1rTk=
github.com/aws/aws-sdk-go-v2/service/sso v1.32.1/go.mod h1:u8af9Nqkmqnr96f7v9nHqzZT9XBwbXEkTiqT4ROuJSE=
github.com/aws/aws-sdk-go-v2/service/sso v1.33.0 h1:CaJyYhxBE0M/HJX/YvSaSmQlsI91VHB0lKU8LtLxL3A=
github.com/aws/aws-sdk-go-v2/service/sso v1.33.0/go.mod h1:+e6BMRMPjBQoCw/WovYR9GLy2IU0z4Q77smOB1DraSg=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.8 h1:6dBT1Lz8fK11m22R+AqfRsFn8320K0T5DTGxxOQBSMw=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.8/go.mod h1:/kiBvRQXBc6xeJTYzhSdGvJ5vm1tjaDEjH+MSeRJnlY=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6 h1:yLr03zQE/5Eu5l3QU0Si+xMbLMbSDF2YXsigqXngs6g=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6/go.mod h1:Q5N6icH+KJZDLh+ESNwzdv6cZ6vLFF/egy3IOxWhmz4=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.37.0 h1:fpOlDPI55HdszaxapEGk6HsGosOUaM2YPWJpjMgp8UI=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.37.0/go.mod h1:DMPWJBjYs6+3+f/qhBFEFPPlQ6NlhWjai3dJNvipJ84=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.37.1 h1:arjT9Cm3/WYbGmD5TUZHk4UQn4Lle1fUNZs5FC6CtF0=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.37.1/go.mod h1:DMPWJBjYs6+3+f/qhBFEFPPlQ6NlhWjai3dJNvipJ84=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.0 h1:tC323YV77QdafeBr6LUhLDTsboyuyHLNRwAyCP44kGU=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.0/go.mod h1:SfLK1sgviHmbI+MozR9iDwDjL4cdCVZtahsjoR+z7wg=
github.com/aws/aws-sdk-go-v2/service/sts v1.33.6 h1:VwhTrsTuVn52an4mXx29PqRzs2Dvu921NpGk7y43tAM=
github.com/aws/aws-sdk-go-v2/service/sts v1.33.6/go.mod h1:+8h7PZb3yY5ftmVLD7ocEoE98hdc8PoKS0H3wfx1dlc=
github.com/aws/aws-sdk-go-v2/service/sts v1.43.3 h1:VrIhKRCSK1umelSgB9RghvA9RTUYeQffyAS5ApXehNI=
github.com/aws/aws-sdk-go-v2/service/sts v1.43.3/go.mod h1:r8wkDOuLaaMFqFiYAb8dGY2A3gJCOujMc6CFOVC4Zhc=
github.com/aws/aws-sdk-go-v2/service/sts v1.44.0 h1:bLZ0PolJ8J+HkJHztcXORUpHXBye2U8298lCEMi6ZCU=
github.com/aws/aws-sdk-go-v2/service/sts v1.44.0/go.mod h1:9gdl4RrflIdpDb2TlXshWgR1F9TeCkvqDx77Vpr4Z/Q=
github.com/aws/aws-sdk-go-v2/service/sts v1.44.1 h1:RvfHDg+xvAeZ+5741vUEjpOVtYSIm93W2zhx10Xtydw=
github.com/aws/aws-sdk-go-v2/service/sts v1.44.1/go.mod h1:9gdl4RrflIdpDb2TlXshWgR1F9TeCkvqDx77Vpr4Z/Q=
github.com/aws/aws-sdk-go-v2/service/sts v1.45.0 h1:Pd6PNlp4t8PTXxqzstICl52Wsy78vpjFZ7PRUj44mJc=
github.com/aws/aws-sdk-go-v2/service/sts v1.45.0/go.mod h1:rmQ0TnHzuLPmabgjPcsywhsSOmaBDgzR4zvDxSPsGdg=
github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro=
github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
github.com/aws/smithy-go v1.27.1 h1:4T340VFndXtADGF52gYa1POyL7s9E4Z1OeZ1hCscIw8=
github.com/aws/smithy-go v1.27.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/aws/smithy-go v1.27.2 h1:y9NPmSE6am6LjEFPfqHqG/jJk7AauQvhCJONKh7kpzk=
github.com/aws/smithy-go v1.27.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/aws/smithy-go v1.27.3 h1:F3Zb497UhhskkfpJmfkXswyo+t0sh9OTBnIHjogWbVY=
github.com/aws/smithy-go v1.27.3/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik=
@@ -121,6 +205,8 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/detectors/gcp v1.42.0 h1:kpt2PEJuOuqYkPcktfJqWWDjTEd/FNgrxcniL7kQrXQ=
go.opentelemetry.io/contrib/detectors/gcp v1.42.0/go.mod h1:W9zQ439utxymRrXsUOzZbFX4JhLxXU4+ZnCt8GG7yA8=
go.opentelemetry.io/contrib/detectors/gcp v1.43.0 h1:62yY3dT7/ShwOxzA0RsKRgshBmfElKI4d/Myu2OxDFU=
go.opentelemetry.io/contrib/detectors/gcp v1.43.0/go.mod h1:RyaZMFY7yi1kAs45S6mbFGz8O8rqB0dTY14uzvG4LCs=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
@@ -149,6 +235,8 @@ golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
@@ -174,6 +262,10 @@ google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
google.golang.org/grpc v1.82.0 h1:vguDnZUPjE26w09A63VoxZPnvPjB5Riyc0mkXPFmAIU=
google.golang.org/grpc v1.82.0/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE=
google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
google.golang.org/protobuf v1.26.0-rc.1 h1:7QnIQpGRHE5RnLKnESfDoxm2dTapTZua5a0kS0A+VXQ=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
+307 -158
View File
@@ -4,9 +4,9 @@
"com.amazonaws:aws-lambda-java-core": -179547809,
"com.amazonaws:aws-lambda-java-events": -1998571449,
"com.google.android:annotations": 1910180409,
"com.google.api.grpc:proto-google-common-protos": -2106826238,
"com.google.auth:google-auth-library-credentials": 1149797254,
"com.google.auth:google-auth-library-oauth2-http": -2102758232,
"com.google.api.grpc:proto-google-common-protos": 391189699,
"com.google.auth:google-auth-library-credentials": 43407369,
"com.google.auth:google-auth-library-oauth2-http": 1085819179,
"com.google.auto.value:auto-value": 1146221104,
"com.google.auto.value:auto-value-annotations": -1101276935,
"com.google.code.findbugs:jsr305": 495355163,
@@ -25,11 +25,11 @@
"com.squareup.okio:okio": 795722926,
"com.thesamet.scalapb:compilerplugin_3": 603908795,
"com.thesamet.scalapb:lenses_3": -1873974815,
"com.thesamet.scalapb:protoc-bridge_3": -1260006911,
"com.thesamet.scalapb:protoc-bridge_3": -1482619205,
"com.thesamet.scalapb:scalapb-json4s_3": -772830803,
"com.thesamet.scalapb:scalapb-runtime-grpc_3": -2026114331,
"com.thesamet.scalapb:scalapb-runtime_3": -1281968352,
"com.zaxxer:HikariCP": 1858991878,
"com.zaxxer:HikariCP": 1991920386,
"io.netty:netty-buffer": 732437843,
"io.netty:netty-codec": -1199307927,
"io.netty:netty-codec-http": -1456497900,
@@ -39,30 +39,31 @@
"io.netty:netty-handler": -1970529483,
"io.netty:netty-handler-proxy": -195328652,
"io.netty:netty-resolver": -548277043,
"io.netty:netty-tcnative-boringssl-static": 2033070843,
"io.netty:netty-tcnative-classes": 2023773715,
"io.netty:netty-tcnative-boringssl-static": -754344644,
"io.netty:netty-tcnative-classes": -763641772,
"io.netty:netty-transport": -1962486730,
"io.netty:netty-transport-native-epoll": -1215800119,
"io.netty:netty-transport-native-unix-common": -818773192,
"io.netty:netty-transport-native-epoll": 833393060,
"io.netty:netty-transport-native-unix-common": 1230419987,
"io.opencensus:opencensus-api": 2042364617,
"io.opencensus:opencensus-contrib-grpc-metrics": 317094357,
"io.perfmark:perfmark-api": 1019157794,
"io.sentry:sentry": 563160305,
"io.sentry:sentry": -699383028,
"javax.xml.bind:jaxb-api": -1735500680,
"junit:junit": -744267592,
"org.checkerframework:checker-qual": -819031361,
"org.codehaus.mojo:animal-sniffer-annotations": -1634689353,
"org.codehaus.mojo:animal-sniffer-annotations": 852823480,
"org.json4s:json4s-ast_3": 687176866,
"org.json4s:json4s-core_3": 977579605,
"org.json4s:json4s-native_3": -123383795,
"org.postgresql:postgresql": -1963352047,
"org.mockito:mockito-core": -1925148636,
"org.postgresql:postgresql": -1283293677,
"org.reactivestreams:reactive-streams": 509083534,
"org.scala-lang:scala3-staging_3": -360398695,
"org.scalameta:scalafmt-cli_2.13": 243143182,
"org.scalameta:scalafmt-cli_2.13": 1603259922,
"org.scalamock:scalamock_3": -1047317697,
"org.slf4j:slf4j-api": -2005163028,
"org.slf4j:slf4j-simple": 1999565066,
"org.xerial:sqlite-jdbc": 1165654128,
"org.xerial:sqlite-jdbc": -641800335,
"repositories": 514854214,
"software.amazon.awssdk:aws-core": 578934204,
"software.amazon.awssdk:http-client-spi": 1408046502,
@@ -85,8 +86,8 @@
"com.google.android:annotations": 769933135,
"com.google.api.grpc:proto-google-common-protos": 1506674904,
"com.google.api:api-common": 1729001290,
"com.google.auth:google-auth-library-credentials": 563614471,
"com.google.auth:google-auth-library-oauth2-http": -1252786723,
"com.google.auth:google-auth-library-credentials": -426926107,
"com.google.auth:google-auth-library-oauth2-http": -307063036,
"com.google.auto.value:auto-value": -1399392164,
"com.google.auto.value:auto-value-annotations": 1092025564,
"com.google.code.findbugs:jsr305": -1038659426,
@@ -95,8 +96,8 @@
"com.google.guava:failureaccess": -56291903,
"com.google.guava:guava": 2119270017,
"com.google.guava:listenablefuture": 1588902908,
"com.google.http-client:google-http-client": 1756875461,
"com.google.http-client:google-http-client-gson": 1267916085,
"com.google.http-client:google-http-client": -174274814,
"com.google.http-client:google-http-client-gson": 1658743026,
"com.google.j2objc:j2objc-annotations": -2074422376,
"com.google.protobuf:protobuf-java": -1065767575,
"com.google.re2j:re2j": -461460138,
@@ -110,15 +111,15 @@
"com.squareup.okhttp:okhttp": -1936180231,
"com.squareup.okio:okio": 1978550531,
"com.squareup.okio:okio-jvm": -1730532943,
"com.thesamet.scalapb:compilerplugin_3": -1073075945,
"com.thesamet.scalapb:lenses_3": 791427023,
"com.thesamet.scalapb:protoc-bridge_3": -1984150046,
"com.thesamet.scalapb:protoc-gen_3": 1860775213,
"com.thesamet.scalapb:scalapb-json4s_3": 1579843487,
"com.thesamet.scalapb:scalapb-runtime-grpc_3": 97373115,
"com.thesamet.scalapb:scalapb-runtime_3": 818778125,
"com.typesafe:config": -1791448454,
"com.zaxxer:HikariCP": -704610647,
"com.thesamet.scalapb:compilerplugin_3": -986481869,
"com.thesamet.scalapb:lenses_3": 1632980089,
"com.thesamet.scalapb:protoc-bridge_3": -1510160103,
"com.thesamet.scalapb:protoc-gen_3": 36781197,
"com.thesamet.scalapb:scalapb-json4s_3": 1449885529,
"com.thesamet.scalapb:scalapb-runtime-grpc_3": 1266714255,
"com.thesamet.scalapb:scalapb-runtime_3": 318411326,
"com.typesafe:config": 1779132647,
"com.zaxxer:HikariCP": 337938184,
"commons-codec:commons-codec": 1377992676,
"commons-io:commons-io": 669567296,
"dev.dirs:directories": 1716880629,
@@ -149,28 +150,30 @@
"io.netty:netty-handler": 1617793819,
"io.netty:netty-handler-proxy": 1503431664,
"io.netty:netty-resolver": -533324791,
"io.netty:netty-tcnative-boringssl-static": -1922162184,
"io.netty:netty-tcnative-boringssl-static:jar:linux-aarch_64": -310918884,
"io.netty:netty-tcnative-boringssl-static:jar:linux-x86_64": 1582797105,
"io.netty:netty-tcnative-boringssl-static:jar:osx-aarch_64": -1315559501,
"io.netty:netty-tcnative-boringssl-static:jar:osx-x86_64": -95251834,
"io.netty:netty-tcnative-boringssl-static:jar:windows-x86_64": -1202154110,
"io.netty:netty-tcnative-classes": -1020773440,
"io.netty:netty-tcnative-boringssl-static": 1274713091,
"io.netty:netty-tcnative-boringssl-static:jar:linux-aarch_64": 423479165,
"io.netty:netty-tcnative-boringssl-static:jar:linux-x86_64": 1509601219,
"io.netty:netty-tcnative-boringssl-static:jar:osx-aarch_64": -1452923369,
"io.netty:netty-tcnative-boringssl-static:jar:osx-x86_64": -1682600003,
"io.netty:netty-tcnative-boringssl-static:jar:windows-x86_64": 741676114,
"io.netty:netty-tcnative-classes": -2096215842,
"io.netty:netty-transport": -1390736500,
"io.netty:netty-transport-classes-epoll": 704589083,
"io.netty:netty-transport-native-epoll:jar:linux-x86_64": -2050990961,
"io.netty:netty-transport-native-epoll:jar:linux-x86_64": -933296886,
"io.netty:netty-transport-native-unix-common": -2090924105,
"io.opencensus:opencensus-api": -1002511030,
"io.opencensus:opencensus-contrib-grpc-metrics": -1797408624,
"io.opencensus:opencensus-contrib-http-util": -541858674,
"io.perfmark:perfmark-api": -752483303,
"io.sentry:sentry": 1564972879,
"io.sentry:sentry": 1646897141,
"javax.activation:javax.activation-api": -217704356,
"javax.annotation:javax.annotation-api": 193132517,
"javax.inject:javax.inject": -1284481446,
"javax.xml.bind:jaxb-api": 1600326587,
"joda-time:joda-time": 230482511,
"junit:junit": 712711302,
"net.bytebuddy:byte-buddy": 105401824,
"net.bytebuddy:byte-buddy-agent": -1761476423,
"net.java.dev.jna:jna": 943627808,
"net.java.dev.jna:jna-platform": 1004830334,
"org.apache.commons:commons-compress": -308222379,
@@ -181,7 +184,7 @@
"org.apache.httpcomponents:httpclient": -504016800,
"org.apache.httpcomponents:httpcore": -353060362,
"org.apache.xbean:xbean-reflect": -1764161877,
"org.checkerframework:checker-qual": -53559553,
"org.checkerframework:checker-qual": 1032417002,
"org.codehaus.mojo:animal-sniffer-annotations": -1570976252,
"org.codehaus.plexus:plexus-archiver": 766678324,
"org.codehaus.plexus:plexus-classworlds": -543968237,
@@ -194,52 +197,54 @@
"org.jetbrains.kotlin:kotlin-stdlib-common": 11605,
"org.jetbrains:annotations": 116138949,
"org.jline:jline:jar:jdk8": -1133032957,
"org.json4s:json4s-ast_3": 689482233,
"org.json4s:json4s-core_3": 481951064,
"org.json4s:json4s-jackson-core_3": 813804735,
"org.json4s:json4s-native-core_3": -289663551,
"org.json4s:json4s-native_3": -2093359247,
"org.json4s:json4s-ast_3": 218697247,
"org.json4s:json4s-core_3": 888852031,
"org.json4s:json4s-jackson-core_3": -1708586056,
"org.json4s:json4s-native-core_3": -1556364554,
"org.json4s:json4s-native_3": 1810593063,
"org.jspecify:jspecify": -1402924792,
"org.mockito:mockito-core": 534163387,
"org.objenesis:objenesis": 684097820,
"org.ow2.asm:asm": 1177470613,
"org.postgresql:postgresql": -570711868,
"org.postgresql:postgresql": -1200274402,
"org.reactivestreams:reactive-streams": -1996658890,
"org.scala-lang.modules:scala-collection-compat_2.13": -2012838353,
"org.scala-lang.modules:scala-collection-compat_3": -915569507,
"org.scala-lang.modules:scala-collection-compat_3": 2001700859,
"org.scala-lang.modules:scala-xml_2.13": -1867291781,
"org.scala-lang:scala-compiler": 993782407,
"org.scala-lang:scala-library": -479379026,
"org.scala-lang:scala-reflect": 2030778053,
"org.scala-lang:scala3-library_3": -340570223,
"org.scala-lang:scala3-library_3": -1714704820,
"org.scala-lang:scala3-staging_3": 1767551804,
"org.scalameta:common2_2.13": 183869992,
"org.scalameta:common_2.13": 1613859331,
"org.scalameta:io_2.13": 1078868274,
"org.scalameta:mdoc-parser_2.13": 1998129208,
"org.scalameta:metaconfig-core_2.13": 1560822216,
"org.scalameta:metaconfig-pprint_2.13": -13676725,
"org.scalameta:metaconfig-typesafe-config_2.13": -1923428474,
"org.scalameta:munit-diff_2.13": 612923316,
"org.scalameta:parsers_2.13": -2012847908,
"org.scalameta:scalafmt-cli_2.13": -788023559,
"org.scalameta:scalafmt-config_2.13": 761717294,
"org.scalameta:scalafmt-core_2.13": 1878102965,
"org.scalameta:scalafmt-dynamic-core_2.13": -1900839476,
"org.scalameta:scalafmt-dynamic_2.13": 860328364,
"org.scalameta:scalafmt-interfaces": 651108583,
"org.scalameta:scalafmt-macros_2.13": -1771055183,
"org.scalameta:scalafmt-sysops_2.13": -1263777532,
"org.scalameta:scalameta_2.13": -428808011,
"org.scalameta:common2_2.13": 1796949606,
"org.scalameta:common_2.13": 1892326143,
"org.scalameta:io_2.13": -1056988288,
"org.scalameta:mdoc-parser_2.13": -377476829,
"org.scalameta:metaconfig-core_2.13": -557456764,
"org.scalameta:metaconfig-pprint_2.13": 268613438,
"org.scalameta:metaconfig-typesafe-config_2.13": 618711541,
"org.scalameta:munit-diff_2.13": 667211464,
"org.scalameta:parsers_2.13": 970620234,
"org.scalameta:scalafmt-cli_2.13": 1904050715,
"org.scalameta:scalafmt-config_2.13": 1077807261,
"org.scalameta:scalafmt-core_2.13": -542782848,
"org.scalameta:scalafmt-dynamic-core_2.13": -565068427,
"org.scalameta:scalafmt-dynamic_2.13": -133643165,
"org.scalameta:scalafmt-interfaces": -1699622215,
"org.scalameta:scalafmt-macros_2.13": -1322813645,
"org.scalameta:scalafmt-sysops_2.13": -1994112303,
"org.scalameta:scalameta_2.13": 625605450,
"org.scalameta:svm-subs": -181782616,
"org.scalameta:trees2_2.13": -1248113756,
"org.scalameta:trees_2.13": -1233441989,
"org.scalamock:scalamock_3": -9538025,
"org.scalameta:trees2_2.13": 1553624450,
"org.scalameta:trees_2.13": 1539326839,
"org.scalamock:scalamock_3": 23905729,
"org.slf4j:slf4j-api": -1249720338,
"org.slf4j:slf4j-simple": -1378006603,
"org.tukaani:xz": -1726944826,
"org.typelevel:paiges-core_2.13": 1005692255,
"org.virtuslab.scala-cli:config_2.13": -113493958,
"org.virtuslab.scala-cli:specification-level_2.13": -176416420,
"org.xerial:sqlite-jdbc": 532995836,
"org.xerial:sqlite-jdbc": -2058325677,
"software.amazon.awssdk:annotations": 998747622,
"software.amazon.awssdk:apache5-client": -1074889369,
"software.amazon.awssdk:arns": -343721679,
@@ -274,12 +279,11 @@
"software.amazon.eventstream:eventstream": 1951806649
},
"conflict_resolution": {
"com.google.api.grpc:proto-google-common-protos:2.63.1": "com.google.api.grpc:proto-google-common-protos:2.64.1",
"com.google.guava:failureaccess:1.0.1": "com.google.guava:failureaccess:1.0.3",
"com.google.j2objc:j2objc-annotations:2.8": "com.google.j2objc:j2objc-annotations:3.1",
"io.netty:netty-transport-native-unix-common:4.1.127.Final": "io.netty:netty-transport-native-unix-common:4.1.135.Final",
"io.netty:netty-transport-native-unix-common:4.1.133.Final": "io.netty:netty-transport-native-unix-common:4.1.135.Final",
"io.opencensus:opencensus-api:0.31.0": "io.opencensus:opencensus-api:0.31.1",
"org.codehaus.mojo:animal-sniffer-annotations:1.26": "org.codehaus.mojo:animal-sniffer-annotations:1.27"
"org.checkerframework:checker-qual:3.49.5": "org.checkerframework:checker-qual:3.55.1"
},
"artifacts": {
"com.amazonaws:aws-lambda-java-core": {
@@ -356,15 +360,15 @@
},
"com.google.auth:google-auth-library-credentials": {
"shasums": {
"jar": "dd7e39b8f5c366b09f8c2d400b7b63c925499df813b0f0d78044abd72098e159"
"jar": "d29934610c3e9eb6c60e95ad05ca34ce486c3867c2f3689548b4aa2c2dd6ec52"
},
"version": "1.40.0"
"version": "1.42.1"
},
"com.google.auth:google-auth-library-oauth2-http": {
"shasums": {
"jar": "67fe2c9df9780d8725c1dff61d2ffbbcb93fdcf15e91c44a5e610a4a69afd151"
"jar": "fb54ada3c0223ffb4470fb6d8fbd55f764588dbd244dc7c05caaf4ab62977898"
},
"version": "1.40.0"
"version": "1.42.1"
},
"com.google.auto.value:auto-value": {
"shasums": {
@@ -416,15 +420,15 @@
},
"com.google.http-client:google-http-client": {
"shasums": {
"jar": "97a0666340956dbc96893bc8cf843a5b57367543f218feddad3ff433fbfdaa68"
"jar": "20aa0948f6ceb5cfa9b2b3adddd9b7e990ab40230e084081642cbb81235d3eef"
},
"version": "2.0.2"
"version": "2.1.0"
},
"com.google.http-client:google-http-client-gson": {
"shasums": {
"jar": "37c8fd5c7869bc4abf3b3219c7e79cb566afa745559772e969c21595199600cd"
"jar": "3cf72c7b88375a34bf721c664f42edc8e096e2370e4126c9b8933c3c92e591f3"
},
"version": "2.0.2"
"version": "2.1.0"
},
"com.google.j2objc:j2objc-annotations": {
"shasums": {
@@ -518,9 +522,9 @@
},
"com.thesamet.scalapb:protoc-bridge_3": {
"shasums": {
"jar": "e7e2f1862f54076b6870bd034a7c16aae7b88cfee3d00b69dbb6b1175108560c"
"jar": "019cee53ec9e4eb6be0f8b72be4c9a97b56ddd780da5128a4db97af4af32e186"
},
"version": "0.9.9"
"version": "0.9.10"
},
"com.thesamet.scalapb:protoc-gen_3": {
"shasums": {
@@ -548,15 +552,15 @@
},
"com.typesafe:config": {
"shasums": {
"jar": "dd6b9fb5da5bc49d31b581703f57050e1e7d403814f62f3832181a7226b36256"
"jar": "47d13e3196c84f2a8ccd014ae563738c3da5b7406b38e03505ec0601a2f4fc37"
},
"version": "1.4.6"
"version": "1.4.9"
},
"com.zaxxer:HikariCP": {
"shasums": {
"jar": "709f378c05756280939ce50fc1b1f1a53bb8e1899dc1b249f21f12703640b48b"
"jar": "2be45c641b841e81df87292e181f2c203500c5f0e9a1676f135927387f3cf40b"
},
"version": "6.3.3"
"version": "7.1.0"
},
"commons-codec:commons-codec": {
"shasums": {
@@ -740,20 +744,20 @@
},
"io.netty:netty-tcnative-boringssl-static": {
"shasums": {
"jar": "ec3b14ceff74c5d7e24a378e64074744cc5e7035c49cf6ca0a4e23dff9713d1f",
"linux-aarch_64": "11dfee82cfcb5c4f596271fe1d56a70fe77b4034e54162d0bd211623b540857f",
"linux-x86_64": "39ac6b1eb4ffc18d5fe9412cc6253a8954bafc8e3ca10109bc370490f73a3673",
"osx-aarch_64": "eef3fc7cdc5e2dc8c5cb50bae11443052bf1ee2f6e8b64378565746d1529f05e",
"osx-x86_64": "e3beaa3ebdb136569528c59aa826d771efadc0ac9db484c6c0e457fa5efd0f07",
"windows-x86_64": "5458b4b502763d49cd2e16c434312687178afac9c164cabf75c4a8bcecf7ac50"
"jar": "ffb1fd42e5952863159f10a370e8c2dcfa3f40615ec4caefaa9ac8b1b1790233",
"linux-aarch_64": "cba3868793eb7ad32963275f32eb3e8932a3d50192557af8abd2516dab8b8956",
"linux-x86_64": "8e4bf375ae8b82fe657f54d142beb35d115d8294ec5d23a9c09afc8a93dd4928",
"osx-aarch_64": "10c6dd7eee84c099ea9e40ccbd933b501c665738d610627c5390d729fc978a47",
"osx-x86_64": "b5a5048cff2a64641cbbe9fcfffbf032ef506b4d2836c8ba9e689b3c461b24fc",
"windows-x86_64": "dee808e0dd5b4c14b4e8bd8d116dff347163b32574b2fb11a2ade476f7d03628"
},
"version": "2.0.74.Final"
"version": "2.0.75.Final"
},
"io.netty:netty-tcnative-classes": {
"shasums": {
"jar": "194874cf723794dd409fd1e728cd91ffbcff0584d73b4d8a1ad0d69d04acf9b3"
"jar": "2d5a6a8be78bba7782eec385ba178b12f5bfc6837771fd3bb8c1d7cd18b6911c"
},
"version": "2.0.74.Final"
"version": "2.0.75.Final"
},
"io.netty:netty-transport": {
"shasums": {
@@ -769,9 +773,9 @@
},
"io.netty:netty-transport-native-epoll": {
"shasums": {
"linux-x86_64": "3d03f27eea1cd23357a56a96e1eeedfeb3d74fa0ba4d5c36a862bd035056275b"
"linux-x86_64": "fdc76fe46e7218ef54547c1002aff69c4597291cc3ef34322d6f1a2f3090c880"
},
"version": "4.1.127.Final"
"version": "4.1.133.Final"
},
"io.netty:netty-transport-native-unix-common": {
"shasums": {
@@ -805,9 +809,9 @@
},
"io.sentry:sentry": {
"shasums": {
"jar": "8f3d16c54f013694fd987689a2b37fa6ed7767b95de79d1949a0e24a1d62cc1a"
"jar": "0bac4f4330e94ca19eb91640618d22a89b47ce45ffb79aa3d55e41f9b4f33517"
},
"version": "8.43.2"
"version": "8.50.1"
},
"javax.activation:javax.activation-api": {
"shasums": {
@@ -845,6 +849,18 @@
},
"version": "4.13.2"
},
"net.bytebuddy:byte-buddy": {
"shasums": {
"jar": "42ba43dcccd8d9d77bfe8776a83e72b67f1fa52c4038a98629e7d288b648da4e"
},
"version": "1.12.8"
},
"net.bytebuddy:byte-buddy-agent": {
"shasums": {
"jar": "18faf4f33893c3e883a3220adfdab25d6ceff9e46b1ad760a5b6f83df9373307"
},
"version": "1.12.8"
},
"net.java.dev.jna:jna": {
"shasums": {
"jar": "0c8eb7acf67261656d79005191debaba3b6bf5dd60a43735a245429381dbecff"
@@ -907,9 +923,9 @@
},
"org.checkerframework:checker-qual": {
"shasums": {
"jar": "508c83c62c344f6f7ee28f47b88a8797d6116d043bfd1ca0576c828dd1df2880"
"jar": "857658c8bdcbff794949a8d9922f88d63ee3751c203c7cd7d21ea97f9e745288"
},
"version": "3.49.5"
"version": "3.55.1"
},
"org.codehaus.mojo:animal-sniffer-annotations": {
"shasums": {
@@ -1019,6 +1035,18 @@
},
"version": "1.0.0"
},
"org.mockito:mockito-core": {
"shasums": {
"jar": "9a5f59ad06d0bb7d0fd2d4a74eb4d66d561ee0327f0febacb7f9cfb5be4abbfb"
},
"version": "4.4.0"
},
"org.objenesis:objenesis": {
"shasums": {
"jar": "03d960bd5aef03c653eb000413ada15eb77cdd2b8e4448886edf5692805e35f3"
},
"version": "3.2"
},
"org.ow2.asm:asm": {
"shasums": {
"jar": "876eab6a83daecad5ca67eb9fcabb063c97b5aeb8cf1fca7a989ecde17522051"
@@ -1027,9 +1055,9 @@
},
"org.postgresql:postgresql": {
"shasums": {
"jar": "1981b31d3993c58702783c1cddf10a34e48c1f413d70ff1cb6def0a143484647"
"jar": "6e0e4cc2d8cae902084f8a2b18728b073a6fd9d1f87c9d8bff8f298c18185b93"
},
"version": "42.7.11"
"version": "42.7.13"
},
"org.reactivestreams:reactive-streams": {
"shasums": {
@@ -1075,9 +1103,9 @@
},
"org.scala-lang:scala3-library_3": {
"shasums": {
"jar": "a16e046f4a9df9e0d0e0bc54bf6298c94d7d867457f7b3b68291a88c568b92d0"
"jar": "2c6eace9158df8ee4485a06128a479d968c1ebfc83ae9b1a394b58429f0d1492"
},
"version": "3.3.7"
"version": "3.3.8"
},
"org.scala-lang:scala3-staging_3": {
"shasums": {
@@ -1087,111 +1115,111 @@
},
"org.scalameta:common2_2.13": {
"shasums": {
"jar": "42a4fae02d82dc7450dddd69ce1d8b8f159119132898af56169f77b8c9a623bd"
"jar": "d16050f3557e472ac3489501edd4d941d975bc0a6f72a54363b6598e29a972d0"
},
"version": "4.17.0"
"version": "4.17.3"
},
"org.scalameta:common_2.13": {
"shasums": {
"jar": "3b5b1f67809daf6044113737af8aa4cc5c3b36f071950cfcf7d02f4fe6b1d19a"
"jar": "e15f1eca7e1d855daf0fd62874b944272b84d2bbee96f3adfaef0cdd34ec91e4"
},
"version": "4.17.0"
"version": "4.17.3"
},
"org.scalameta:io_2.13": {
"shasums": {
"jar": "d09dac9887efb0aaf7bbcc5b5e20f7e7e4947800592a44359e2f31b2b6c9699d"
"jar": "c9aa7493cf90a1acafcc96dad84670409aeb3b5e280a38ce40332eb6a21cf49f"
},
"version": "4.17.0"
"version": "4.17.3"
},
"org.scalameta:mdoc-parser_2.13": {
"shasums": {
"jar": "37442a255f0e6a31c9786d7d8af4ef1e29ded76c4f44af29895b4b36d904a068"
"jar": "4ce63efb015b6ea92bd0bcf107c8153b3e2857495821a9a59dc41424b4015831"
},
"version": "2.9.0"
"version": "2.9.1"
},
"org.scalameta:metaconfig-core_2.13": {
"shasums": {
"jar": "f8ed537b95758409a0dd0a9533fdd6da703914f5901f42d6f96ba07c77f58aaf"
"jar": "9493d98e6a9b8b43bdbd7a00e06fa1dcf295180e0bf0d94d705f18801c75ec80"
},
"version": "0.18.6"
"version": "0.18.7"
},
"org.scalameta:metaconfig-pprint_2.13": {
"shasums": {
"jar": "cbe1531b67e0c080294e3eaeea35952524c91af0c100f517ba7004607f44be46"
"jar": "9f727dfdf0883c4066509e82f9ab58142ce0fb25371599d4a1b15e35d52467af"
},
"version": "0.18.6"
"version": "0.18.7"
},
"org.scalameta:metaconfig-typesafe-config_2.13": {
"shasums": {
"jar": "1ddbdfcc9f425ca385fa0e0079b8f6fc12571b2e03c9a3ae2bc0c832ca288bca"
"jar": "85085a7a082eead66bf0dccd08d402e5a4bad1f3fc73673284fac8e8f9e6c4ec"
},
"version": "0.18.6"
"version": "0.18.7"
},
"org.scalameta:munit-diff_2.13": {
"shasums": {
"jar": "789f02dfc17e62626560d48522d16bc550990a4d722d27b38d215a39dd8c9a5d"
"jar": "79d52c3007ae90ee2c6e156e3b2a5c0cfeb5a0dd67e570a36cb9a9e84f6e1521"
},
"version": "1.3.0"
"version": "1.3.4"
},
"org.scalameta:parsers_2.13": {
"shasums": {
"jar": "c0669f252bf96db52ac32c92bf54f2853b8f192287a827626c3efe51b20f94b3"
"jar": "21931f60d741a95f6fc364f5514672b587b24a2316df0a80fc67059a9989828d"
},
"version": "4.17.0"
"version": "4.17.3"
},
"org.scalameta:scalafmt-cli_2.13": {
"shasums": {
"jar": "1da5a56393f5fcf01d65c86ca91adc5f8d5448447a75bf9d8b229db5a0a84009"
"jar": "86ab6706dcefa4e0efd8bd9fe6ab5d3223dfa3efcf2cd74cd0fddc2dc09864ee"
},
"version": "3.11.1"
"version": "3.11.5"
},
"org.scalameta:scalafmt-config_2.13": {
"shasums": {
"jar": "c058aefc9f874c78072fca15a25e72a4d3ab31bc2872266c42cc6e2651f6cbf1"
"jar": "0e000fe9c11c991f9a012331b829a7295b10995c1f5421701755e1cce6373149"
},
"version": "3.11.1"
"version": "3.11.5"
},
"org.scalameta:scalafmt-core_2.13": {
"shasums": {
"jar": "f5f0e48927f0d5a32763f6b82a8427b40231db65a908c48b4438b000562c32d2"
"jar": "301fffd68a5723390d62ce62b075ed2e9c84003a0994634951638c4df6eb77f8"
},
"version": "3.11.1"
"version": "3.11.5"
},
"org.scalameta:scalafmt-dynamic-core_2.13": {
"shasums": {
"jar": "d4efc029f83f7c3ac6f3374b33636c3d38ef24bbb4d04ba300996d0e63991f2b"
"jar": "f55a6614c5815f4435451eaf24b0b0a771fc26a2b3603cc8f4428ccfc6ae96c0"
},
"version": "3.11.1"
"version": "3.11.5"
},
"org.scalameta:scalafmt-dynamic_2.13": {
"shasums": {
"jar": "101275030c12c0cc8baa9b6b015b93a4500915990a088d964f41fe482366b890"
"jar": "fcaaf533c46fd0c297e33a4363587202e973d27f37dc37538e17195d964d0f3a"
},
"version": "3.11.1"
"version": "3.11.5"
},
"org.scalameta:scalafmt-interfaces": {
"shasums": {
"jar": "ec8ac6769c5960e8ee04d4fee15a4b82ba6032b3df3f4d725221ef94dd1dad90"
"jar": "eb8c7d5fd1728b1ea1905817d18d6e5859e5614ee4bf417b20c8f60c439f6f4c"
},
"version": "3.11.1"
"version": "3.11.5"
},
"org.scalameta:scalafmt-macros_2.13": {
"shasums": {
"jar": "49a4ab5745cc0b54acdb74b3fd6017451d5699d5d327ca1bea0747bc622edc9f"
"jar": "7edb248e9d74ad95ce8cabf9fd903dc20419f405a3e86eedbeefdf02e291cbe5"
},
"version": "3.11.1"
"version": "3.11.5"
},
"org.scalameta:scalafmt-sysops_2.13": {
"shasums": {
"jar": "a862c2bd21ba6b83d403a8aca33af541de356f7d39e88c1656bd2cf6a3b28a80"
"jar": "314582e8086be283b9df1c22b41269cf22eb6fed5af37be0a6049ba5ee08594e"
},
"version": "3.11.1"
"version": "3.11.5"
},
"org.scalameta:scalameta_2.13": {
"shasums": {
"jar": "7606f691512cac7e52e1c28146cc95001212aeb3737b44d5ea6f4ec51b9a454c"
"jar": "5d7b942d074c764f10da068903f7a8667838d68c6e7a737ad0d0061cada0d182"
},
"version": "4.17.0"
"version": "4.17.3"
},
"org.scalameta:svm-subs": {
"shasums": {
@@ -1201,15 +1229,15 @@
},
"org.scalameta:trees2_2.13": {
"shasums": {
"jar": "c1ad0ac4c5617ee25ad97b72fb2d03c48d5ee89770e7cc70b0e3e984689296ce"
"jar": "3f3729adf6faa48802f8b7c77ac67a6077cb99073596dc3eaf48d6ca9371fe55"
},
"version": "4.17.0"
"version": "4.17.3"
},
"org.scalameta:trees_2.13": {
"shasums": {
"jar": "b45d46d995e166889cbda015f541ca79afd94ada860513438e6c0ab5f4f84591"
"jar": "38047a5a7b0f66faad77e18177a6152413de848b23353b615b1cecab2c0212f3"
},
"version": "4.17.0"
"version": "4.17.3"
},
"org.scalamock:scalamock_3": {
"shasums": {
@@ -1255,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": {
@@ -1850,6 +1878,14 @@
"org.json4s:json4s-native-core_3",
"org.scala-lang:scala3-library_3"
],
"org.mockito:mockito-core": [
"net.bytebuddy:byte-buddy",
"net.bytebuddy:byte-buddy-agent",
"org.objenesis:objenesis"
],
"org.postgresql:postgresql": [
"org.checkerframework:checker-qual"
],
"org.scala-lang.modules:scala-collection-compat_2.13": [
"org.scala-lang:scala-library"
],
@@ -2322,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",
@@ -2756,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",
@@ -2932,6 +2958,50 @@
"org.junit.runners.parameterized",
"org.junit.validator"
],
"net.bytebuddy:byte-buddy": [
"net.bytebuddy",
"net.bytebuddy.agent.builder",
"net.bytebuddy.asm",
"net.bytebuddy.build",
"net.bytebuddy.description",
"net.bytebuddy.description.annotation",
"net.bytebuddy.description.enumeration",
"net.bytebuddy.description.field",
"net.bytebuddy.description.method",
"net.bytebuddy.description.modifier",
"net.bytebuddy.description.type",
"net.bytebuddy.dynamic",
"net.bytebuddy.dynamic.loading",
"net.bytebuddy.dynamic.scaffold",
"net.bytebuddy.dynamic.scaffold.inline",
"net.bytebuddy.dynamic.scaffold.subclass",
"net.bytebuddy.implementation",
"net.bytebuddy.implementation.attribute",
"net.bytebuddy.implementation.auxiliary",
"net.bytebuddy.implementation.bind",
"net.bytebuddy.implementation.bind.annotation",
"net.bytebuddy.implementation.bytecode",
"net.bytebuddy.implementation.bytecode.assign",
"net.bytebuddy.implementation.bytecode.assign.primitive",
"net.bytebuddy.implementation.bytecode.assign.reference",
"net.bytebuddy.implementation.bytecode.collection",
"net.bytebuddy.implementation.bytecode.constant",
"net.bytebuddy.implementation.bytecode.member",
"net.bytebuddy.jar.asm",
"net.bytebuddy.jar.asm.commons",
"net.bytebuddy.jar.asm.signature",
"net.bytebuddy.matcher",
"net.bytebuddy.pool",
"net.bytebuddy.utility",
"net.bytebuddy.utility.dispatcher",
"net.bytebuddy.utility.nullability",
"net.bytebuddy.utility.privilege",
"net.bytebuddy.utility.visitor"
],
"net.bytebuddy:byte-buddy-agent": [
"net.bytebuddy.agent",
"net.bytebuddy.agent.utility.nullability"
],
"net.java.dev.jna:jna": [
"com.sun.jna",
"com.sun.jna.ptr",
@@ -2979,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",
@@ -3385,6 +3454,84 @@
"org.jspecify:jspecify": [
"org.jspecify.annotations"
],
"org.mockito:mockito-core": [
"org.mockito",
"org.mockito.codegen",
"org.mockito.configuration",
"org.mockito.creation.instance",
"org.mockito.exceptions.base",
"org.mockito.exceptions.misusing",
"org.mockito.exceptions.stacktrace",
"org.mockito.exceptions.verification",
"org.mockito.exceptions.verification.junit",
"org.mockito.exceptions.verification.opentest4j",
"org.mockito.hamcrest",
"org.mockito.internal",
"org.mockito.internal.configuration",
"org.mockito.internal.configuration.injection",
"org.mockito.internal.configuration.injection.filter",
"org.mockito.internal.configuration.injection.scanner",
"org.mockito.internal.configuration.plugins",
"org.mockito.internal.creation",
"org.mockito.internal.creation.bytebuddy",
"org.mockito.internal.creation.instance",
"org.mockito.internal.creation.proxy",
"org.mockito.internal.creation.settings",
"org.mockito.internal.creation.util",
"org.mockito.internal.debugging",
"org.mockito.internal.exceptions",
"org.mockito.internal.exceptions.stacktrace",
"org.mockito.internal.exceptions.util",
"org.mockito.internal.framework",
"org.mockito.internal.hamcrest",
"org.mockito.internal.handler",
"org.mockito.internal.invocation",
"org.mockito.internal.invocation.finder",
"org.mockito.internal.invocation.mockref",
"org.mockito.internal.junit",
"org.mockito.internal.listeners",
"org.mockito.internal.matchers",
"org.mockito.internal.matchers.apachecommons",
"org.mockito.internal.matchers.text",
"org.mockito.internal.progress",
"org.mockito.internal.reporting",
"org.mockito.internal.runners",
"org.mockito.internal.runners.util",
"org.mockito.internal.session",
"org.mockito.internal.stubbing",
"org.mockito.internal.stubbing.answers",
"org.mockito.internal.stubbing.defaultanswers",
"org.mockito.internal.util",
"org.mockito.internal.util.collections",
"org.mockito.internal.util.concurrent",
"org.mockito.internal.util.io",
"org.mockito.internal.util.reflection",
"org.mockito.internal.verification",
"org.mockito.internal.verification.api",
"org.mockito.internal.verification.argumentmatching",
"org.mockito.internal.verification.checkers",
"org.mockito.invocation",
"org.mockito.junit",
"org.mockito.listeners",
"org.mockito.mock",
"org.mockito.plugins",
"org.mockito.quality",
"org.mockito.session",
"org.mockito.stubbing",
"org.mockito.verification"
],
"org.objenesis:objenesis": [
"org.objenesis",
"org.objenesis.instantiator",
"org.objenesis.instantiator.android",
"org.objenesis.instantiator.annotations",
"org.objenesis.instantiator.basic",
"org.objenesis.instantiator.gcj",
"org.objenesis.instantiator.perc",
"org.objenesis.instantiator.sun",
"org.objenesis.instantiator.util",
"org.objenesis.strategy"
],
"org.ow2.asm:asm": [
"org.objectweb.asm",
"org.objectweb.asm.signature"
@@ -4042,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",
@@ -4174,6 +4319,8 @@
"javax.xml.bind:jaxb-api",
"joda-time:joda-time",
"junit:junit",
"net.bytebuddy:byte-buddy",
"net.bytebuddy:byte-buddy-agent",
"net.java.dev.jna:jna",
"net.java.dev.jna:jna-platform",
"org.apache.commons:commons-compress",
@@ -4203,6 +4350,8 @@
"org.json4s:json4s-native-core_3",
"org.json4s:json4s-native_3",
"org.jspecify:jspecify",
"org.mockito:mockito-core",
"org.objenesis:objenesis",
"org.ow2.asm:asm",
"org.postgresql:postgresql",
"org.reactivestreams:reactive-streams",
+46 -5
View File
@@ -6,10 +6,13 @@
"dependencyDashboard": true,
"enabledManagers": [
"bazel-module",
"bazelisk",
"cargo",
"custom.regex",
"github-actions",
"gomod",
"swift"
"swift",
"unity3d"
],
"labels": [
"dependencies"
@@ -18,7 +21,9 @@
{
"description": "Bazel module artifact updates run in GitHub Actions so hosted Renovate does not need unsafe bazel mod deps execution",
"matchManagers": [
"bazel-module"
"bazel-module",
"bazelisk",
"custom.regex"
],
"skipArtifactsUpdate": true
},
@@ -28,19 +33,55 @@
"gomod"
],
"skipArtifactsUpdate": true
},
{
"description": "Propose stable Unity editor patch updates for manual review",
"matchManagers": [
"unity3d"
],
"automerge": false,
"dependencyDashboardApproval": true,
"minimumReleaseAge": "7 days"
},
{
"description": "Keep Unity editor upgrades on the current release line",
"matchManagers": [
"unity3d"
],
"matchUpdateTypes": [
"major",
"minor"
],
"enabled": false
}
],
"customManagers": [
{
"customType": "regex",
"description": "Update the LLVM and Clang toolchain release",
"managerFilePatterns": [
"/^MODULE\\.bazel$/"
],
"matchStrings": [
"LLVM_VERSION = \"(?<currentValue>\\d+\\.\\d+\\.\\d+)\""
],
"depNameTemplate": "llvm/llvm-project",
"datasourceTemplate": "github-releases",
"extractVersionTemplate": "^llvmorg-(?<version>\\d+\\.\\d+\\.\\d+)$",
"versioningTemplate": "semver"
}
],
"lockFileMaintenance": {
"enabled": true,
"schedule": [
"before 6am on monday"
"* * * * 1"
]
},
"prConcurrentLimit": 3,
"prHourlyLimit": 1,
"prHourlyLimit": 3,
"rebaseWhen": "behind-base-branch",
"schedule": [
"before 6am on monday"
"* * * * 1"
],
"separateMajorMinor": true,
"timezone": "America/Los_Angeles"
+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 \
"$@" \
+102 -25
View File
@@ -4,41 +4,118 @@
# Reads the active instance from /opt/eagle0/.active-instance (set by deploy-blue-green.sh).
#
# Usage:
# eagle-logs # Tail logs (follow mode)
# eagle-logs -n 100 # Show last 100 lines and follow
# eagle-logs --no-follow -n 50 # Show last 50 lines without following
# eagle-logs # Show existing logs and keep following
# eagle-logs -n 100 # Show the last 100 lines and keep following
# eagle-logs --no-follow -n 50 # Show the last 50 lines and exit
#
# To create an alias, add to ~/.bashrc:
# alias eagle-logs='/opt/eagle0/scripts/eagle-logs.sh'
#
set -euo pipefail
set -uo pipefail
APP_DIR="${APP_DIR:-/opt/eagle0}"
ACTIVE_FILE="${APP_DIR}/.active-instance"
RETRY_INTERVAL_SECONDS="${EAGLE_LOGS_RETRY_INTERVAL_SECONDS:-2}"
# Read active instance from file, with fallback
if [ -f "$ACTIVE_FILE" ]; then
ACTIVE=$(cat "$ACTIVE_FILE")
else
# Fallback: check which container is actually running
if docker inspect --format='{{.State.Running}}' eagle-blue 2>/dev/null | grep -q true; then
ACTIVE="eagle-blue"
elif docker inspect --format='{{.State.Running}}' eagle-green 2>/dev/null | grep -q true; then
ACTIVE="eagle-green"
else
ACTIVE=""
trap 'exit 130' INT
trap 'exit 143' TERM
FOLLOW=true
DOCKER_LOG_ARGS=()
for arg in "$@"; do
case "$arg" in
--no-follow)
FOLLOW=false
;;
-f | --follow)
FOLLOW=true
;;
*)
DOCKER_LOG_ARGS+=("$arg")
;;
esac
done
container_is_running() {
[ "$(docker inspect --format='{{.State.Running}}' "$1" 2>/dev/null)" = "true" ]
}
resolve_active_instance() {
local recorded_active=""
if [ -f "$ACTIVE_FILE" ]; then
IFS= read -r recorded_active < "$ACTIVE_FILE" || true
fi
if [ -n "$recorded_active" ] && container_is_running "$recorded_active"; then
printf '%s\n' "$recorded_active"
elif container_is_running eagle-blue; then
printf '%s\n' "eagle-blue"
elif container_is_running eagle-green; then
printf '%s\n' "eagle-green"
else
return 1
fi
}
print_disconnect_marker() {
local stopped_instance=${1:-unknown}
printf '\n' >&2
printf '%s\n' '================================================================================' >&2
printf '%s\n' '==================== EAGLE LOG STREAM DISCONNECTED ============================' >&2
printf ' Server unavailable at %s (last instance: %s)\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$stopped_instance" >&2
printf '%s\n' ' Waiting for the deployment or server restart; log following will resume.' >&2
printf '%s\n' '================================================================================' >&2
}
print_resume_marker() {
local active_instance=$1
printf '\n' >&2
printf '%s\n' '================================================================================' >&2
printf '%s\n' '======================= EAGLE LOG STREAM RESUMED ==============================' >&2
printf ' Attached to %s at %s\n' "$active_instance" "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" >&2
printf '%s\n' '================================================================================' >&2
}
if [ "$FOLLOW" = false ]; then
if ! ACTIVE=$(resolve_active_instance); then
echo "Error: No active Eagle instance found" >&2
exit 1
fi
exec docker logs "${DOCKER_LOG_ARGS[@]}" "$ACTIVE"
fi
if [ -z "$ACTIVE" ]; then
echo "Error: No active Eagle instance found" >&2
exit 1
fi
ACTIVE=""
DISCONNECTED=false
RECONNECT_SINCE=""
# Default to follow mode if no args provided
if [ $# -eq 0 ]; then
exec docker logs -f "$ACTIVE"
else
exec docker logs "$@" "$ACTIVE"
fi
while true; do
if NEXT_ACTIVE=$(resolve_active_instance); then
if [ "$DISCONNECTED" = true ]; then
print_resume_marker "$NEXT_ACTIVE"
fi
ACTIVE="$NEXT_ACTIVE"
DISCONNECTED=false
if [ -n "$RECONNECT_SINCE" ]; then
docker logs --follow "${DOCKER_LOG_ARGS[@]}" --since "$RECONNECT_SINCE" "$ACTIVE" || true
else
docker logs --follow "${DOCKER_LOG_ARGS[@]}" "$ACTIVE" || true
fi
RECONNECT_SINCE=$(date +%s)
fi
if [ "$DISCONNECTED" = false ]; then
print_disconnect_marker "$ACTIVE"
DISCONNECTED=true
fi
sleep "$RETRY_INTERVAL_SECONDS"
done
+85
View File
@@ -0,0 +1,85 @@
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TEST_DIR=$(mktemp -d)
trap 'rm -rf "$TEST_DIR"' EXIT
FAKE_BIN="${TEST_DIR}/bin"
APP_DIR="${TEST_DIR}/app"
STATE_FILE="${TEST_DIR}/state"
CALLS_FILE="${TEST_DIR}/calls"
OUTPUT_FILE="${TEST_DIR}/output"
mkdir -p "$FAKE_BIN" "$APP_DIR"
cat > "${FAKE_BIN}/docker" <<'EOF'
#!/bin/bash
set -euo pipefail
case "$1" in
inspect)
container=${@: -1}
state=$(cat "$EAGLE_LOGS_TEST_STATE_FILE")
if { [ "$state" = "blue" ] && [ "$container" = "eagle-blue" ]; } ||
{ [ "$state" = "green" ] && [ "$container" = "eagle-green" ]; }; then
echo true
else
echo false
fi
;;
logs)
container=${@: -1}
printf '%s\n' "$*" >> "$EAGLE_LOGS_TEST_CALLS_FILE"
if [ "$container" = "eagle-blue" ]; then
echo "last blue log"
echo "eagle-green" > "$EAGLE_LOGS_TEST_ACTIVE_FILE"
echo "green" > "$EAGLE_LOGS_TEST_STATE_FILE"
else
echo "first green log"
fi
;;
*)
echo "Unexpected docker command: $*" >&2
exit 1
;;
esac
EOF
chmod +x "${FAKE_BIN}/docker"
export EAGLE_LOGS_TEST_STATE_FILE="$STATE_FILE"
export EAGLE_LOGS_TEST_CALLS_FILE="$CALLS_FILE"
export EAGLE_LOGS_TEST_ACTIVE_FILE="${APP_DIR}/.active-instance"
echo "blue" > "$STATE_FILE"
echo "eagle-blue" > "${APP_DIR}/.active-instance"
PATH="${FAKE_BIN}:$PATH" APP_DIR="$APP_DIR" "${SCRIPT_DIR}/eagle-logs.sh" --no-follow -n 25
grep -q '^logs -n 25 eagle-blue$' "$CALLS_FILE"
: > "$CALLS_FILE"
echo "blue" > "$STATE_FILE"
echo "eagle-blue" > "${APP_DIR}/.active-instance"
PATH="${FAKE_BIN}:$PATH" APP_DIR="$APP_DIR" EAGLE_LOGS_RETRY_INTERVAL_SECONDS=0.01 \
"${SCRIPT_DIR}/eagle-logs.sh" -n 10 > "$OUTPUT_FILE" 2>&1 &
FOLLOW_PID=$!
for _ in $(seq 1 100); do
if grep -q 'first green log' "$OUTPUT_FILE"; then
break
fi
sleep 0.01
done
kill "$FOLLOW_PID" 2>/dev/null || true
wait "$FOLLOW_PID" 2>/dev/null || true
grep -q 'EAGLE LOG STREAM DISCONNECTED' "$OUTPUT_FILE"
grep -q 'EAGLE LOG STREAM RESUMED' "$OUTPUT_FILE"
grep -q 'last blue log' "$OUTPUT_FILE"
grep -q 'first green log' "$OUTPUT_FILE"
grep -q '^logs --follow -n 10 eagle-blue$' "$CALLS_FILE"
grep -Eq '^logs --follow -n 10 --since [0-9]+ eagle-green$' "$CALLS_FILE"
echo "eagle-logs tests passed"
@@ -105,6 +105,7 @@ public:
class Iter {
public:
using iterator_concept = std::forward_iterator_tag;
using iterator_category = std::forward_iterator_tag;
using difference_type = std::ptrdiff_t;
using value_type = size_t;
@@ -112,14 +113,14 @@ public:
using reference = size_t;
private:
size_t storeIndex;
size_t bitIndex;
size_t storeIndex = STORE_COUNT;
size_t bitIndex = 0;
const IterableBitset& bs;
const IterableBitset* bs = nullptr;
void ToNext() {
do {
const STORE_TYPE current = bs.store[storeIndex] >> bitIndex;
const STORE_TYPE current = bs->store[storeIndex] >> bitIndex;
if (current != 0) {
// Skip ahead by the number of trailing zeroes
const auto ctz = std::countr_zero(current);
@@ -132,10 +133,12 @@ public:
}
public:
Iter() = default;
Iter(const IterableBitset& bs, const int startStore, const int startBit)
: storeIndex(startStore),
bitIndex(startBit),
bs(bs) {
bs(&bs) {
ToNext();
}
@@ -40,7 +40,8 @@ auto AttackLocations::LocationsWithEnemyInRange(const Unit *unit) const -> Coord
AttackLocationsCache::AttackLocationsCache(const HexMap *hexMap, const SettingsGetter &settings)
: rowCount(hexMap->row_count()),
columnCount(hexMap->column_count()) {
columnCount(hexMap->column_count()),
landRegions(hexMap) {
const int mageRange =
std::max(settings.Backing().meteor_range(), settings.Backing().lightning_range());
const auto fearRange = settings.Backing().fear_range();
@@ -11,6 +11,7 @@
#include "src/main/cpp/net/eagle0/shardok/library/map/Coordinates.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/map/LandRegions.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
#include "src/main/flatbuffer/net/eagle0/shardok/storage/unit.hpp"
@@ -151,6 +152,8 @@ struct alignas(128) AttackLocationsCache {
private:
const MapIndex rowCount;
const MapIndex columnCount;
// Immutable static-map analysis shared by every speculative state evaluated by this AI.
const LandRegions landRegions;
// For each of these vectors, the index is the map index of the tile *that can be attacked*,
// and the entry is the CoordsSet of the tiles that can attack that location.
@@ -161,6 +164,7 @@ private:
public:
AttackLocationsCache(const HexMap* hexMap, const SettingsGetter& settings);
[[nodiscard]] auto GetLandRegions() const -> const LandRegions& { return landRegions; }
auto CachedLocations(const Coords& singleTarget, bool lateGame) -> const AttackLocations&;
auto CachedLocations(const CoordsSet& targets, bool lateGame) -> AttackLocations;
auto CachedLocations(const std::vector<const Unit*>& enemies, bool lateGame) -> AttackLocations;
@@ -23,7 +23,6 @@
namespace shardok {
constexpr bool kMultithread = true;
constexpr bool kLogging = false;
[[nodiscard]] static auto AverageGenerator() -> std::shared_ptr<SequenceRandomGenerator> {
@@ -120,10 +119,12 @@ static auto CommandSorter(
AICommandEvaluator::AICommandEvaluator(
const AIScoreCalculator& scorer,
const APDCache& apdCache,
BattalionTypeGetter battalionTypeGetter)
BattalionTypeGetter battalionTypeGetter,
AIThreadPool& threadPool)
: scorer_(scorer),
apdCache_(apdCache),
battalionTypeGetter_(std::move(battalionTypeGetter)) {}
battalionTypeGetter_(std::move(battalionTypeGetter)),
threadPool_(threadPool) {}
auto AICommandEvaluator::PerformLookahead(
const PlayerId pid,
@@ -295,7 +296,7 @@ auto AICommandEvaluator::EvaluateWithRandomness(
returnValue.lookaheadScore = p.get_future();
p.set_value(EvaluationResult{.score = innerUtility, .completed = true});
} else {
auto lookaheadLambda = [this,
auto lookaheadLambda = [evaluator = *this,
pid,
isDefender,
remainingLookahead,
@@ -307,8 +308,11 @@ auto AICommandEvaluator::EvaluateWithRandomness(
guessedCommands,
allCastleCoords,
deadline]() -> EvaluationResult {
if (std::chrono::steady_clock::now() > deadline) {
return EvaluationResult{.score = 0.0, .completed = false};
}
try {
auto lookaheadFuture = PerformLookahead(
auto lookaheadFuture = evaluator.PerformLookahead(
pid,
isDefender,
remainingLookahead,
@@ -329,15 +333,14 @@ auto AICommandEvaluator::EvaluateWithRandomness(
}
};
if constexpr (kMultithread) {
auto launchPolicy =
remainingLookahead == 1 ? std::launch::async : std::launch::deferred;
returnValue.lookaheadScore = std::async(launchPolicy, lookaheadLambda);
// Only the depth-one frontier is parallel. A pool worker therefore reaches depth zero,
// never another queued frontier. AIThreadPool also executes same-pool reentrant
// submissions inline as a hard backstop against worker-waits-on-worker deadlocks.
if (remainingLookahead == 1) {
returnValue.lookaheadScore = threadPool_.Submit(std::move(lookaheadLambda));
} else {
std::promise<EvaluationResult> p;
returnValue.lookaheadScore = p.get_future();
auto lambdaResult = lookaheadLambda();
p.set_value(lambdaResult);
returnValue.lookaheadScore =
std::async(std::launch::deferred, std::move(lookaheadLambda));
}
}
@@ -10,6 +10,7 @@
#include <future>
#include "src/main/cpp/net/eagle0/shardok/ai/AIStrategy.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/AIThreadPool.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCTypes.h"
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistancesCache.hpp"
@@ -35,7 +36,8 @@ public:
AICommandEvaluator(
const AIScoreCalculator& scorer,
const APDCache& apdCache,
BattalionTypeGetter battalionTypeGetter); // Pass by value
BattalionTypeGetter battalionTypeGetter,
AIThreadPool& threadPool = SharedAIThreadPool()); // Pass getter by value
/// Evaluates the score for a particular command index with lookahead.
struct EvaluationResult {
@@ -79,6 +81,7 @@ private:
const AIScoreCalculator& scorer_;
const APDCache& apdCache_;
BattalionTypeGetter battalionTypeGetter_;
AIThreadPool& threadPool_;
struct ImmediateAndLookaheadScore {
ScoreValue immediateScore{};
@@ -4,9 +4,6 @@
#include "AIDefenderStrategySelector.hpp"
#include <algorithm>
#include <ranges>
#include "src/main/cpp/net/eagle0/shardok/ai/AIAttackGroups.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/AIScoreUtilities.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/AIWaterCrossingCalculator.hpp"
@@ -23,31 +20,14 @@ auto AIDefenderStrategySelector::BestDefenderStrategy(
const CoordsSet& criticalTileCoords,
int maxRounds,
const APDCache& apdCache,
const LandRegions& landRegions,
const BattalionTypeGetter& battalionTypeGetter) -> AIStrategy {
uint32_t attackerNonUndeadUnitCount = 0;
uint32_t attackerNonUndeadUnitNotRequiringWaterCrossingCount = 0;
int attackerTroops = 0;
int defenderTroops = 0;
bool canFlee = false;
vector<UnitId> attackerUnitIdsRequiringWaterCrossing{};
attackerUnitIdsRequiringWaterCrossing.reserve(gameState->units()->size());
for (const auto& player : *gameState->player_infos()) {
if (!player->is_defender()) {
const auto& unitIdsRequiringWaterCrossing = UnitIdsRequiringWaterCrossing(
gameState,
player->player_id(),
criticalTileCoords,
apdCache,
battalionTypeGetter);
attackerUnitIdsRequiringWaterCrossing.insert(
attackerUnitIdsRequiringWaterCrossing.end(),
unitIdsRequiringWaterCrossing.begin(),
unitIdsRequiringWaterCrossing.end());
}
}
for (const Unit* unit : *gameState->units()) {
if (unit->status() != net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
unit->status() != net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT)
@@ -63,12 +43,6 @@ auto AIDefenderStrategySelector::BestDefenderStrategy(
if (unit->battalion().type() !=
net::eagle0::shardok::storage::fb::BattalionTypeId_UNDEAD) {
++attackerNonUndeadUnitCount;
if (!std::ranges::contains(
attackerUnitIdsRequiringWaterCrossing,
unit->unit_id())) {
++attackerNonUndeadUnitNotRequiringWaterCrossingCount;
}
}
attackerTroops += unit->battalion().size();
}
@@ -86,11 +60,21 @@ auto AIDefenderStrategySelector::BestDefenderStrategy(
// other considerations we should have but do not presently:
// * the attacker is close to the defender / castles
if (canFlee && roundsRemaining > 2 &&
defenderTroops < MAXIMUM_RATIO_FOR_DEFENDER_TO_FLEE * attackerTroops &&
attackerNonUndeadUnitNotRequiringWaterCrossingCount >= criticalTileCoords.size()) {
chosenStrategy = FleeStrategy;
} else if (attackerNonUndeadUnitCount >= criticalTileCoords.size()) {
const bool waterCrossingCouldAffectStrategy =
canFlee && roundsRemaining > 2 &&
defenderTroops < MAXIMUM_RATIO_FOR_DEFENDER_TO_FLEE * attackerTroops &&
attackerNonUndeadUnitCount >= criticalTileCoords.size();
if (waterCrossingCouldAffectStrategy && HasEnoughNonUndeadAttackersWithoutWaterCrossing(
gameState,
criticalTileCoords,
landRegions,
apdCache,
battalionTypeGetter,
criticalTileCoords.size())) {
return FleeStrategy;
}
if (attackerNonUndeadUnitCount >= criticalTileCoords.size()) {
chosenStrategy = HoldCastlesStrategy;
} else if (defenderTroops > MINIMUM_RATIO_FOR_DEFENDER_TO_HOLD * attackerTroops) {
chosenStrategy = HoldCastlesStrategy;
@@ -22,6 +22,7 @@ public:
const CoordsSet& criticalTileCoords,
int maxRounds,
const APDCache& apdCache,
const LandRegions& landRegions,
const BattalionTypeGetter& battalionTypeGetter) -> AIStrategy;
};
} // namespace shardok
@@ -0,0 +1,70 @@
#include "AIThreadPool.hpp"
#include <algorithm>
namespace shardok {
namespace {
// Keep the process responsive to gRPC and command handling while still giving lookahead enough
// parallelism to saturate typical production hosts.
constexpr size_t kMaximumAIWorkerCount = 12;
} // namespace
thread_local const AIThreadPool* AIThreadPool::currentPool_ = nullptr;
AIThreadPool::AIThreadPool(const size_t workerCount) {
const size_t normalizedWorkerCount = std::max<size_t>(1, workerCount);
workers_.reserve(normalizedWorkerCount);
try {
for (size_t index = 0; index < normalizedWorkerCount; ++index) {
workers_.emplace_back([this] { RunWorker(); });
}
} catch (...) {
StopAndJoin();
throw;
}
}
AIThreadPool::~AIThreadPool() { StopAndJoin(); }
void AIThreadPool::RunWorker() {
currentPool_ = this;
while (true) {
std::function<void()> task;
{
std::unique_lock lock(mutex_);
condition_.wait(lock, [this] { return stopping_ || !tasks_.empty(); });
if (stopping_ && tasks_.empty()) { break; }
task = std::move(tasks_.front());
tasks_.pop();
}
task();
}
currentPool_ = nullptr;
}
void AIThreadPool::StopAndJoin() {
{
std::lock_guard lock(mutex_);
stopping_ = true;
}
condition_.notify_all();
for (auto& worker : workers_) {
if (worker.joinable()) { worker.join(); }
}
}
auto AIWorkerCountForHardwareConcurrency(const unsigned int hardwareConcurrency) -> size_t {
if (hardwareConcurrency <= 1) { return 1; }
return std::min(kMaximumAIWorkerCount, static_cast<size_t>(hardwareConcurrency - 1));
}
auto SharedAIThreadPool() -> AIThreadPool& {
static AIThreadPool pool(
AIWorkerCountForHardwareConcurrency(std::thread::hardware_concurrency()));
return pool;
}
} // namespace shardok
@@ -0,0 +1,79 @@
//
// Fixed-size worker pool for iterative-deepening lookahead.
//
#ifndef EAGLE0_SHARDOK_AI_AI_THREAD_POOL_HPP
#define EAGLE0_SHARDOK_AI_AI_THREAD_POOL_HPP
#include <condition_variable>
#include <cstddef>
#include <functional>
#include <future>
#include <memory>
#include <mutex>
#include <queue>
#include <stdexcept>
#include <thread>
#include <type_traits>
#include <utility>
#include <vector>
namespace shardok {
class AIThreadPool {
public:
explicit AIThreadPool(size_t workerCount);
~AIThreadPool();
AIThreadPool(const AIThreadPool&) = delete;
auto operator=(const AIThreadPool&) -> AIThreadPool& = delete;
AIThreadPool(AIThreadPool&&) = delete;
auto operator=(AIThreadPool&&) -> AIThreadPool& = delete;
template<typename Function>
[[nodiscard]] auto Submit(Function&& function)
-> std::future<std::invoke_result_t<std::decay_t<Function>&>> {
using Result = std::invoke_result_t<std::decay_t<Function>&>;
auto task =
std::make_shared<std::packaged_task<Result()>>(std::forward<Function>(function));
auto result = task->get_future();
// A worker must never enqueue work and then wait for the same bounded pool. Running
// reentrant submissions inline makes that safe even if recursive search scheduling
// changes in the future.
if (currentPool_ == this) {
(*task)();
return result;
}
{
std::lock_guard lock(mutex_);
if (stopping_) { throw std::runtime_error("Submit on stopped AIThreadPool"); }
tasks_.emplace([task] { (*task)(); });
}
condition_.notify_one();
return result;
}
[[nodiscard]] auto WorkerCount() const -> size_t { return workers_.size(); }
private:
void RunWorker();
void StopAndJoin();
static thread_local const AIThreadPool* currentPool_;
std::vector<std::thread> workers_;
std::queue<std::function<void()>> tasks_;
std::mutex mutex_;
std::condition_variable condition_;
bool stopping_ = false;
};
[[nodiscard]] auto AIWorkerCountForHardwareConcurrency(unsigned int hardwareConcurrency) -> size_t;
[[nodiscard]] auto SharedAIThreadPool() -> AIThreadPool&;
} // namespace shardok
#endif // EAGLE0_SHARDOK_AI_AI_THREAD_POOL_HPP
@@ -4,53 +4,251 @@
#include "AIWaterCrossingCalculator.hpp"
#include <algorithm>
#include <array>
#include <optional>
#include "AIMinimumDistanceAndTarget.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/HexMapHelpers.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/map/HexMapHasher.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/map/LandRegions.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"
namespace shardok {
namespace {
constexpr int kMaxWaterCrossingDistance = 1000;
constexpr double kCrossingCreatorDistanceMultiplier = 100.0;
auto RecomputeModifierHash(HexMap *hexMap) -> MapId {
hexMap->mutate_modifier_hash(GetModifierHash(hexMap));
return ActionPointDistancesCache::GetMapId(hexMap);
}
auto HasNonWaterFire(const HexMap *hexMap) -> bool {
return std::ranges::any_of(*hexMap->terrain(), [](const auto *terrain) {
return !IsWater(terrain->type()) && terrain->modifier().fire().present();
});
}
auto StaticRegionsAreConclusiveFor(const BattalionType &battalionType) -> bool {
return battalionType.costToEnterWater.type == ActionCost::impossible &&
battalionType.costToEnterMountain.type == ActionCost::impossible;
}
auto CanReachWithDistances(
const Coords &origin,
const Coords &destination,
const ActionPointDistances *distances) -> bool {
return distances->Distance(origin, destination) != ActionPointDistances::IMPOSSIBLE;
}
auto CanReachAllWithDistances(
const Coords &origin,
const CoordsSet &destinations,
const ActionPointDistances *distances) -> bool {
return std::ranges::all_of(destinations, [&](const Coords &destination) {
return CanReachWithDistances(origin, destination, distances);
});
}
class EffectiveDestinations {
private:
const EffectiveLandRegions &effectiveLandRegions;
std::optional<Coords> representative;
bool shareRegion;
public:
EffectiveDestinations(
const EffectiveLandRegions &effectiveLandRegions,
const CoordsSet &destinations)
: effectiveLandRegions(effectiveLandRegions),
shareRegion(true) {
for (const Coords &destination : destinations) {
if (!representative.has_value()) {
representative = destination;
} else if (!effectiveLandRegions.DefinitelyConnectedWithoutCreatingCrossing(
*representative,
destination)) {
shareRegion = false;
break;
}
}
}
[[nodiscard]] auto DefinitelyReachableFrom(const Coords &origin) const -> bool {
return !representative.has_value() ||
(shareRegion && effectiveLandRegions.DefinitelyConnectedWithoutCreatingCrossing(
origin,
*representative));
}
};
auto DefinitelyReachesAllWithoutCreatingCrossing(
const HexMap *map,
const net::eagle0::shardok::storage::fb::Unit *unit,
const EffectiveDestinations &effectiveDestinations) -> bool {
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) {
return effectiveDestinations.DefinitelyReachableFrom(unit->location());
}
const auto *startingPositions =
map->attacker_starting_positions()->Get(unit->starting_position_index())->positions();
return std::ranges::all_of(*startingPositions, [&](const Coords *position) {
return effectiveDestinations.DefinitelyReachableFrom(*position);
});
}
auto CanReachAllWithoutWater(
const HexMap *map,
const net::eagle0::shardok::storage::fb::Unit *unit,
const CoordsSet &destinations,
const ActionPointDistances *distances) -> bool {
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) {
return CanReachAllWithDistances(unit->location(), destinations, distances);
}
const auto *startingPositions =
map->attacker_starting_positions()->Get(unit->starting_position_index())->positions();
return std::ranges::all_of(*startingPositions, [&](const Coords *position) {
return CanReachAllWithDistances(*position, destinations, distances);
});
}
auto IsActiveNonUndeadAttacker(
const GameState *gameState,
const net::eagle0::shardok::storage::fb::Unit *unit) -> bool {
if (unit->status() != net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
unit->status() != net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT) {
return false;
}
if (unit->battalion().type() == net::eagle0::shardok::storage::fb::BattalionTypeId_UNDEAD) {
return false;
}
return std::ranges::any_of(*gameState->player_infos(), [&](const auto *playerInfo) {
return playerInfo->player_id() == unit->player_id() && !playerInfo->is_defender();
});
}
auto CanCreateWaterCrossing(
const net::eagle0::shardok::storage::fb::Unit *unit,
const BattalionTypeGetter &battalionTypeGetter) -> bool {
if (!unit->has_attached_hero()) return false;
const auto profession = unit->attached_hero().profession_info().profession();
const auto &battalionType = battalionTypeGetter(unit->battalion().type());
return profession == net::eagle0::shardok::storage::fb::Profession_ENGINEER ||
(profession == net::eagle0::shardok::storage::fb::Profession_MAGE &&
battalionType->allowsCasting);
}
} // namespace
auto UnitIdsRequiringWaterCrossing(
const GameStateW &gameState,
const PlayerId pid,
const CoordsSet &destinations,
const LandRegions &landRegions,
const APDCache &apdCache,
const BattalionTypeGetter &battalionTypeGetter) -> vector<UnitId> {
// Put out all the fires, except on bridges
fb::HexMapW mapCopy = fb::CopyHexMap(gameState->hex_map());
for (uint32_t index = 0; index < mapCopy->terrain()->size(); ++index) {
if (IsWater(mapCopy->terrain()->Get(index)->type())) continue;
// const_cast is safe because we own the mutable buffer (mapCopy)
const_cast<net::eagle0::shardok::storage::fb::Terrain *>(
mapCopy->mutable_terrain()->GetMutableObject(index))
->mutable_modifier()
.mutable_fire()
.mutate_present(false);
}
const GameState *state = gameState.Get();
const auto *units = state->units();
const HexMap *map = state->hex_map();
const EffectiveLandRegions effectiveLandRegions(landRegions, map);
const EffectiveDestinations effectiveDestinations(effectiveLandRegions, destinations);
const auto mapId = ActionPointDistancesCache::GetMapId(mapCopy);
vector<UnitId> unitIdsToReturn{};
// Set this to -1 for unchecked, 0 for does not need crossing, 1 for does need crossing
std::vector<int> startingPositionIndices(9, -1);
for (const auto *unit : *gameState->units()) {
std::vector<const net::eagle0::shardok::storage::fb::Unit *> unitsToCheck{};
const auto queueForExactCheck = [&](const auto *unit) {
if (unitsToCheck.empty()) { unitsToCheck.reserve(units->size()); }
unitsToCheck.push_back(unit);
};
std::array<int8_t, 9> startingPositionIndices{};
startingPositionIndices.fill(-1);
for (const auto *unit : *units) {
if (unit->player_id() != pid) continue;
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) {
if (!effectiveDestinations.DefinitelyReachableFrom(unit->location())) {
queueForExactCheck(unit);
}
} else if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT) {
if (startingPositionIndices[unit->starting_position_index()] == -1) {
startingPositionIndices[unit->starting_position_index()] = 0;
const auto *startingPositions = map->attacker_starting_positions()
->Get(unit->starting_position_index())
->positions();
for (const Coords *position : *startingPositions) {
if (!effectiveDestinations.DefinitelyReachableFrom(*position)) {
startingPositionIndices[unit->starting_position_index()] = 1;
break;
}
}
}
if (startingPositionIndices[unit->starting_position_index()] == 1) {
queueForExactCheck(unit);
}
}
}
if (unitsToCheck.empty()) return {};
MapId mapId = ActionPointDistancesCache::GetMapId(map);
fb::HexMapW fireClearedMap;
bool mapPreparedForDistances = false;
std::array<const ActionPointDistances *, BattalionTypeId::BattalionTypeId_MAX + 1>
distancesByBattalionType{};
const auto distancesFor = [&](const BattalionTypeSPtr &battalionType) {
if (!mapPreparedForDistances) {
mapPreparedForDistances = true;
// Most states have no relevant fire, so avoid copying the entire map unless exact
// distances need it.
if (HasNonWaterFire(map)) {
fireClearedMap = fb::CopyHexMap(map);
for (uint32_t index = 0; index < fireClearedMap->terrain()->size(); ++index) {
if (IsWater(fireClearedMap->terrain()->Get(index)->type())) continue;
// const_cast is safe because we own the mutable buffer (fireClearedMap)
const_cast<net::eagle0::shardok::storage::fb::Terrain *>(
fireClearedMap->mutable_terrain()->GetMutableObject(index))
->mutable_modifier()
.mutable_fire()
.mutate_present(false);
}
map = fireClearedMap.Get();
mapId = RecomputeModifierHash(fireClearedMap.Get());
}
}
auto &distances = distancesByBattalionType[battalionType->typeId];
if (distances == nullptr) {
distances = apdCache->GetRaw(map, mapId, battalionType, false);
}
return distances;
};
vector<UnitId> unitIdsToReturn{};
unitIdsToReturn.reserve(unitsToCheck.size());
// The effective-region pass above removed every unit that can reach every destination through
// static land and current bridges. Resolve full APDs only for battalions that can traverse raw
// water or mountains.
startingPositionIndices.fill(-1);
for (const auto *unit : unitsToCheck) {
const auto &battType = battalionTypeGetter(unit->battalion().type());
if (StaticRegionsAreConclusiveFor(*battType)) {
unitIdsToReturn.push_back(unit->unit_id());
continue;
}
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) {
const auto *distances = distancesFor(battType);
for (const Coords &destination : destinations) {
if (!CanReach(unit->location(), destination, mapCopy, mapId, apdCache, battType)) {
if (!CanReachWithDistances(unit->location(), destination, distances)) {
unitIdsToReturn.push_back(unit->unit_id());
break;
}
@@ -58,14 +256,14 @@ auto UnitIdsRequiringWaterCrossing(
} else if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT) {
if (startingPositionIndices[unit->starting_position_index()] == -1) {
startingPositionIndices[unit->starting_position_index()] = 0;
const auto *startingPositions = gameState->hex_map()
->attacker_starting_positions()
const auto *distances = distancesFor(battType);
const auto *startingPositions = map->attacker_starting_positions()
->Get(unit->starting_position_index())
->positions();
for (const Coords *pos : *startingPositions) {
for (const Coords &destination : destinations) {
if (!CanReach(*pos, destination, mapCopy, mapId, apdCache, battType)) {
if (!CanReachWithDistances(*pos, destination, distances)) {
startingPositionIndices[unit->starting_position_index()] = 1;
break;
}
@@ -83,6 +281,87 @@ auto UnitIdsRequiringWaterCrossing(
return unitIdsToReturn;
}
auto HasEnoughNonUndeadAttackersWithoutWaterCrossing(
const GameStateW &gameState,
const CoordsSet &destinations,
const LandRegions &landRegions,
const APDCache &apdCache,
const BattalionTypeGetter &battalionTypeGetter,
const std::size_t requiredUnitCount) -> bool {
if (requiredUnitCount == 0) return true;
const GameState *state = gameState.Get();
const auto *units = state->units();
const HexMap *baseMap = state->hex_map();
const EffectiveLandRegions effectiveLandRegions(landRegions, baseMap);
const EffectiveDestinations effectiveDestinations(effectiveLandRegions, destinations);
std::size_t eligibleUnitCount = 0;
std::size_t reachableUnitCount = 0;
for (const auto *unit : *units) {
if (!IsActiveNonUndeadAttacker(state, unit)) continue;
++eligibleUnitCount;
if (DefinitelyReachesAllWithoutCreatingCrossing(baseMap, unit, effectiveDestinations) &&
++reachableUnitCount >= requiredUnitCount) {
return true;
}
}
if (eligibleUnitCount < requiredUnitCount) return false;
const HexMap *map = baseMap;
MapId mapId = ActionPointDistancesCache::GetMapId(baseMap);
fb::HexMapW fireClearedMap;
bool mapPreparedForDistances = false;
std::array<const ActionPointDistances *, BattalionTypeId::BattalionTypeId_MAX + 1>
distancesByBattalionType{};
const auto distancesFor = [&](const BattalionTypeSPtr &battalionType) {
if (!mapPreparedForDistances) {
mapPreparedForDistances = true;
if (HasNonWaterFire(map)) {
fireClearedMap = fb::CopyHexMap(map);
for (uint32_t index = 0; index < fireClearedMap->terrain()->size(); ++index) {
if (IsWater(fireClearedMap->terrain()->Get(index)->type())) continue;
// const_cast is safe because we own the mutable buffer (fireClearedMap)
const_cast<net::eagle0::shardok::storage::fb::Terrain *>(
fireClearedMap->mutable_terrain()->GetMutableObject(index))
->mutable_modifier()
.mutable_fire()
.mutate_present(false);
}
map = fireClearedMap.Get();
mapId = RecomputeModifierHash(fireClearedMap.Get());
}
}
auto &distances = distancesByBattalionType[battalionType->typeId];
if (distances == nullptr) {
distances = apdCache->GetRaw(map, mapId, battalionType, false);
}
return distances;
};
for (const auto *unit : *units) {
if (!IsActiveNonUndeadAttacker(state, unit)) continue;
if (DefinitelyReachesAllWithoutCreatingCrossing(baseMap, unit, effectiveDestinations)) {
continue;
}
const auto &battalionType = battalionTypeGetter(unit->battalion().type());
if (StaticRegionsAreConclusiveFor(*battalionType)) continue;
if (CanReachAllWithoutWater(map, unit, destinations, distancesFor(battalionType)) &&
++reachableUnitCount >= requiredUnitCount) {
return true;
}
}
return false;
}
auto UnitIdsToCreateWaterCrossing(
const GameStateW &gameState,
const PlayerId pid,
@@ -94,14 +373,8 @@ auto UnitIdsToCreateWaterCrossing(
if (unit->status() != net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
unit->status() != net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT)
continue;
if (!unit->has_attached_hero()) continue;
const auto profession = unit->attached_hero().profession_info().profession();
const auto &battalionType = battalionTypeGetter(unit->battalion().type());
if (profession == net::eagle0::shardok::storage::fb::Profession_ENGINEER ||
(profession == net::eagle0::shardok::storage::fb::Profession_MAGE &&
battalionType->allowsCasting)) {
if (CanCreateWaterCrossing(unit, battalionTypeGetter)) {
unitIds.push_back(unit->unit_id());
}
}
@@ -116,10 +389,10 @@ auto CanReach(
const MapId &mapId,
const APDCache &apdCache,
const BattalionTypeSPtr &battalionType) -> bool {
const DIST_T startingDistance =
apdCache->GetRaw(hexMap, mapId, battalionType, false)->Distance(origin, destination);
return startingDistance != ActionPointDistances::IMPOSSIBLE;
return CanReachWithDistances(
origin,
destination,
apdCache->GetRaw(hexMap, mapId, battalionType, false));
}
// Returns the *non*-water tiles from which you could bridge/freeze water to allow a unit to cross
@@ -181,7 +454,7 @@ auto WaterCrossingTiles(
terr->mutable_modifier().mutable_bridge().mutate_integrity(100.0);
terr->mutable_modifier().mutable_fire().mutate_present(false);
auto hash = ActionPointDistancesCache::GetMapId(mapCopy);
const MapId hash = RecomputeModifierHash(mapCopy.Get());
if (const auto *distances = apdCache->GetRaw(mapCopy, hash, battalionType, false);
distances->Distance(origin, destination) != ActionPointDistances::IMPOSSIBLE) {
@@ -274,6 +547,7 @@ auto WaterCrossingScore(
const GameStateW &gameState,
const CoordsSet &castleCoords,
const CoordsSet &startCrossingFrom,
const LandRegions &landRegions,
const APDCache &apdCache) -> double {
uint32_t castleClaimCount = 0;
for (const auto *unit : *gameState->units()) {
@@ -303,6 +577,7 @@ auto WaterCrossingScore(
gameState,
playerId,
castleCoords,
landRegions,
apdCache,
battalionTypeGetter);
if (unitIdsRequiringCrossing.empty()) return kNoRequiredCrossingScore;
@@ -314,6 +589,31 @@ auto WaterCrossingScore(
const auto mapId = ActionPointDistancesCache::GetMapId(gameState->hex_map());
// Put a big penalty on the distance for every unit that could create a crossing, on a single
// saturating scale: unavailable (fled/captured/destroyed) and unplaced creators sit at the
// maximum, and live distances (including IMPOSSIBLE) are clamped to it. Losing a crossing
// creator therefore never scores better than any placed position.
for (const auto *unit : *gameState->units()) {
if (unit->player_id() != playerId) continue;
if (!CanCreateWaterCrossing(unit, battalionTypeGetter)) continue;
const auto status = unit->status();
const bool available =
status == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT ||
status == net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT;
int thisDistance = kMaxWaterCrossingDistance;
if (available && unit->location().row() >= 0) {
const auto &battalionType = battalionTypeGetter(unit->battalion().type());
const auto *apd = apdCache->GetRaw(gameState->hex_map(), mapId, battalionType, false);
thisDistance = std::min(
MinimumDistance(apd, unit->location(), startCrossingFrom),
kMaxWaterCrossingDistance);
}
totalScore -= thisDistance * kCrossingCreatorDistanceMultiplier;
}
if (unitIdsCreatingCrossing.empty()) {
for (const UnitId uid : unitIdsRequiringCrossing) {
const Unit *unit = gameState->units()->Get(uid);
@@ -321,31 +621,15 @@ auto WaterCrossingScore(
const Coords location = unit->location();
const auto *apd = apdCache->GetRaw(gameState->hex_map(), mapId, battalionType, false);
const int thisDistance =
location.row() < 0 ? 1000 : MinimumDistance(apd, location, startCrossingFrom);
const int thisDistance = location.row() < 0
? kMaxWaterCrossingDistance
: MinimumDistance(apd, location, startCrossingFrom);
totalScore -= thisDistance;
}
return totalScore;
}
// First put a big penalty on the distance for units that can create a crossing
for (const UnitId uid : unitIdsCreatingCrossing) {
const Unit *unit = gameState->units()->Get(uid);
const auto &battalionType = battalionTypeGetter(unit->battalion().type());
Coords location = unit->location();
int thisDistance;
if (location.row() < 0) thisDistance = 1000;
else {
const auto *apd = apdCache->GetRaw(gameState->hex_map(), mapId, battalionType, false);
thisDistance = MinimumDistance(apd, location, startCrossingFrom);
}
totalScore -= thisDistance * 100.0;
}
// Now a smaller penalty for distance for units that need to cross, except if they block -- then
// a large penalty
for (const UnitId uid : unitIdsRequiringCrossing) {
@@ -358,7 +642,7 @@ auto WaterCrossingScore(
const auto *apd = apdCache->GetRaw(gameState->hex_map(), mapId, battalionType, false);
int thisDistance;
if (location.row() < 0) thisDistance = 1000;
if (location.row() < 0) thisDistance = kMaxWaterCrossingDistance;
else { thisDistance = MinimumDistance(apd, location, startCrossingFrom); }
bool targetBlocks = false;
@@ -5,10 +5,13 @@
#ifndef EAGLE0_SHARDOK_AI_AI_WATER_CROSSING_CALCULATOR_HPP
#define EAGLE0_SHARDOK_AI_AI_WATER_CROSSING_CALCULATOR_HPP
#include <cstddef>
#include "src/main/cpp/net/eagle0/shardok/ai/AICommonTypes.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistancesCache.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/map/LandRegions.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
@@ -35,9 +38,20 @@ static inline void AssertValid(const Coords& c, const HexMap* hexMap) {
const GameStateW& gameState,
PlayerId pid,
const CoordsSet& destinations,
const LandRegions& landRegions,
const APDCache& apdCache,
const BattalionTypeGetter& battalionTypeGetter) -> vector<UnitId>;
// Whether at least requiredUnitCount active, non-undead attacker units can reach every destination
// without crossing water.
[[nodiscard]] auto HasEnoughNonUndeadAttackersWithoutWaterCrossing(
const GameStateW& gameState,
const CoordsSet& destinations,
const LandRegions& landRegions,
const APDCache& apdCache,
const BattalionTypeGetter& battalionTypeGetter,
std::size_t requiredUnitCount) -> bool;
// Units belonging to the player that are capable of creating water crossings
[[nodiscard]] auto UnitIdsToCreateWaterCrossing(
const GameStateW& gameState,
@@ -92,6 +106,7 @@ static inline void AssertValid(const Coords& c, const HexMap* hexMap) {
const GameStateW& gameState,
const CoordsSet& castleCoords,
const CoordsSet& startCrossingFrom,
const LandRegions& landRegions,
const APDCache& apdCache) -> double;
} // namespace shardok
@@ -4,132 +4,12 @@
#include "AIWaterCrossingCommandChooser.hpp"
#include <algorithm>
#include <ranges>
#include "AIMinimumDistanceAndTarget.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/AIWaterCrossingCalculator.hpp"
namespace shardok {
using Unit = net::eagle0::shardok::storage::fb::Unit;
constexpr ScoreValue kNoRequiredCrossingScore = std::numeric_limits<ScoreValue>::max();
[[nodiscard]] auto AIWaterCrossingCommandChooser::WaterCrossingScore(
const BattalionTypeGetter &battalionTypeGetter,
const GameStateW &gameState,
const CoordsSet &castleCoords,
const CoordsSet &startCrossingFrom) const -> ScoreValue {
uint32_t castleClaimCount = 0;
for (const auto *unit : *gameState->units()) {
if (unit->player_id() != playerId) continue;
const auto status = unit->status();
if (status != net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
status != net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT)
continue;
if (!unit->has_attached_hero()) continue;
++castleClaimCount;
}
CoordsSet destinations = castleCoords;
if (castleClaimCount < castleCoords.size()) {
destinations = CoordsSet(gameState->hex_map());
for (const auto *enemyUnit : *gameState->units()) {
if (enemyUnit->player_id() == playerId) continue;
const auto status = enemyUnit->status();
if (status != net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) continue;
AssertValid(enemyUnit->location(), gameState->hex_map());
destinations.Add(enemyUnit->location());
}
}
const auto unitIdsRequiringCrossing = UnitIdsRequiringWaterCrossing(
gameState,
playerId,
castleCoords,
apdCache,
battalionTypeGetter);
if (unitIdsRequiringCrossing.empty()) return kNoRequiredCrossingScore;
const auto unitIdsCreatingCrossing =
UnitIdsToCreateWaterCrossing(gameState, playerId, battalionTypeGetter);
ScoreValue totalScore = 0;
const auto mapId = ActionPointDistancesCache::GetMapId(gameState->hex_map());
if (unitIdsCreatingCrossing.empty()) {
for (const UnitId uid : unitIdsRequiringCrossing) {
const Unit *unit = gameState->units()->Get(uid);
const auto &battalionType = battalionTypeGetter(unit->battalion().type());
const Coords location = unit->location();
const auto *apd = apdCache->GetRaw(gameState->hex_map(), mapId, battalionType, false);
const int thisDistance =
location.row() < 0 ? 1000 : MinimumDistance(apd, location, startCrossingFrom);
totalScore -= thisDistance;
}
return totalScore;
}
// First put a big penalty on the distance for units that can create a crossing
for (const UnitId uid : unitIdsCreatingCrossing) {
const Unit *unit = gameState->units()->Get(uid);
const auto &battalionType = battalionTypeGetter(unit->battalion().type());
Coords location = unit->location();
int thisDistance;
if (location.row() < 0) thisDistance = 1000;
else {
const auto *apd = apdCache->GetRaw(gameState->hex_map(), mapId, battalionType, false);
thisDistance = MinimumDistance(apd, location, startCrossingFrom);
}
totalScore -= thisDistance * 100.0;
}
// Now a smaller penalty for distance for units that need to cross, except if they block -- then
// a large penalty
for (const UnitId uid : unitIdsRequiringCrossing) {
// If this unit ID can also create a crossing, we already handled it
if (std::ranges::contains(unitIdsCreatingCrossing, uid)) continue;
const Unit *unit = gameState->units()->Get(uid);
const auto &battalionType = battalionTypeGetter(unit->battalion().type());
Coords location = unit->location();
const auto *apd = apdCache->GetRaw(gameState->hex_map(), mapId, battalionType, false);
int thisDistance;
if (location.row() < 0) thisDistance = 1000;
else { thisDistance = MinimumDistance(apd, location, startCrossingFrom); }
bool targetBlocks = false;
// If we're not capable of creating a crossing, don't get in the way of somebody that is.
for (const UnitId crossingUid : unitIdsCreatingCrossing) {
const auto *crossingCapableUnit = gameState->units()->Get(crossingUid);
// Don't check for units that aren't yet placed
if (crossingCapableUnit->location().row() < 0) continue;
AssertValid(crossingCapableUnit->location(), gameState->hex_map());
if (thisDistance <
MinimumDistance(apd, crossingCapableUnit->location(), startCrossingFrom)) {
targetBlocks = true;
break;
}
}
if (targetBlocks) continue;
totalScore -= thisDistance;
}
return totalScore;
}
auto AIWaterCrossingCommandChooser::StartCrossingFrom(
const BattalionTypeGetter &battalionTypeGetter,
const GameStateW &gameState,
@@ -164,6 +44,7 @@ auto AIWaterCrossingCommandChooser::StartCrossingFrom(
gameState,
playerId,
castleCoords,
landRegions,
apdCache,
battalionTypeGetter);
if (unitIdsRequiringCrossing.empty()) return startCrossingFrom;
@@ -11,6 +11,7 @@
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistancesCache.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/map/LandRegions.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
@@ -18,28 +19,26 @@ namespace shardok {
using GameState = net::eagle0::shardok::storage::fb::GameState;
using Unit = net::eagle0::shardok::storage::fb::Unit;
using ScoreValue = double;
class AIWaterCrossingCommandChooser {
private:
const PlayerId playerId;
const APDCache apdCache;
const LandRegions &landRegions;
public:
AIWaterCrossingCommandChooser(const PlayerId pid, APDCache apdCache)
AIWaterCrossingCommandChooser(
const PlayerId pid,
APDCache apdCache,
const LandRegions &landRegions)
: playerId(pid),
apdCache(std::move(apdCache)) {}
apdCache(std::move(apdCache)),
landRegions(landRegions) {}
[[nodiscard]] auto StartCrossingFrom(
const BattalionTypeGetter &battalionTypeGetter,
const GameStateW &gameState,
const CoordsSet &castleCoords) const -> CoordsSet;
[[nodiscard]] auto WaterCrossingScore(
const BattalionTypeGetter &battalionTypeGetter,
const GameStateW &gameState,
const CoordsSet &castleCoords,
const CoordsSet &startCrossingFrom) const -> ScoreValue;
};
} // namespace shardok
@@ -73,6 +73,7 @@ cc_library(
],
deps = [
":ai_score_utilities",
"//src/main/cpp/net/eagle0/shardok/library/map:land_regions",
"//src/main/cpp/net/eagle0/shardok/library/map:terrain",
"//src/main/cpp/net/eagle0/shardok/library/settings:game_settings",
"//src/main/cpp/net/eagle0/shardok/library/util:hex_map_utils",
@@ -194,6 +195,16 @@ cc_library(
],
)
cc_library(
name = "ai_thread_pool",
srcs = ["AIThreadPool.cpp"],
hdrs = ["AIThreadPool.hpp"],
copts = COPTS,
visibility = [
"//src/test/cpp/net/eagle0/shardok/ai:__pkg__",
],
)
cc_library(
name = "ai_command_evaluator",
srcs = ["AICommandEvaluator.cpp"],
@@ -207,6 +218,7 @@ cc_library(
":ai_command_filter",
":ai_experiment_config",
":ai_strategy",
":ai_thread_pool",
":transposition_table",
"//src/main/cpp/net/eagle0/common:sequence_random_generator",
"//src/main/cpp/net/eagle0/shardok/ai/score:ai_score_calculator_interface",
@@ -306,6 +318,7 @@ cc_library(
"//src/main/cpp/net/eagle0/shardok/library/action_point_distances",
"//src/main/cpp/net/eagle0/shardok/library/action_point_distances:action_point_distances_cache",
"//src/main/cpp/net/eagle0/shardok/library/fb_helpers:hex_map_helpers",
"//src/main/cpp/net/eagle0/shardok/library/map:land_regions",
"//src/main/flatbuffer/net/eagle0/shardok/storage:game_state_cc_fbs",
"//src/main/flatbuffer/net/eagle0/shardok/storage:hex_map_cc_fbs",
],
@@ -323,6 +336,7 @@ cc_library(
"//src/main/cpp/net/eagle0/shardok/library:game_state_w",
"//src/main/cpp/net/eagle0/shardok/library/action_point_distances",
"//src/main/cpp/net/eagle0/shardok/library/action_point_distances:action_point_distances_cache",
"//src/main/cpp/net/eagle0/shardok/library/map:land_regions",
],
)
@@ -404,3 +418,19 @@ cc_library(
"@com_google_protobuf//:protobuf",
],
)
cc_library(
name = "starting_position_recommender",
srcs = ["StartingPositionRecommender.cpp"],
hdrs = ["StartingPositionRecommender.hpp"],
copts = COPTS,
visibility = ["//visibility:public"],
deps = [
":ai_config",
":shardok_ai_client",
"//src/main/cpp/net/eagle0/shardok/library:engine",
"//src/main/cpp/net/eagle0/shardok/library:shardok_c_types",
"//src/main/protobuf/net/eagle0/shardok/api:placement_command_cc_proto",
"//src/main/protobuf/net/eagle0/shardok/common:command_type_cc_proto",
],
)
@@ -292,8 +292,6 @@ auto IterativeDeepeningAI::IterativeSearch(
<< " < 100%\n";
}
// Print TranspositionTable statistics
g_transpositionTable.printStats();
return result;
}
@@ -72,7 +72,7 @@ ShardokAIClient::ShardokAIClient(
aiAlgorithmType(aiAlgorithmType),
scoringCalculatorType(scoringCalculatorType),
alCache(std::make_unique<AttackLocationsCache>(hexMap, settings)),
waterCrossingCommandChooser(playerId, apdCache),
waterCrossingCommandChooser(playerId, apdCache, alCache->GetLandRegions()),
mctsConfig(mctsConfig) {
// Pre-generate the most common cache entries for better performance
const auto mapId = ActionPointDistancesCache::GetMapId(hexMap);
@@ -137,15 +137,20 @@ void CheckCommand(const CommandSPtr &realCommand, const CommandSPtr &guessedComm
auto ShardokAIClient::StandardChooseCommandIndex(
const GameSettingsSPtr &settings,
const GameStateW &guessedState,
const CommandListSPtr &realAvailableCommands) const -> CommandChoiceResults {
const CommandListSPtr &realAvailableCommands,
const std::optional<std::chrono::milliseconds> timeLimit) const -> CommandChoiceResults {
const auto settingsGetter = settings->GetGetter();
const auto guessedEngine = ShardokEngine(settings, guessedState);
const auto guessedCommands = guessedEngine.GetAvailableCommandsForAIPlayer(playerId);
const auto commandCount = guessedCommands->size();
// Calculate time budget based on game situation using new dynamic per-command settings
const auto timeBudget =
auto timeBudget =
CalculateTimeBudget(playerId, settings, guessedState, commandCount, isAllAiBattle);
if (timeLimit.has_value()) {
timeBudget.remainingBudget = *timeLimit;
timeBudget.minDepthRequired = 1;
}
// Configure MCTS based on proximity to enemy
// When far from enemy: use AVERAGING with maxPlayerFlips=0 (single-player lookahead)
@@ -297,6 +302,7 @@ auto ShardokAIClient::StandardChooseCommandIndex(
castleCoords,
maxRounds,
apdCache,
alCache->GetLandRegions(),
battalionTypeGetter)
: AIAttackerStrategySelector::BestAttackerStrategy(
playerId,
@@ -398,7 +404,8 @@ auto ShardokAIClient::StandardChooseCommandIndex(
auto ShardokAIClient::LateRoundAttackerChooseCommandIndex(
const GameSettingsSPtr &settings,
const GameStateW &guessedState,
const CommandListSPtr &realAvailableCommands) const -> CommandChoiceResults {
const CommandListSPtr &realAvailableCommands,
const std::optional<std::chrono::milliseconds> timeLimit) const -> CommandChoiceResults {
if (const auto dismissCommand = std::ranges::find_if(
*realAvailableCommands,
[](const CommandSPtr &cmd) {
@@ -406,7 +413,7 @@ auto ShardokAIClient::LateRoundAttackerChooseCommandIndex(
net::eagle0::shardok::common::DISMISS_UNIT_COMMAND;
});
dismissCommand == realAvailableCommands->end()) {
return StandardChooseCommandIndex(settings, guessedState, realAvailableCommands);
return StandardChooseCommandIndex(settings, guessedState, realAvailableCommands, timeLimit);
} else {
CommandChoiceResults results{};
results.chosenIndex =
@@ -423,14 +430,19 @@ auto ShardokAIClient::LateRoundAttackerChooseCommandIndex(
auto ShardokAIClient::FinalRoundAttackerChooseCommandIndex(
const GameSettingsSPtr &settings,
const GameStateW &guessedState,
const CommandListSPtr &realAvailableCommands) const -> CommandChoiceResults {
const CommandListSPtr &realAvailableCommands,
const std::optional<std::chrono::milliseconds> timeLimit) const -> CommandChoiceResults {
const auto fleeCommand =
std::ranges::find_if(*realAvailableCommands, [](const CommandSPtr &cmd) {
return cmd->GetCommandType() == net::eagle0::shardok::common::FLEE_COMMAND;
});
if (fleeCommand == realAvailableCommands->end()) {
return LateRoundAttackerChooseCommandIndex(settings, guessedState, realAvailableCommands);
return LateRoundAttackerChooseCommandIndex(
settings,
guessedState,
realAvailableCommands,
timeLimit);
}
// Extract values directly from settings for flee decision evaluation
@@ -460,14 +472,15 @@ auto ShardokAIClient::FinalRoundAttackerChooseCommandIndex(
return results;
} else {
// Fight instead of flee
return StandardChooseCommandIndex(settings, guessedState, realAvailableCommands);
return StandardChooseCommandIndex(settings, guessedState, realAvailableCommands, timeLimit);
}
}
auto ShardokAIClient::ChooseCommandIndex(
const GameSettingsSPtr &settings,
const GameStateView &gsv,
const CommandListSPtr &realAvailableCommands) const -> CommandChoiceResults {
const CommandListSPtr &realAvailableCommands,
const std::optional<std::chrono::milliseconds> timeLimit) const -> CommandChoiceResults {
static std::array<std::atomic<int>, net::eagle0::shardok::common::CommandType_MAX + 1>
typeChosenCount;
static std::atomic<int> totalChoices = 0;
@@ -479,13 +492,23 @@ auto ShardokAIClient::ChooseCommandIndex(
if (const int roundsRemaining = RoundsRemaining(settings, gsv);
!isDefender && roundsRemaining <= 1) {
results =
FinalRoundAttackerChooseCommandIndex(settings, guessedState, realAvailableCommands);
results = FinalRoundAttackerChooseCommandIndex(
settings,
guessedState,
realAvailableCommands,
timeLimit);
} else if (!isDefender && roundsRemaining <= 3) {
results =
LateRoundAttackerChooseCommandIndex(settings, guessedState, realAvailableCommands);
results = LateRoundAttackerChooseCommandIndex(
settings,
guessedState,
realAvailableCommands,
timeLimit);
} else {
results = StandardChooseCommandIndex(settings, guessedState, realAvailableCommands);
results = StandardChooseCommandIndex(
settings,
guessedState,
realAvailableCommands,
timeLimit);
}
const auto chosenType = (*realAvailableCommands)[results.chosenIndex]->GetCommandType();
@@ -526,7 +549,7 @@ auto ShardokAIClient::ChooseCommandIndex(const ShardokEngine &engine) const
const auto &settings = engine.GetGameSettings();
const auto &gsv = engine.GetGameStateView(GetPlayerId());
const auto results = ChooseCommandIndex(settings, gsv, availableCommands);
const auto results = ChooseCommandIndex(settings, gsv, availableCommands, std::nullopt);
apdCache->ConsolidateThreadLocalCache_Racy();
return results;
}
@@ -9,6 +9,8 @@
#ifndef EAGLE0_SHARDOK_AI_SHARDOK_AI_CLIENT_HPP
#define EAGLE0_SHARDOK_AI_SHARDOK_AI_CLIENT_HPP
#include <chrono>
#include <optional>
#include <vector>
#include "src/main/cpp/net/eagle0/common/RandomGenerator.hpp"
@@ -56,15 +58,18 @@ private:
[[nodiscard]] auto StandardChooseCommandIndex(
const GameSettingsSPtr& settings,
const GameStateW& guessedState,
const CommandListSPtr& realAvailableCommands) const -> CommandChoiceResults;
const CommandListSPtr& realAvailableCommands,
std::optional<std::chrono::milliseconds> timeLimit) const -> CommandChoiceResults;
[[nodiscard]] auto LateRoundAttackerChooseCommandIndex(
const GameSettingsSPtr& settings,
const GameStateW& guessedState,
const CommandListSPtr& realAvailableCommands) const -> CommandChoiceResults;
const CommandListSPtr& realAvailableCommands,
std::optional<std::chrono::milliseconds> timeLimit) const -> CommandChoiceResults;
[[nodiscard]] auto FinalRoundAttackerChooseCommandIndex(
const GameSettingsSPtr& settings,
const GameStateW& guessedState,
const CommandListSPtr& realAvailableCommands) const -> CommandChoiceResults;
const CommandListSPtr& realAvailableCommands,
std::optional<std::chrono::milliseconds> timeLimit) const -> CommandChoiceResults;
public:
explicit ShardokAIClient(
@@ -87,7 +92,8 @@ public:
[[nodiscard]] auto ChooseCommandIndex(
const GameSettingsSPtr& settings,
const net::eagle0::shardok::api::GameStateView& gsv,
const CommandListSPtr& realAvailableCommands) const -> CommandChoiceResults;
const CommandListSPtr& realAvailableCommands,
std::optional<std::chrono::milliseconds> timeLimit) const -> CommandChoiceResults;
// MCTS configuration methods (only relevant when using MCTS algorithm)
[[nodiscard]] auto GetMCTSConfig() const -> const mcts::MCTSConfig& { return mctsConfig; }
@@ -0,0 +1,174 @@
//
// Created by Codex on 7/14/26.
//
#include "StartingPositionRecommender.hpp"
#include <algorithm>
#include <array>
#include <optional>
#include <ranges>
#include "AIConfig.hpp"
#include "ShardokAIClient.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
#include "src/main/protobuf/net/eagle0/shardok/common/command_type.pb.h"
namespace shardok {
using net::eagle0::shardok::api::PlacementCommand;
using net::eagle0::shardok::common::CommandType;
namespace {
struct TutorialPlacement {
UnitId unitId;
int row;
int column;
};
auto RecommendTutorialStartingPositions(const ShardokEngine& engine, const PlayerId playerId)
-> std::optional<std::vector<PlacementCommand>> {
const auto* units = engine.GetCurrentGameState()->units();
const auto isUnplacedPlayerUnit = [playerId](const auto* unit) {
return unit->player_id() == playerId &&
unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT;
};
if (std::ranges::count_if(*units, isUnplacedPlayerUnit) != 3) return std::nullopt;
const auto longbowmen = std::ranges::find_if(*units, [playerId](const auto* unit) {
return unit->player_id() == playerId &&
unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT &&
unit->battalion().type() ==
net::eagle0::shardok::storage::fb::BattalionTypeId_LONGBOWMEN;
});
const auto sadar = std::ranges::find_if(*units, [playerId](const auto* unit) {
return unit->player_id() == playerId &&
unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT &&
unit->attached_hero().profession_info().profession() ==
net::eagle0::shardok::storage::fb::Profession_CHAMPION;
});
const auto marek =
std::ranges::find_if(*units, [playerId, &units, &longbowmen, &sadar](const auto* unit) {
return unit->player_id() == playerId &&
unit->status() ==
net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT &&
(longbowmen == units->end() ||
unit->unit_id() != (*longbowmen)->unit_id()) &&
(sadar == units->end() || unit->unit_id() != (*sadar)->unit_id());
});
if (longbowmen == units->end() || sadar == units->end() || marek == units->end()) {
return std::nullopt;
}
if ((*longbowmen)->unit_id() == (*sadar)->unit_id()) return std::nullopt;
const std::array desiredPlacements{
TutorialPlacement{(*longbowmen)->unit_id(), 6, 12},
TutorialPlacement{(*sadar)->unit_id(), 8, 11},
TutorialPlacement{(*marek)->unit_id(), 10, 11}};
ShardokEngine simulatedEngine(engine, false);
std::vector<PlacementCommand> recommendations;
recommendations.reserve(desiredPlacements.size());
for (const auto& placement : desiredPlacements) {
const auto commands = simulatedEngine.GetAvailableCommandsForAIPlayer(playerId);
const auto command = std::ranges::find_if(*commands, [&placement](const auto& candidate) {
return candidate->GetActorUnitId() == placement.unitId &&
candidate->GetTargetRow() == placement.row &&
candidate->GetTargetColumn() == placement.column &&
(candidate->GetCommandType() ==
net::eagle0::shardok::common::PLACE_UNIT_COMMAND ||
candidate->GetCommandType() ==
net::eagle0::shardok::common::PLACE_HIDDEN_UNIT_COMMAND);
});
if (command == commands->end()) return std::nullopt;
PlacementCommand recommendation;
recommendation.set_unit_id(placement.unitId);
recommendation.set_row(placement.row);
recommendation.set_column(placement.column);
recommendations.push_back(recommendation);
simulatedEngine.PostCommand(playerId, std::distance(commands->begin(), command));
}
return recommendations;
}
} // namespace
auto RecommendStartingPositions(
const ShardokEngine& engine,
const PlayerId playerId,
const std::chrono::milliseconds timePerChoice) -> std::vector<PlacementCommand> {
if (engine.GetGameStatus()->state() !=
net::eagle0::shardok::storage::fb::GameStatus_::State_SET_UP ||
engine.GetCurrentPlayerId() != playerId) {
return {};
}
const auto playerInfos = engine.GetPlayerInfos();
const auto playerInfo = std::ranges::find_if(playerInfos, [playerId](const auto& info) {
return info.player_id() == playerId;
});
if (playerInfo == playerInfos.end()) return {};
if (engine.IsTutorialBattleEnabled() && playerInfo->is_defender()) {
if (const auto tutorialRecommendations =
RecommendTutorialStartingPositions(engine, playerId)) {
return *tutorialRecommendations;
}
}
ShardokEngine simulatedEngine(engine, false);
mcts::MCTSConfig mctsConfig;
mctsConfig.backpropagationPolicy = mcts::MCTSBackpropagationPolicy::MINIMAX;
mctsConfig.maxPlayerFlips = 1;
mctsConfig.maxSimulationFlips = 1;
mctsConfig.simulationPolicy = mcts::MCTSSimulationPolicy::WEIGHTED_HEURISTIC;
ShardokAIClient ai(
playerId,
playerInfo->is_defender(),
false,
simulatedEngine.GetCurrentGameState()->hex_map(),
simulatedEngine.GetGameSettings()->GetGetter(),
AIAlgorithmType::ITERATIVE_DEEPENING,
ScoringCalculatorType::STANDARD,
mctsConfig);
std::vector<PlacementCommand> recommendations;
while (simulatedEngine.GetGameStatus()->state() ==
net::eagle0::shardok::storage::fb::GameStatus_::State_SET_UP &&
simulatedEngine.GetCurrentPlayerId() == playerId) {
const auto commands = simulatedEngine.GetAvailableCommandsForAIPlayer(playerId);
if (commands->empty()) break;
const auto gameStateView = simulatedEngine.GetGameStateView(playerId);
const auto choice = ai.ChooseCommandIndex(
simulatedEngine.GetGameSettings(),
gameStateView,
commands,
timePerChoice);
const auto& command = (*commands)[choice.chosenIndex];
const auto commandType = command->GetCommandType();
if (commandType == net::eagle0::shardok::common::END_PLAYER_SETUP_COMMAND) break;
if (commandType != net::eagle0::shardok::common::PLACE_UNIT_COMMAND &&
commandType != net::eagle0::shardok::common::PLACE_HIDDEN_UNIT_COMMAND) {
break;
}
PlacementCommand recommendation;
recommendation.set_unit_id(command->GetActorUnitId());
recommendation.set_row(command->GetTargetRow());
recommendation.set_column(command->GetTargetColumn());
recommendations.push_back(recommendation);
simulatedEngine.PostCommand(playerId, static_cast<int64_t>(choice.chosenIndex));
}
return recommendations;
}
} // namespace shardok
@@ -0,0 +1,26 @@
//
// Created by Codex on 7/14/26.
//
#ifndef EAGLE0_SHARDOK_AI_STARTING_POSITION_RECOMMENDER_HPP
#define EAGLE0_SHARDOK_AI_STARTING_POSITION_RECOMMENDER_HPP
#include <chrono>
#include <vector>
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCTypes.h"
#include "src/main/protobuf/net/eagle0/shardok/api/placement_command.pb.h"
namespace shardok {
class ShardokEngine;
[[nodiscard]] auto RecommendStartingPositions(
const ShardokEngine& engine,
PlayerId playerId,
std::chrono::milliseconds timePerChoice = std::chrono::milliseconds(500))
-> std::vector<net::eagle0::shardok::api::PlacementCommand>;
} // namespace shardok
#endif // EAGLE0_SHARDOK_AI_STARTING_POSITION_RECOMMENDER_HPP
@@ -4,8 +4,9 @@
#include "AbstractAIScoreCalculator.hpp"
#include <array>
#include "AIScoreCalculatorSharedUtilities.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/AIScoreUtilities.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/AIStrategy.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/AIUnitScoreCalculator.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/AIWaterCrossingCalculator.hpp"
@@ -41,16 +42,44 @@ auto AbstractAIScoreCalculator::CalculateUnitsScoreComponents(
bool isLateGame = cachedCurrentRound > 18; // Inline IsLateGame for efficiency
// APDCache now has built-in thread-local caching - no need for PreCachedAPDs
ActionPoints braveWaterCost = GetBraveWaterCost();
// Memoization cache for EffectiveDistance calls
EffectiveDistanceCache distanceCache;
// Early return for empty game states
const size_t estimatedUnitCount = cachedUnits->size();
if (estimatedUnitCount == 0) { return UnitsScoreComponents{0.0, 0.0}; }
ActionPoints braveWaterCost = GetBraveWaterCost();
struct ResolvedActionPointDistances {
const ActionPointDistances *notBraving = nullptr;
const ActionPointDistances *braving = nullptr;
};
std::array<ResolvedActionPointDistances, BattalionTypeId::BattalionTypeId_MAX + 1>
resolvedActionPointDistances{};
// A scoring call repeatedly asks for the same map/type APDs. Resolve each pointer once while
// leaving ownership and cross-call caching with ActionPointDistancesCache.
const auto notBravingApdFor = [&](int battalionTypeId, const BattalionTypeSPtr &battalionType) {
auto &resolved = resolvedActionPointDistances[battalionTypeId];
if (resolved.notBraving == nullptr) {
resolved.notBraving = GetApdCache()->GetRaw(cachedHexMap, mapId, battalionType, false);
}
return resolved.notBraving;
};
const auto bravingApdFor = [&](int battalionTypeId, const BattalionTypeSPtr &battalionType) {
if (!battalionType->allowsBraveWater) {
return static_cast<const ActionPointDistances *>(nullptr);
}
auto &resolved = resolvedActionPointDistances[battalionTypeId];
if (resolved.braving == nullptr) {
resolved.braving =
GetApdCache()->GetRaw(cachedHexMap, mapId, battalionType, true, braveWaterCost);
}
return resolved.braving;
};
// Memoization cache for EffectiveDistance calls
EffectiveDistanceCache distanceCache;
std::vector<const Unit *> attackerUnits{};
std::vector<const Unit *> defenderUnits{};
// Pre-allocate vectors based on estimated unit ratios to avoid reallocations
@@ -62,13 +91,24 @@ auto AbstractAIScoreCalculator::CalculateUnitsScoreComponents(
auto occupants = Occupants(*cachedUnits, cachedRowCount, cachedColumnCount);
// StartGame assigns real player IDs densely from zero, matching their player_infos indexes.
// Use that invariant instead of scanning PlayerInfo for every unit or initializing a separate
// lookup table for every evaluated state.
const auto *playerInfos = gameStateRawPtr->player_infos();
if (playerInfos == nullptr) { return UnitsScoreComponents{0.0, 0.0}; }
for (const Unit *unit : *cachedUnits) {
const auto *pi = PlayerInfoForPid(gameState, unit->player_id());
if (pi == nullptr) { continue; }
const PlayerId playerId = unit->player_id();
if (playerId < 0 || static_cast<flatbuffers::uoffset_t>(playerId) >= playerInfos->size()) {
continue;
}
const auto *playerInfo = playerInfos->Get(static_cast<flatbuffers::uoffset_t>(playerId));
if (playerInfo->player_id() != playerId) { continue; }
const bool isDefender = playerInfo->is_defender();
switch (unit->status()) {
case net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT: {
if (pi->is_defender()) {
if (isDefender) {
defenderUnits.push_back(unit);
} else {
attackerUnits.push_back(unit);
@@ -79,7 +119,7 @@ auto AbstractAIScoreCalculator::CalculateUnitsScoreComponents(
double thisScore = unit->has_attached_hero() && unit->attached_hero().is_vip()
? CAPTURED_VIP_SCORE
: CAPTURED_UNIT_SCORE;
if (pi->is_defender()) {
if (isDefender) {
defenderUnitsValue += thisScore;
} else {
attackerUnitsValue += thisScore;
@@ -115,16 +155,8 @@ auto AbstractAIScoreCalculator::CalculateUnitsScoreComponents(
// Cache battalion type reference to avoid shared_ptr atomic operations
const auto &battalionType = GetBattalionType(static_cast<BattalionTypeId>(battTypeId));
// Cache APD lookups - same battalion type is used multiple times below
const auto *notBravingApd =
GetApdCache()->GetRaw(cachedHexMap, mapId, battalionType, false);
const auto *bravingApd = battalionType->allowsBraveWater ? GetApdCache()->GetRaw(
cachedHexMap,
mapId,
battalionType,
true,
braveWaterCost)
: nullptr;
const auto *notBravingApd = notBravingApdFor(battTypeId, battalionType);
const auto *bravingApd = bravingApdFor(battTypeId, battalionType);
const auto &priorityList = std::ranges::find_if(
attackerTargetPriorities,
@@ -174,9 +206,7 @@ auto AbstractAIScoreCalculator::CalculateUnitsScoreComponents(
// Cache battalion type reference to avoid shared_ptr atomic operations
const auto &battalionType = GetBattalionType(static_cast<BattalionTypeId>(battTypeId));
// Cache APD lookups for this defender unit
const auto *defenderNotBravingApd =
GetApdCache()->GetRaw(cachedHexMap, mapId, battalionType, false);
const auto *defenderNotBravingApd = notBravingApdFor(battTypeId, battalionType);
auto dv = UnitValue(
unit,
@@ -211,14 +241,8 @@ auto AbstractAIScoreCalculator::CalculateUnitsScoreComponents(
const DIST_T thisDistance = distanceCache.GetOrCompute(
attackerUnit,
unit->location(),
GetApdCache()->GetRaw(cachedHexMap, mapId, attackerBattalionType, false),
attackerBattalionType->allowsBraveWater ? GetApdCache()->GetRaw(
cachedHexMap,
mapId,
attackerBattalionType,
true,
braveWaterCost)
: nullptr,
notBravingApdFor(attackerBattTypeId, attackerBattalionType),
bravingApdFor(attackerBattTypeId, attackerBattalionType),
cachedHexMap);
if (thisDistance < closestDistanceToEnemy) {
closestDistanceToEnemy = thisDistance;
@@ -241,19 +265,8 @@ auto AbstractAIScoreCalculator::CalculateUnitsScoreComponents(
const DIST_T thisDistance = distanceCache.GetOrCompute(
defenderUnit,
unit->location(),
GetApdCache()->GetRaw(
cachedHexMap,
mapId,
defenderBattalionType,
false),
defenderBattalionType->allowsBraveWater
? GetApdCache()->GetRaw(
cachedHexMap,
mapId,
defenderBattalionType,
true,
braveWaterCost)
: nullptr,
notBravingApdFor(defenderBattTypeId, defenderBattalionType),
bravingApdFor(defenderBattTypeId, defenderBattalionType),
cachedHexMap);
if (thisDistance < closestDistanceToFriendly) {
closestDistanceToFriendly = thisDistance;
@@ -306,6 +319,7 @@ auto AbstractAIScoreCalculator::CalculateAttackerVictoryConditionScore(
gameState,
castleCoords,
attackerStrategy.targetLocations,
GetAlCache()->GetLandRegions(),
GetApdCache());
break;
@@ -15,6 +15,7 @@ cc_library(
":game_update_receiver",
"//src/main/cpp/net/eagle0/common:shardok_latency_trace",
"//src/main/cpp/net/eagle0/shardok/ai:shardok_ai_client",
"//src/main/cpp/net/eagle0/shardok/ai:starting_position_recommender",
"//src/main/cpp/net/eagle0/shardok/library:engine",
"//src/main/cpp/net/eagle0/shardok/library:shardok_c_types",
"//src/main/cpp/net/eagle0/shardok/library:unit_placement_info",
@@ -20,10 +20,37 @@
#include "src/main/cpp/net/eagle0/common/ShardokLatencyTrace.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/ShardokAIClient.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/StartingPositionRecommender.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
#include "src/main/protobuf/net/eagle0/shardok/api/game_state_view.pb.h"
namespace shardok {
auto PlayerBattleProgressFrom(const net::eagle0::shardok::storage::fb::GameState &gameState)
-> std::vector<PlayerBattleProgress> {
std::vector<PlayerBattleProgress> progress;
progress.reserve(gameState.player_infos()->size());
for (const auto *player : *gameState.player_infos()) {
int32_t troopCount = 0;
bool isRetreating = false;
for (const auto *unit : *gameState.units()) {
if (unit->player_id() != player->player_id()) continue;
switch (unit->status()) {
case net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT:
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT:
case net::eagle0::shardok::storage::fb::UnitStatus_NEVER_ENTERED_UNIT:
troopCount += unit->battalion().size();
break;
case net::eagle0::shardok::storage::fb::UnitStatus_FLED_UNIT:
isRetreating = true;
break;
default: break;
}
}
progress.push_back(PlayerBattleProgress{player->player_id(), troopCount, isRetreating});
}
return progress;
}
using net::eagle0::common::ScopedShardokLatencyTrace;
using net::eagle0::shardok::common::GameStatus;
@@ -237,7 +264,7 @@ void ShardokGameController::DoAIThread() {
{{"player_id", std::to_string(playerId)},
{"round", std::to_string(gsv.current_round())},
{"available_command_count", std::to_string(availableCommands->size())}});
return aiClient->ChooseCommandIndex(settings, gsv, availableCommands);
return aiClient->ChooseCommandIndex(settings, gsv, availableCommands, std::nullopt);
}();
chosenIndex = results.chosenIndex;
@@ -455,6 +482,10 @@ auto ShardokGameController::GetUpdates(const int64_t startingActionId) -> AllUpd
cachedGameId,
{{"starting_action_id", std::to_string(startingActionId)}});
AllUpdates updates{};
std::unique_ptr<ShardokEngine> recommendationEngine;
std::shared_ptr<AvailableCommands> recommendationCommands;
PlayerId recommendationPlayerId = -1;
std::pair<int64_t, PlayerId> recommendationCacheKey;
incomingRegistrations++;
std::vector<net::eagle0::shardok::storage::ShardokActionWithResultingState> awrs;
@@ -527,6 +558,15 @@ auto ShardokGameController::GetUpdates(const int64_t startingActionId) -> AllUpd
*acs->mutable_preview_command() = {
std::begin(previewCommands),
std::end(previewCommands)};
if (engine->GetGameStatus()->state() ==
net::eagle0::shardok::storage::fb::GameStatus_::State_SET_UP &&
engine->GetCurrentPlayerId() == pi.player_id() && !availableCommands.empty()) {
recommendationEngine = std::make_unique<ShardokEngine>(*engine, false);
recommendationCommands = acs;
recommendationPlayerId = pi.player_id();
recommendationCacheKey = {updates.newUnfilteredCount, recommendationPlayerId};
}
}
updates.filteredResults.emplace_back(fid, actionResultViews, acs);
@@ -574,22 +614,26 @@ auto ShardokGameController::GetUpdates(const int64_t startingActionId) -> AllUpd
// Extract battle progress data from the FlatBuffer game state
const auto *gs = engine->GetCurrentGameState().Get();
updates.currentRound = gs->current_round();
updates.playerTroopCounts.reserve(gs->player_infos()->size());
for (const auto *player : *gs->player_infos()) {
int32_t troopCount = 0;
for (const auto *unit : *gs->units()) {
if (unit->player_id() != player->player_id()) continue;
switch (unit->status()) {
case net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT:
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT:
case net::eagle0::shardok::storage::fb::UnitStatus_NEVER_ENTERED_UNIT:
troopCount += unit->battalion().size();
break;
default: break;
}
}
updates.playerTroopCounts.emplace_back(player->player_id(), troopCount);
updates.playerBattleProgress = PlayerBattleProgressFrom(*gs);
}
if (recommendationEngine) {
std::scoped_lock<std::mutex> recommendationGuard(recommendationLock);
auto cached = recommendedStartingPositionsCache.find(recommendationCacheKey);
if (cached == recommendedStartingPositionsCache.end()) {
cached = recommendedStartingPositionsCache
.emplace(
recommendationCacheKey,
RecommendStartingPositions(
*recommendationEngine,
recommendationPlayerId))
.first;
}
recommendationCommands->mutable_recommended_starting_positions()->Reserve(
cached->second.size());
*recommendationCommands->mutable_recommended_starting_positions() = {
cached->second.begin(),
cached->second.end()};
}
return updates;
@@ -691,7 +735,7 @@ auto ShardokGameController::WaitForUpdatesAndPush(
updates.newUnfilteredCount,
updates.currentGameState,
updates.currentRound,
updates.playerTroopCounts);
updates.playerBattleProgress);
}
// Tutorial battle reset: defender lost, signal reset instead of game over
@@ -13,6 +13,7 @@
#include <condition_variable>
#include <ctime>
#include <functional>
#include <map>
#include <memory>
#include <mutex>
#include <optional>
@@ -27,6 +28,7 @@
#include "src/main/cpp/net/eagle0/shardok/library/UnitPlacementInfo.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/GameStateHelpers.hpp"
#include "src/main/protobuf/net/eagle0/shardok/api/game_state_view.pb.h"
#include "src/main/protobuf/net/eagle0/shardok/api/placement_command.pb.h"
namespace shardok {
using net::eagle0::shardok::api::ActionResultView;
@@ -57,6 +59,16 @@ struct OnePlayerUpdates {
availableCommands(acs) {}
};
struct PlayerBattleProgress {
int32_t playerId;
int32_t troopCount;
bool isRetreating;
};
[[nodiscard]] auto PlayerBattleProgressFrom(
const net::eagle0::shardok::storage::fb::GameState& gameState)
-> std::vector<PlayerBattleProgress>;
/// Result of WaitForUpdatesAndPush indicating how the wait ended
enum class WaitResult {
GAME_OVER, // Game ended normally
@@ -76,7 +88,7 @@ public:
int32_t newUnfilteredCount,
const byte_vector& currentGameState,
int32_t currentRound,
const std::vector<std::pair<int32_t, int32_t>>& playerTroopCounts) = 0;
const std::vector<PlayerBattleProgress>& playerBattleProgress) = 0;
/// Called when the game ends
virtual void OnGameOver(const GameOverInfo& info) = 0;
@@ -121,6 +133,10 @@ private:
std::vector<std::shared_ptr<ShardokAIClient>> aiClients;
std::thread aiThread;
std::mutex recommendationLock{};
std::map<std::pair<int64_t, PlayerId>, std::vector<net::eagle0::shardok::api::PlacementCommand>>
recommendedStartingPositionsCache;
// Each entry maps an Eagle faction id of a non-participant watcher to the
// Shardok pids of participants whose allies list named this watcher. Used
// when generating per-watcher filtered views in GetUpdates.
@@ -183,7 +199,7 @@ public:
int32_t newUnfilteredCount;
byte_vector currentGameState;
int32_t currentRound = 0;
std::vector<std::pair<int32_t, int32_t>> playerTroopCounts; // (playerId, troopCount)
std::vector<PlayerBattleProgress> playerBattleProgress;
};
auto GetUpdates(int64_t startingActionId) -> AllUpdates;
auto GetCurrentGameStateBytes() -> byte_vector;
@@ -52,11 +52,22 @@ private:
const std::vector<std::shared_ptr<const CommandFactory>> commandFactories;
const PlayerSetupCommandFactory playerSetupCommandFactory;
struct PlayerCommandContext {
std::vector<PlayerId> allyPids;
PossibleChargees possibleChargees;
bool isAttacker;
bool cannotBecomeOutlaw;
};
static auto MakePlayerCommandContext(const GameStateW &gameState, PlayerId playerId)
-> PlayerCommandContext;
// AvailableCommands helpers for a single unit
void AddAvailableCommandsForOneUnit(
const GameStateW &gameState,
CommandList &commands,
const Unit *unit,
const PlayerCommandContext &playerContext,
bool onlyFollowUps) const;
public:
@@ -95,30 +106,39 @@ auto AvailableCommandsFactoryImpl::GetPlayerSetupCommands(
gameState,
reinforcementPositions);
return std::make_shared<CommandList>(previewCommands);
return std::make_shared<CommandList>(std::move(previewCommands));
}
auto AvailableCommandsFactoryImpl::MakePlayerCommandContext(
const GameStateW &gameState,
const PlayerId playerId) -> PlayerCommandContext {
PossibleChargees possibleChargees{};
possibleChargees.reserve(gameState->possible_chargee_ids()->size());
for (const UnitId chargeeId : *gameState->possible_chargee_ids()) {
if (chargeeId != -1) { possibleChargees.push_back(chargeeId); }
}
return PlayerCommandContext{
.allyPids = AlliedPids(gameState, playerId),
.possibleChargees = std::move(possibleChargees),
.isAttacker = IsAttacker(gameState, playerId),
.cannotBecomeOutlaw = CannotBecomeOutlaw(gameState, playerId)};
}
void AvailableCommandsFactoryImpl::AddAvailableCommandsForOneUnit(
const GameStateW &gameState,
CommandList &commands,
const Unit *unit,
const PlayerCommandContext &playerContext,
const bool onlyFollowUps) const {
const auto &battType = settings.GetBattalionType(unit->battalion().type());
const bool hasHero = unit->has_attached_hero();
const auto allyPids = AlliedPids(gameState, unit->player_id());
const bool isAttacker = IsAttacker(gameState, unit->player_id());
const bool cannotBecomeOutlaw = CannotBecomeOutlaw(gameState, unit->player_id());
const bool unitMovedIntoZoc = unit->has_moved_in_zoc();
const ActionPoints remainingActionPoints = unit->remaining_action_points();
const Coords &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
@@ -12,6 +12,7 @@
#include <optional>
#include <ranges>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
@@ -566,9 +567,81 @@ void ShardokEngine::PostCommand(
const CommandSPtr command = (*cachedAvailableCommands)[commandIndex];
cachedAvailableCommands = nullptr;
if (MaybeInterceptTutorialEndTurn(command)) { return; }
PostActionUnchecked(command, randomGenerator, roll);
}
auto ShardokEngine::MaybeInterceptTutorialEndTurn(const CommandSPtr &command) -> bool {
using net::eagle0::shardok::common::ActionType;
using net::eagle0::shardok::common::CommandType;
if (command->GetCommandType() != CommandType::END_TURN_COMMAND ||
!tutorialController_.IsEnabled()) {
return false;
}
const auto configuredHeroIds = tutorialController_.EndTurnWarningEagleHeroIds();
if (configuredHeroIds.empty()) { return false; }
std::unordered_set<int32_t> warnedHeroIds;
for (const auto &entry : actionHistory) {
const auto &result = entry.action_result();
if (result.type() != ActionType::TUTORIAL_UNUSED_HERO_WARNING) { continue; }
warnedHeroIds.insert(
result.tutorial_warning_eagle_hero_ids().begin(),
result.tutorial_warning_eagle_hero_ids().end());
}
std::unordered_set<UnitId> actedUnitIds;
for (auto entry = actionHistory.rbegin(); entry != actionHistory.rend(); ++entry) {
const auto &result = entry->action_result();
if (result.type() == ActionType::PLAYER_TURN_START && result.has_next_player() &&
result.next_player().value() == command->GetPlayerId()) {
break;
}
if (result.has_actor()) { actedUnitIds.insert(result.actor().value()); }
}
const auto availableCommands = availableCommandsFactory->GetAvailableCommands(
gameState,
command->GetPlayerId(),
/* includeFollowUps=*/true);
std::unordered_set<UnitId> unitsAbleToAct;
for (const auto &availableCommand : *availableCommands) {
const auto proto = availableCommand->GetCommandProto();
if (proto.has_actor()) { unitsAbleToAct.insert(proto.actor().value()); }
}
const std::unordered_set<int32_t> configuredHeroIdSet(
configuredHeroIds.begin(),
configuredHeroIds.end());
std::vector<int32_t> newlyWarnedHeroIds;
for (const auto *unit : *gameState->units()) {
if (unit->player_id() != command->GetPlayerId() || !unit->has_attached_hero() ||
unit->status() != net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT ||
actedUnitIds.contains(unit->unit_id()) || !unitsAbleToAct.contains(unit->unit_id())) {
continue;
}
const auto eagleHeroId = unit->attached_hero().eagle_hero_id();
if (configuredHeroIdSet.contains(eagleHeroId) && !warnedHeroIds.contains(eagleHeroId)) {
newlyWarnedHeroIds.push_back(eagleHeroId);
}
}
if (newlyWarnedHeroIds.empty()) { return false; }
ActionResultProto warningResult;
warningResult.set_type(ActionType::TUTORIAL_UNUSED_HERO_WARNING);
warningResult.mutable_player()->set_value(command->GetPlayerId());
warningResult.mutable_tutorial_warning_eagle_hero_ids()->Add(
newlyWarnedHeroIds.begin(),
newlyWarnedHeroIds.end());
ApplyAndAddActionResult(warningResult);
return true;
}
void ShardokEngine::PostActionUnchecked(
const CommandSPtr &command,
const std::shared_ptr<RandomGenerator> &randomGenerator,
@@ -82,6 +82,8 @@ private:
const std::shared_ptr<RandomGenerator> &randomGenerator,
std::optional<int32_t> roll);
[[nodiscard]] auto MaybeInterceptTutorialEndTurn(const CommandSPtr &command) -> bool;
public:
// Constructor using existing game state
ShardokEngine(
@@ -57,7 +57,7 @@ auto shardok::MoveCommand::InternalExecute(
const auto* destinationTerrain = GetTerrain(map, destination);
bool startedInEnemyZoc = enemyZocCoords.Contains(origin);
vector<const Unit*> knownAdjacentEnemies =
KnownAdjacentEnemies(player, allUnits, map, allyPids, origin);
KnownAdjacentEnemies(player, currentState, allyPids, origin);
MutatingSpendActionPoints(
&mover,
@@ -132,7 +132,7 @@ auto shardok::MoveCommand::InternalExecute(
if (!startedInEnemyZoc) {
vector<const Unit*> newAdjacentEnemies =
KnownAdjacentEnemies(player, allUnits, map, allyPids, destination);
KnownAdjacentEnemies(player, currentState, allyPids, destination);
for (const Unit* possibleChargee : newAdjacentEnemies) {
if (std::ranges::find_if(
@@ -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
@@ -56,6 +56,11 @@ public:
/// Returns true if the controller is enabled for this battle.
[[nodiscard]] auto IsEnabled() const -> bool { return enabled_; }
[[nodiscard]] auto EndTurnWarningEagleHeroIds() const -> std::vector<int32_t> {
return {config_.end_turn_warning_eagle_hero_ids().begin(),
config_.end_turn_warning_eagle_hero_ids().end()};
}
/// Check all events at end of round, return actions for triggered events.
/// Events fire in config order - first triggered event executes, then next.
/// Each event fires at most once.
@@ -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;
@@ -131,6 +131,12 @@ auto TargetIsHiddenFromOpponents(const ActionResultType type) -> bool {
resultView.clear_target_unit();
}
if (actorIsSelfOrAlly) {
resultView.mutable_tutorial_warning_eagle_hero_ids()->Add(
result.tutorial_warning_eagle_hero_ids().begin(),
result.tutorial_warning_eagle_hero_ids().end());
}
if (result.has_target_coords() &&
(actorIsSelfOrAlly || !TargetIsHiddenFromOpponents(result.type()))) {
*resultView.mutable_target_coords() = result.target_coords();
@@ -590,7 +590,10 @@ auto GuessedHero(const HeroViewProto &hv) -> net::eagle0::shardok::storage::fb::
hero.mutate_bravery(stats.bravery());
hero.mutate_vigor(stats.vigor());
hero.mutate_starting_vigor(stats.starting_vigor());
// The battle-start recovery ceiling is internal state and is not part of HeroView.
// Use the visible current vigor as a conservative ceiling rather than inventing
// recovery capacity that the player cannot observe.
hero.mutate_starting_vigor(stats.vigor());
hero.mutate_spent_vigor(kGuessedHeroStat);
} else {
// Don't believe it's destroyed
@@ -67,8 +67,6 @@ using net::eagle0::shardok::api::HeroView;
stats->set_vigor(hero.vigor());
if (belongsToAsker) {
stats->set_starting_vigor(hero.vigor());
HeroView::StatXp *statXp = filtered.mutable_stat_xp();
statXp->set_strength_xp(hero.strength_xp());
statXp->set_agility_xp(hero.agility_xp());
@@ -80,4 +78,4 @@ using net::eagle0::shardok::api::HeroView;
return filtered;
}
} // namespace shardok
} // namespace shardok
@@ -1,6 +1,17 @@
load("@rules_cc//cc:defs.bzl", "cc_library")
load("//tools:copts.bzl", "COPTS")
cc_library(
name = "cpu_profiler",
srcs = ["CpuProfiler.cpp"],
hdrs = ["CpuProfiler.hpp"],
copts = COPTS,
visibility = [
"//src/main/cpp/net/eagle0/shardok:__subpackages__",
"//src/test/cpp/net/eagle0/shardok:__subpackages__",
],
)
cc_library(
name = "games_manager",
srcs = ["ShardokGamesManager.cpp"],
@@ -75,6 +86,7 @@ cc_library(
copts = COPTS,
visibility = ["//src/main/cpp/net/eagle0/shardok:__subpackages__"],
deps = [
":cpu_profiler",
":game_over_response_populator",
":games_manager",
":token_auth",
@@ -0,0 +1,261 @@
#include "CpuProfiler.hpp"
#include <fcntl.h>
#include <signal.h>
#include <spawn.h>
#include <sys/wait.h>
#include <unistd.h>
#include <algorithm>
#include <cerrno>
#include <chrono>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <system_error>
#include <thread>
#include <utility>
#include <vector>
extern char** environ;
namespace shardok {
namespace {
auto EpochMillisNow() -> int64_t {
return std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch())
.count();
}
auto FileSize(const std::string& path) -> int64_t {
std::error_code error;
const auto size = std::filesystem::file_size(path, error);
if (error) { return 0; }
return static_cast<int64_t>(size);
}
auto ReadFile(const std::string& path) -> std::string {
std::ifstream input(path);
if (!input.is_open()) { return {}; }
std::ostringstream contents;
contents << input.rdbuf();
return contents.str();
}
auto PerfExecutable() -> std::string {
if (const char* configured = std::getenv("SHARDOK_PERF_EXECUTABLE");
configured != nullptr && *configured != '\0') {
return configured;
}
return "perf";
}
class PerfCpuProfileProcess final : public CpuProfileProcess {
private:
pid_t pid_ = -1;
std::string errorPath_;
void Reap() {
if (pid_ <= 0) { return; }
int status = 0;
while (waitpid(pid_, &status, 0) < 0 && errno == EINTR) {}
pid_ = -1;
}
public:
auto Start(const std::string& profilePath, const int32_t sampleFrequencyHz)
-> std::optional<std::string> override {
errorPath_ = profilePath + ".stderr";
std::error_code removeError;
std::filesystem::remove(errorPath_, removeError);
const std::string executable = PerfExecutable();
std::vector<std::string> arguments{
executable,
"record",
"--output",
profilePath,
"--freq",
std::to_string(sampleFrequencyHz),
"--event",
"cpu-clock:u",
"--inherit",
"--call-graph",
"dwarf,8192",
"--pid",
std::to_string(getpid()),
};
std::vector<char*> argumentPointers;
argumentPointers.reserve(arguments.size() + 1);
for (auto& argument : arguments) { argumentPointers.push_back(argument.data()); }
argumentPointers.push_back(nullptr);
posix_spawn_file_actions_t fileActions;
int result = posix_spawn_file_actions_init(&fileActions);
if (result != 0) {
return "Unable to initialize perf output capture: " + std::string(strerror(result));
}
result = posix_spawn_file_actions_addopen(
&fileActions,
STDERR_FILENO,
errorPath_.c_str(),
O_WRONLY | O_CREAT | O_TRUNC,
0600);
if (result == 0) {
result = posix_spawnp(
&pid_,
executable.c_str(),
&fileActions,
nullptr,
argumentPointers.data(),
environ);
}
posix_spawn_file_actions_destroy(&fileActions);
if (result != 0) {
pid_ = -1;
return "Unable to start perf: " + std::string(strerror(result));
}
// Permission and unsupported-event failures happen immediately. Give perf enough time to
// report them so the start RPC returns a useful error instead of a false recording state.
std::this_thread::sleep_for(std::chrono::milliseconds(100));
int status = 0;
const pid_t waitResult = waitpid(pid_, &status, WNOHANG);
if (waitResult == pid_) {
pid_ = -1;
std::string detail = ReadFile(errorPath_);
if (detail.empty()) { detail = "perf exited before recording began"; }
return "perf failed to start the Shardok CPU profile: " + detail;
}
if (waitResult < 0) {
const std::string error = strerror(errno);
pid_ = -1;
return "Unable to observe perf after startup: " + error;
}
return std::nullopt;
}
void Stop() override {
if (pid_ <= 0) { return; }
if (kill(pid_, SIGINT) < 0 && errno != ESRCH) {
// The child belongs to this process, so this is only a last-resort cleanup path.
(void)kill(pid_, SIGKILL);
}
Reap();
}
auto IsRunning() -> bool override {
if (pid_ <= 0) { return false; }
int status = 0;
pid_t waitResult = 0;
do {
waitResult = waitpid(pid_, &status, WNOHANG);
} while (waitResult < 0 && errno == EINTR);
if (waitResult == 0) { return true; }
pid_ = -1;
return false;
}
~PerfCpuProfileProcess() override { Stop(); }
};
auto DefaultProcessFactory() -> std::unique_ptr<CpuProfileProcess> {
return std::make_unique<PerfCpuProfileProcess>();
}
} // namespace
CpuProfiler::CpuProfiler(
std::string profilePath,
std::string executablePath,
const std::chrono::milliseconds maxDuration,
const int64_t maxProfileSizeBytes,
CpuProfileProcessFactory processFactory)
: state_(std::make_shared<State>()),
executablePath_(std::move(executablePath)) {
state_->profilePath = std::move(profilePath);
state_->maxDuration = maxDuration;
state_->maxProfileSizeBytes = maxProfileSizeBytes;
state_->processFactory = processFactory ? std::move(processFactory)
: CpuProfileProcessFactory(DefaultProcessFactory);
}
CpuProfiler::~CpuProfiler() { Stop(); }
auto CpuProfiler::Start() -> std::optional<std::string> {
auto state = state_;
std::lock_guard lock(state->mutex);
if (state->recording) { return "A Shardok CPU profile is already recording"; }
std::error_code removeError;
std::filesystem::remove(state->profilePath, removeError);
if (removeError) {
return "Unable to remove the previous Shardok CPU profile: " + removeError.message();
}
state->process = state->processFactory();
if (const auto error = state->process->Start(state->profilePath, kSampleFrequencyHz)) {
state->process.reset();
return error;
}
state->recording = true;
state->startedAtEpochMillis = EpochMillisNow();
const uint64_t generation = ++state->generation;
const auto deadline = std::chrono::steady_clock::now() + state->maxDuration;
const auto monitorInterval = std::min(std::chrono::milliseconds(1000), state->maxDuration);
std::thread([state = std::move(state), generation, deadline, monitorInterval]() {
while (true) {
std::this_thread::sleep_for(monitorInterval);
std::lock_guard monitorLock(state->mutex);
if (!state->recording || state->generation != generation) { return; }
if (!state->process->IsRunning()) {
state->recording = false;
state->process.reset();
return;
}
if (std::chrono::steady_clock::now() >= deadline ||
FileSize(state->profilePath) >= state->maxProfileSizeBytes) {
CpuProfiler::StopLocked(*state);
return;
}
}
}).detach();
return std::nullopt;
}
void CpuProfiler::StopLocked(State& state) {
if (!state.recording) { return; }
state.process->Stop();
state.process.reset();
state.recording = false;
}
void CpuProfiler::Stop() {
std::lock_guard lock(state_->mutex);
StopLocked(*state_);
}
auto CpuProfiler::Snapshot() const -> CpuProfileSnapshot {
std::lock_guard lock(state_->mutex);
const int64_t profileSize = FileSize(state_->profilePath);
return CpuProfileSnapshot{
.recording = state_->recording,
.profileAvailable = !state_->recording && profileSize > 0,
.startedAtEpochMillis = state_->startedAtEpochMillis,
.profileSizeBytes = profileSize,
};
}
auto CpuProfiler::ProfilePath() const -> const std::string& { return state_->profilePath; }
auto CpuProfiler::ExecutablePath() const -> const std::string& { return executablePath_; }
} // namespace shardok
@@ -0,0 +1,77 @@
#ifndef EAGLE0_SHARDOK_SERVER_CPU_PROFILER_HPP
#define EAGLE0_SHARDOK_SERVER_CPU_PROFILER_HPP
#include <chrono>
#include <cstdint>
#include <functional>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
namespace shardok {
class CpuProfileProcess {
public:
virtual ~CpuProfileProcess() = default;
virtual auto Start(const std::string& profilePath, int32_t sampleFrequencyHz)
-> std::optional<std::string> = 0;
virtual void Stop() = 0;
[[nodiscard]] virtual auto IsRunning() -> bool = 0;
};
using CpuProfileProcessFactory = std::function<std::unique_ptr<CpuProfileProcess>()>;
struct CpuProfileSnapshot {
bool recording;
bool profileAvailable;
int64_t startedAtEpochMillis;
int64_t profileSizeBytes;
};
class CpuProfiler {
private:
struct State {
std::mutex mutex;
bool recording = false;
int64_t startedAtEpochMillis = 0;
uint64_t generation = 0;
std::string profilePath;
std::chrono::milliseconds maxDuration;
int64_t maxProfileSizeBytes;
CpuProfileProcessFactory processFactory;
std::unique_ptr<CpuProfileProcess> process;
};
std::shared_ptr<State> state_;
std::string executablePath_;
static void StopLocked(State& state);
public:
static constexpr int32_t kSampleFrequencyHz = 99;
static constexpr std::chrono::milliseconds kMaxDuration = std::chrono::hours(1);
static constexpr int64_t kMaxProfileSizeBytes = 500 * 1024 * 1024;
explicit CpuProfiler(
std::string profilePath = "/tmp/shardok-admin.data",
std::string executablePath = "/proc/self/exe",
std::chrono::milliseconds maxDuration = kMaxDuration,
int64_t maxProfileSizeBytes = kMaxProfileSizeBytes,
CpuProfileProcessFactory processFactory = {});
~CpuProfiler();
CpuProfiler(const CpuProfiler&) = delete;
auto operator=(const CpuProfiler&) -> CpuProfiler& = delete;
auto Start() -> std::optional<std::string>;
void Stop();
[[nodiscard]] auto Snapshot() const -> CpuProfileSnapshot;
[[nodiscard]] auto ProfilePath() const -> const std::string&;
[[nodiscard]] auto ExecutablePath() const -> const std::string&;
};
} // namespace shardok
#endif // EAGLE0_SHARDOK_SERVER_CPU_PROFILER_HPP
@@ -9,11 +9,15 @@
#include "EagleInterfaceGrpcServer.hpp"
#include <algorithm>
#include <array>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <iterator>
#include <memory>
#include <ranges>
#include <string>
#include <string_view>
#include <thread>
#include <utility>
@@ -29,6 +33,10 @@
namespace shardok {
using grpc::Status;
using grpc::StatusCode;
using net::eagle0::common::CPU_PROFILE_ARTIFACT_TYPE_C_LIBRARY;
using net::eagle0::common::CPU_PROFILE_ARTIFACT_TYPE_DYNAMIC_LINKER;
using net::eagle0::common::CPU_PROFILE_ARTIFACT_TYPE_EXECUTABLE;
using net::eagle0::common::CPU_PROFILE_ARTIFACT_TYPE_PROFILE;
using net::eagle0::common::GameSetupInfo;
using net::eagle0::common::ScopedShardokLatencyTrace;
using net::eagle0::common::ShardokLatencyFields;
@@ -36,6 +44,28 @@ using net::eagle0::common::VictoryCondition;
using net::eagle0::shardok::api::PlacementCommand;
using net::eagle0::shardok::common::EndGameCondition;
namespace {
auto FindMappedRuntimeFile(
const std::string_view exactFilename,
const std::string_view filenamePrefix = {}) -> std::string {
std::ifstream maps("/proc/self/maps");
for (std::string line; std::getline(maps, line);) {
const auto pathStart = line.find('/');
if (pathStart == std::string::npos) { continue; }
auto path = line.substr(pathStart);
const auto filename = std::filesystem::path(path).filename().string();
if (filename == exactFilename ||
(!filenamePrefix.empty() && filename.starts_with(filenamePrefix))) {
return path;
}
}
return {};
}
} // namespace
class NewGameException : public std::exception {
private:
Status status;
@@ -46,6 +76,16 @@ public:
auto GetStatus() const -> Status { return status; }
};
void PopulateCpuProfileStatus(
const CpuProfileSnapshot &snapshot,
CpuProfileStatusResponse *response) {
response->set_recording(snapshot.recording);
response->set_profile_available(snapshot.profileAvailable);
response->set_started_at_epoch_millis(snapshot.startedAtEpochMillis);
response->set_profile_size_bytes(snapshot.profileSizeBytes);
response->set_sample_frequency_hz(CpuProfiler::kSampleFrequencyHz);
}
auto EagleInterfaceImpl::ControllerForGame(const GameId &gameId, const GameSetupInfo &setupInfo)
-> std::shared_ptr<ShardokGameController> {
ScopedShardokLatencyTrace trace(
@@ -96,6 +136,89 @@ EagleInterfaceImpl::EagleInterfaceImpl(
}
}
auto EagleInterfaceImpl::GetCpuProfileStatus(
ServerContext *context,
const CpuProfileStatusRequest * /*request*/,
CpuProfileStatusResponse *response) -> Status {
if (auto authStatus = tokenValidator_.ValidateOrStatus(context)) { return *authStatus; }
PopulateCpuProfileStatus(cpuProfiler_.Snapshot(), response);
return Status::OK;
}
auto EagleInterfaceImpl::StartCpuProfile(
ServerContext *context,
const StartCpuProfileRequest * /*request*/,
CpuProfileStatusResponse *response) -> Status {
if (auto authStatus = tokenValidator_.ValidateOrStatus(context)) { return *authStatus; }
if (const auto error = cpuProfiler_.Start()) {
return Status(StatusCode::FAILED_PRECONDITION, *error);
}
PopulateCpuProfileStatus(cpuProfiler_.Snapshot(), response);
return Status::OK;
}
auto EagleInterfaceImpl::StopCpuProfile(
ServerContext *context,
const StopCpuProfileRequest * /*request*/,
CpuProfileStatusResponse *response) -> Status {
if (auto authStatus = tokenValidator_.ValidateOrStatus(context)) { return *authStatus; }
cpuProfiler_.Stop();
PopulateCpuProfileStatus(cpuProfiler_.Snapshot(), response);
return Status::OK;
}
auto EagleInterfaceImpl::DownloadCpuProfile(
ServerContext *context,
const CpuProfileDownloadRequest *request,
grpc::ServerWriter<CpuProfileChunk> *writer) -> Status {
if (auto authStatus = tokenValidator_.ValidateOrStatus(context)) { return *authStatus; }
std::string path;
switch (request->artifact_type()) {
case CPU_PROFILE_ARTIFACT_TYPE_PROFILE:
if (!cpuProfiler_.Snapshot().profileAvailable) {
return Status(
StatusCode::FAILED_PRECONDITION,
"No Shardok CPU profile is available");
}
path = cpuProfiler_.ProfilePath();
break;
case CPU_PROFILE_ARTIFACT_TYPE_EXECUTABLE: path = cpuProfiler_.ExecutablePath(); break;
case CPU_PROFILE_ARTIFACT_TYPE_C_LIBRARY: path = FindMappedRuntimeFile("libc.so.6"); break;
case CPU_PROFILE_ARTIFACT_TYPE_DYNAMIC_LINKER:
path = FindMappedRuntimeFile("", "ld-linux-");
break;
default: return Status(StatusCode::INVALID_ARGUMENT, "Unknown CPU profile artifact type");
}
if (path.empty()) {
return Status(StatusCode::NOT_FOUND, "CPU profile runtime artifact is not mapped");
}
std::ifstream input(path, std::ios::binary);
if (!input.is_open()) {
return Status(StatusCode::NOT_FOUND, "Unable to open CPU profile artifact");
}
std::array<char, 64 * 1024> buffer{};
while (input.good()) {
input.read(buffer.data(), static_cast<std::streamsize>(buffer.size()));
const std::streamsize count = input.gcount();
if (count <= 0) { break; }
CpuProfileChunk chunk;
chunk.set_data(buffer.data(), static_cast<size_t>(count));
if (!writer->Write(chunk)) {
return Status(StatusCode::CANCELLED, "Profile download cancelled");
}
}
if (input.bad()) {
return Status(StatusCode::INTERNAL, "Failed while reading CPU profile artifact");
}
return Status::OK;
}
auto ConvertPlayerInfo(
const google::protobuf::RepeatedPtrField<net::eagle0::common::PlayerSetupInfo> &allPis,
PlayerId shardokPid) -> PlayerInfoWithUnits {
@@ -420,10 +543,11 @@ void EagleInterfaceImpl::PopulateGameStatusResponse(
string(controller->GetCurrentGameStateBytes());
response->mutable_game_update_response()->set_current_round(results.currentRound);
for (const auto &[playerId, troopCount] : results.playerTroopCounts) {
for (const auto &[playerId, troopCount, isRetreating] : results.playerBattleProgress) {
auto *ptc = response->mutable_game_update_response()->add_player_troop_counts();
ptc->set_player_id(playerId);
ptc->set_troop_count(troopCount);
ptc->set_is_retreating(isRetreating);
}
}
}
@@ -539,7 +663,7 @@ auto EagleInterfaceImpl::SubscribeToGame(
int32_t newUnfilteredCount,
const byte_vector &currentGameState,
int32_t currentRound,
const vector<std::pair<int32_t, int32_t>> &playerTroopCounts) override {
const vector<PlayerBattleProgress> &playerBattleProgress) override {
if (!active_) return;
GameStatusResponse response;
@@ -568,10 +692,11 @@ auto EagleInterfaceImpl::SubscribeToGame(
std::string(currentGameState.begin(), currentGameState.end());
response.mutable_game_update_response()->set_current_round(currentRound);
for (const auto &[playerId, troopCount] : playerTroopCounts) {
for (const auto &[playerId, troopCount, isRetreating] : playerBattleProgress) {
auto *ptc = response.mutable_game_update_response()->add_player_troop_counts();
ptc->set_player_id(playerId);
ptc->set_troop_count(troopCount);
ptc->set_is_retreating(isRetreating);
}
ScopedShardokLatencyTrace trace(
@@ -14,6 +14,7 @@
#include <thread>
#include "src/main/cpp/net/eagle0/common/ProtobufWarningSuppression.hpp"
#include "src/main/cpp/net/eagle0/shardok/server/CpuProfiler.hpp"
#include "src/main/cpp/net/eagle0/shardok/server/ShardokGamesManager.hpp"
#include "src/main/cpp/net/eagle0/shardok/server/TokenAuthInterceptor.hpp"
#pragma GCC diagnostic push
@@ -31,6 +32,10 @@ SUPPRESS_PROTOBUF_WARNINGS
namespace shardok {
using grpc::ServerContext;
using grpc::Status;
using net::eagle0::common::CpuProfileChunk;
using net::eagle0::common::CpuProfileDownloadRequest;
using net::eagle0::common::CpuProfileStatusRequest;
using net::eagle0::common::CpuProfileStatusResponse;
using net::eagle0::common::GameSetupInfo;
using net::eagle0::common::GameStatusResponse;
using net::eagle0::common::GameSubscriptionRequest;
@@ -42,11 +47,14 @@ using net::eagle0::common::NewGameRequest;
using net::eagle0::common::PlacementCommandsRequest;
using net::eagle0::common::PostCommandRequest;
using net::eagle0::common::ShardokInternalInterface;
using net::eagle0::common::StartCpuProfileRequest;
using net::eagle0::common::StopCpuProfileRequest;
class EagleInterfaceImpl final : public ShardokInternalInterface::Service {
private:
std::shared_ptr<ShardokGamesManager> gamesManager;
TokenValidator tokenValidator_;
CpuProfiler cpuProfiler_;
auto ControllerForGame(const GameId& gameId, const GameSetupInfo& setupInfo)
-> std::shared_ptr<ShardokGameController>;
@@ -87,6 +95,23 @@ public:
ServerContext* context,
const GameSubscriptionRequest* request,
grpc::ServerWriter<GameStatusResponse>* writer) -> Status override;
auto GetCpuProfileStatus(
ServerContext* context,
const CpuProfileStatusRequest* request,
CpuProfileStatusResponse* response) -> Status override;
auto StartCpuProfile(
ServerContext* context,
const StartCpuProfileRequest* request,
CpuProfileStatusResponse* response) -> Status override;
auto StopCpuProfile(
ServerContext* context,
const StopCpuProfileRequest* request,
CpuProfileStatusResponse* response) -> Status override;
auto DownloadCpuProfile(
ServerContext* context,
const CpuProfileDownloadRequest* request,
grpc::ServerWriter<CpuProfileChunk>* writer) -> Status override;
};
} // namespace shardok
@@ -56,9 +56,13 @@ namespace Auth {
/// Routed to Go auth service.
/// </summary>
/// <param name="provider">OAuth provider (Discord, Google, GitHub, Apple)</param>
/// <returns>Tuple of (URL to open in browser, state token for polling)</returns>
public async Task<(string authUrl, string state)> GetOAuthUrlAsync(OAuthProvider provider) {
var request = new GetOAuthUrlRequest { Provider = provider };
/// <param name="pollOnly">Whether the client will poll instead of accepting a deep
/// link</param> <returns>Tuple of (URL to open in browser, state token for
/// polling)</returns>
public async Task<(string authUrl, string state)> GetOAuthUrlAsync(
OAuthProvider provider,
bool pollOnly = false) {
var request = new GetOAuthUrlRequest { Provider = provider, PollOnly = pollOnly };
var response = await _authServiceClient.GetOAuthUrlAsync(request);
Debug.Log($"[AuthClient] Got OAuth URL for {provider}, state={response.State}");
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using common;
using Grpc.Core;
@@ -26,6 +27,10 @@ namespace Auth {
private string _pendingOAuthState; // Queued if OAuth result arrives before auth
// client ready
private const float TokenRefreshCheckIntervalSeconds = 5 * 60f;
private float _nextTokenRefreshCheckTime;
private readonly SemaphoreSlim _tokenRefreshSemaphore = new(1, 1);
// Events for UI updates
public event Action<UserInfo> OnLoginSuccess;
public event Action<string> OnLoginFailed;
@@ -66,6 +71,40 @@ namespace Auth {
CheckCommandLineForDeepLink();
}
private void Update() {
if (Time.unscaledTime < _nextTokenRefreshCheckTime) return;
_nextTokenRefreshCheckTime = Time.unscaledTime + TokenRefreshCheckIntervalSeconds;
TryProactiveTokenRefresh();
}
private void OnApplicationFocus(bool hasFocus) {
if (hasFocus) { TryProactiveTokenRefresh(); }
}
private void OnApplicationPause(bool pauseStatus) {
if (!pauseStatus) { TryProactiveTokenRefresh(); }
}
private void TryProactiveTokenRefresh() {
if (_authClient == null || !TokenStorage.NeedsRefresh ||
!TokenStorage.HasRefreshToken) {
return;
}
_ = RefreshTokenProactivelyAsync();
}
private async Task RefreshTokenProactivelyAsync() {
try {
await EnsureValidTokenAsync();
} catch (Exception ex) {
// A reconnect will make one final refresh attempt and surface an explicit
// authentication error if the refresh token is no longer usable.
Debug.LogWarning($"[OAuthManager] Proactive token refresh failed: {ex.Message}");
}
}
private void CheckCommandLineForDeepLink() {
var args = System.Environment.GetCommandLineArgs();
// Args[0] is the executable path, check remaining args for eagle0:// URLs
@@ -281,8 +320,13 @@ namespace Auth {
_currentLoginProvider = provider; // Track for later storage
try {
#if UNITY_EDITOR
const bool pollOnly = true;
#else
const bool pollOnly = false;
#endif
// Get OAuth URL and state from server
var (authUrl, state) = await _authClient.GetOAuthUrlAsync(provider);
var (authUrl, state) = await _authClient.GetOAuthUrlAsync(provider, pollOnly);
// Open system browser
Debug.Log($"[OAuthManager] Opening browser for OAuth: {authUrl}");
@@ -410,9 +454,15 @@ namespace Auth {
/// </summary>
public async Task EnsureValidTokenAsync() {
EnsureConfigured();
if (TokenStorage.NeedsRefresh && TokenStorage.HasRefreshToken) {
await _authClient.RefreshTokenAsync();
}
await _tokenRefreshSemaphore.WaitAsync();
try {
// Re-check after acquiring the semaphore because another caller may have
// completed the refresh while this caller was waiting.
if (TokenStorage.NeedsRefresh && TokenStorage.HasRefreshToken) {
await _authClient.RefreshTokenAsync();
}
} finally { _tokenRefreshSemaphore.Release(); }
}
/// <summary>
@@ -8,6 +8,8 @@ namespace Auth {
/// </summary>
[Serializable]
public class StoredAccount {
private const long RefreshEarlySeconds = 30 * 60;
public string Provider; // "discord" or "google"
public string UserId; // OAuth provider user ID
public string DisplayName; // User's display name
@@ -26,7 +28,7 @@ namespace Auth {
public bool NeedsRefresh {
get {
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
return ExpiresAt > 0 && ExpiresAt - now < 300; // 5 minutes
return ExpiresAt > 0 && ExpiresAt - now < RefreshEarlySeconds;
}
}
}
@@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Linq;
using eagle;
using Eagle0.Tutorial;
using Net.Eagle0.Eagle.Api;
using Net.Eagle0.Eagle.Common;
using Net.Eagle0.Eagle.Views;
@@ -38,11 +37,7 @@ public class AvailableGameItem : MonoBehaviour {
}
}
public void JoinClicked() {
// Track multiplayer status for tutorial system
TutorialManager.IsMultiplayerGame = maxHumanPlayerCount > 1;
JoinCallback(gameId, SelectedLeaderTextId);
}
public void JoinClicked() { JoinCallback(gameId, SelectedLeaderTextId); }
public void DropClicked() { DropCallback?.Invoke(gameId); }
@@ -12,7 +12,6 @@ using common;
using common.GUIUtils;
using common.WhatsNew;
using eagle;
using Eagle0.Tutorial;
using Grpc.Core;
using Grpc.Core.Interceptors;
using Net.Eagle0.Eagle.Api;
@@ -144,6 +143,8 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
List<GameInfo> fetchedRunningGames;
List<AvailableLeader> fetchedNewGameLeaders;
private Task<EagleGameController> _eagleScenePreloadTask;
private long _acceptedGameId;
private EagleConnection eagleConnection;
private PersistentClientConnection _persistentClientConnection;
private HttpClient _httpClient;
@@ -301,10 +302,9 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
// Refresh stored account buttons
RefreshStoredAccountButtons();
// Show sign-in tutorial for new users who have no stored accounts
// Show a sign-in hint for new users who have no stored accounts.
var accounts = OAuthManager.Instance?.GetStoredAccounts();
if (accounts == null || accounts.Count == 0) {
TutorialManager.Instance?.OnGameEvent("auth_panel_shown");
// Show hint for users coming from invitation flow
SetOAuthStatusText(InvitationSignInHint);
// Exit fullscreen so user can see browser with the sign-in link
@@ -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));
@@ -854,9 +854,6 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
// Clear loading indicator now that lobby is populated
if (lobbyStatusText != null) { lobbyStatusText.text = ""; }
// Trigger lobby tutorial for users who haven't seen it
TutorialManager.Instance?.OnGameEvent("lobby_entered");
});
}
@@ -897,7 +894,8 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
_persistentClientConnection = new PersistentClientConnection(
eagleConnection.EagleGrpcClient,
eagleConnection.credentials,
EagleConnection.EagleCancellationToken);
EagleConnection.EagleCancellationToken,
TryRefreshAuthenticationOnMainThreadAsync);
_persistentClientConnection.OnChannelDead = () => {
// Queue to main thread to avoid threading issues
MainQueue.Q.Enqueue(() => {
@@ -918,6 +916,28 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
ResourceFetcher.SetUpConnection(_httpClient);
}
private Task<bool> TryRefreshAuthenticationOnMainThreadAsync() {
var completion =
new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
MainQueue.Q.Enqueue(async () => {
try {
if (OAuthManager.Instance == null) {
completion.TrySetResult(false);
return;
}
await OAuthManager.Instance.EnsureValidTokenAsync();
completion.TrySetResult(TokenStorage.HasValidToken);
} catch (Exception ex) {
Debug.LogWarning($"[ConnectionHandler] Session refresh failed: {ex.Message}");
completion.TrySetResult(false);
}
});
return completion.Task;
}
private void _internalCustomBattle() {
// Connection already exists when called from lobby
_persistentClientConnection.SetLobbySubscriber(this);
@@ -984,9 +1004,6 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
private void ConfirmDropGame(long gameId) { _ = _internalDropGame(gameId); }
private void CreateGame(string leaderNameTextId, int humanPlayerCount, int totalPlayerCount) {
// Track multiplayer status for tutorial system
TutorialManager.IsMultiplayerGame = humanPlayerCount > 1;
TutorialManager.IsTutorialGame = false;
var game = _internalCreateGame(leaderNameTextId, humanPlayerCount, totalPlayerCount);
}
@@ -998,8 +1015,6 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
}
public void CreateTutorialGame(TutorialPhase startPhase) {
TutorialManager.IsMultiplayerGame = false;
TutorialManager.IsTutorialGame = true;
var game = _internalCreateGame(
desiredLeaderTextId: "",
humanPlayerCount: 1,
@@ -1026,11 +1041,20 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
switch (response.Result) {
case JoinGameResult.SuccessJoinGameResult: SelectEagleGame(response.GameId); break;
default: Debug.LogError($"Game creation failed: {response.Result}"); break;
default:
Debug.LogError($"Game creation failed: {response.Result}");
_ = ResetGameCreation();
break;
}
} catch (Exception e) { Console.WriteLine(e.ToString()); }
}
public void HandleCreateGameAcceptedResponse(CreateGameAcceptedResponse response) {
_acceptedGameId = response.GameId;
createGameItem.SetCreating(true);
if (_eagleScenePreloadTask == null) { _eagleScenePreloadTask = PreloadEagleScene(); }
}
public void HandleCustomBattleResponse(CustomBattleResponse response) {
try {
customBattleHandler.HandleCustomBattleResponse(response);
@@ -1164,9 +1188,13 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
SetLobbyActive(false);
var selectedPersistentConnection = _persistentClientConnection;
var selectedEnvironmentName = ConnectedEnvironmentName;
var eagleController = GameSceneManager.Instance == null
var eagleController = _acceptedGameId == gameId && _eagleScenePreloadTask != null
? await _eagleScenePreloadTask
: GameSceneManager.Instance == null
? eagleCanvas?.GetComponent<EagleGameController>()
: await GameSceneManager.Instance.TransitionToEagle();
_eagleScenePreloadTask = null;
_acceptedGameId = 0;
if (eagleController == null) {
Debug.LogError(
"Cannot select Eagle game because the Eagle scene did not provide an " +
@@ -1192,6 +1220,31 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
selectedPersistentConnection,
selectedEnvironmentName);
ShowEaglePanel(eagleCanvas);
createGameItem.SetCreating(false);
}
}
private async Task<EagleGameController> PreloadEagleScene() {
var eagleController = GameSceneManager.Instance == null
? eagleCanvas?.GetComponent<EagleGameController>()
: await GameSceneManager.Instance.TransitionToEagle();
var loadedCanvas = eagleController?.GetComponent<Canvas>();
if (loadedCanvas != null) { loadedCanvas.gameObject.SetActive(false); }
return eagleController;
}
private async Task ResetGameCreation() {
createGameItem.SetCreating(false);
_acceptedGameId = 0;
var preloadTask = _eagleScenePreloadTask;
_eagleScenePreloadTask = null;
if (preloadTask != null) {
try {
await preloadTask;
} catch (Exception e) { Debug.LogException(e); }
}
if (GameSceneManager.Instance != null) {
await GameSceneManager.Instance.UnloadEagleScene();
}
}
}

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