mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-29 02:15:43 +00:00
Compare commits
56
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5d636a94c | ||
|
|
e0d5a9f2ec | ||
|
|
bf02de3190 | ||
|
|
cbc14e5131 | ||
|
|
9974f32ebb | ||
|
|
ee6189a9d7 | ||
|
|
e0304125b8 | ||
|
|
11eac42e30 | ||
|
|
d0a63f1c61 | ||
|
|
97df984189 | ||
|
|
6c771df84f | ||
|
|
7e40420fe1 | ||
|
|
2ed6ad3c4c | ||
|
|
844e2b0407 | ||
|
|
1617867c60 | ||
|
|
7c49746c37 | ||
|
|
711c6606bb | ||
|
|
e142da7c57 | ||
|
|
c8906b1c04 | ||
|
|
1c8f328c58 | ||
|
|
7ebca60863 | ||
|
|
ce22b59a8a | ||
|
|
1fe062baaa | ||
|
|
6b2ab53574 | ||
|
|
6208d2cf10 | ||
|
|
e105461692 | ||
|
|
2e03352dea | ||
|
|
8c19a93f3c | ||
|
|
9b2bce6537 | ||
|
|
ee4914dcc8 | ||
|
|
1e019c533a | ||
|
|
3d092e580f | ||
|
|
85e530c5a4 | ||
|
|
486960a6aa | ||
|
|
1cb2dd7b6a | ||
|
|
5483c732cc | ||
|
|
6402a8c283 | ||
|
|
eb762e1bae | ||
|
|
be31464e99 | ||
|
|
e6f9d4e4ac | ||
|
|
4d3b2ddb36 | ||
|
|
6edb4de0dc | ||
|
|
72a0f84105 | ||
|
|
f73798ae6e | ||
|
|
19691682e0 | ||
|
|
0e51bece68 | ||
|
|
a89f740b3b | ||
|
|
7959da0a5a | ||
|
|
bc119b2aab | ||
|
|
25c7788254 | ||
|
|
1246f8bcf6 | ||
|
|
f3e44fb9cf | ||
|
|
50aa61b77c | ||
|
|
facfcf9ac9 | ||
|
|
4c21368f96 | ||
|
|
7eccd69a01 |
@@ -6,4 +6,7 @@
|
||||
*.bytes filter=lfs diff=lfs merge=lfs -text
|
||||
*.psd filter=lfs diff=lfs merge=lfs -text
|
||||
*.ttf filter=lfs diff=lfs merge=lfs -text
|
||||
# Exclude pre-existing font files that were committed as blobs (not LFS pointers)
|
||||
src/main/csharp/**/GUI[[:space:]]Pro[[:space:]]Kit*/**/*.ttf !filter !diff !merge
|
||||
src/main/csharp/**/Modern[[:space:]]UI[[:space:]]Pack/**/*.ttf !filter !diff !merge
|
||||
*.herodata filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
name: Build Linux Sysroot
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Sysroot version (e.g., v2, v3)'
|
||||
required: true
|
||||
default: 'v2'
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build-sysroot:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build sysroot
|
||||
run: ./tools/sysroot/build_sysroot.sh
|
||||
|
||||
- name: Upload sysroot artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ubuntu-noble-sysroot
|
||||
path: tools/sysroot/output/
|
||||
|
||||
- name: Install AWS CLI
|
||||
run: |
|
||||
if ! command -v aws &> /dev/null; then
|
||||
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
|
||||
unzip -q awscliv2.zip
|
||||
sudo ./aws/install
|
||||
fi
|
||||
|
||||
- name: Upload to DigitalOcean Spaces
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.SECRET_KEY }}
|
||||
run: |
|
||||
# Upload sysroot tarball to DO Spaces (using eagle0-windows bucket, same as other workflows)
|
||||
aws s3 cp tools/sysroot/output/ubuntu_noble_amd64_sysroot.tar.xz \
|
||||
s3://eagle0-windows/sysroot/${{ inputs.version }}/ubuntu_noble_amd64_sysroot.tar.xz \
|
||||
--endpoint-url https://sfo3.digitaloceanspaces.com \
|
||||
--acl public-read
|
||||
|
||||
# Upload sha256 file
|
||||
aws s3 cp tools/sysroot/output/ubuntu_noble_amd64_sysroot.sha256 \
|
||||
s3://eagle0-windows/sysroot/${{ inputs.version }}/ubuntu_noble_amd64_sysroot.sha256 \
|
||||
--endpoint-url https://sfo3.digitaloceanspaces.com \
|
||||
--acl public-read
|
||||
|
||||
echo ""
|
||||
echo "=== Sysroot uploaded ==="
|
||||
echo "URL: https://eagle0-windows.sfo3.digitaloceanspaces.com/sysroot/${{ inputs.version }}/ubuntu_noble_amd64_sysroot.tar.xz"
|
||||
echo "SHA256: $(cat tools/sysroot/output/ubuntu_noble_amd64_sysroot.sha256)"
|
||||
echo ""
|
||||
echo "Update MODULE.bazel with:"
|
||||
echo "sysroot("
|
||||
echo " name = \"linux_sysroot\","
|
||||
echo " sha256 = \"$(cat tools/sysroot/output/ubuntu_noble_amd64_sysroot.sha256)\","
|
||||
echo " urls = [\"https://eagle0-windows.sfo3.digitaloceanspaces.com/sysroot/${{ inputs.version }}/ubuntu_noble_amd64_sysroot.tar.xz\"],"
|
||||
echo ")"
|
||||
@@ -0,0 +1,72 @@
|
||||
name: Docker Build and Push
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
paths:
|
||||
- 'src/main/cpp/**'
|
||||
- 'src/main/scala/**'
|
||||
- 'src/main/protobuf/**'
|
||||
- 'src/main/resources/**'
|
||||
- 'ci/BUILD.bazel'
|
||||
- 'MODULE.bazel'
|
||||
- '.github/workflows/docker_build.yml'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
push_images:
|
||||
description: 'Push images to container registry'
|
||||
required: true
|
||||
default: 'false'
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build-eagle:
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: false
|
||||
|
||||
- name: Build Eagle Docker image
|
||||
run: bazel build //ci:eagle_server_image
|
||||
|
||||
- name: Login to DigitalOcean Container Registry
|
||||
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
|
||||
env:
|
||||
DO_TOKEN: ${{ secrets.DO_REGISTRY_TOKEN }}
|
||||
run: |
|
||||
mkdir -p ~/.docker
|
||||
AUTH=$(echo -n "${DO_TOKEN}:${DO_TOKEN}" | base64)
|
||||
echo "{\"auths\":{\"registry.digitalocean.com\":{\"auth\":\"${AUTH}\"}}}" > ~/.docker/config.json
|
||||
|
||||
- name: Push Eagle image to DO registry
|
||||
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
|
||||
run: bazel run //ci:eagle_server_push
|
||||
|
||||
build-shardok:
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: false
|
||||
|
||||
- name: Build Shardok Docker image (cross-compile for Linux)
|
||||
run: bazel build --platforms=//:linux_x86_64 //ci:shardok_server_image
|
||||
|
||||
- name: Login to DigitalOcean Container Registry
|
||||
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
|
||||
env:
|
||||
DO_TOKEN: ${{ secrets.DO_REGISTRY_TOKEN }}
|
||||
run: |
|
||||
mkdir -p ~/.docker
|
||||
AUTH=$(echo -n "${DO_TOKEN}:${DO_TOKEN}" | base64)
|
||||
echo "{\"auths\":{\"registry.digitalocean.com\":{\"auth\":\"${AUTH}\"}}}" > ~/.docker/config.json
|
||||
|
||||
- name: Push Shardok image to DO registry
|
||||
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
|
||||
run: bazel run --platforms=//:linux_x86_64 //ci:shardok_server_push
|
||||
@@ -3,6 +3,15 @@ load("@io_bazel_rules_go//go:def.bzl", "nogo")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
# Platform for cross-compiling to Linux x86_64
|
||||
platform(
|
||||
name = "linux_x86_64",
|
||||
constraint_values = [
|
||||
"@platforms//os:linux",
|
||||
"@platforms//cpu:x86_64",
|
||||
],
|
||||
)
|
||||
|
||||
gazelle(name = "gazelle")
|
||||
|
||||
# gazelle:proto file
|
||||
|
||||
+60
-17
@@ -26,50 +26,68 @@ scala_config = use_extension(
|
||||
"@rules_scala//scala/extensions:config.bzl",
|
||||
"scala_config",
|
||||
)
|
||||
|
||||
scala_config.settings(scala_version = SCALA_VERSION)
|
||||
|
||||
scala_deps = use_extension(
|
||||
"@rules_scala//scala/extensions:deps.bzl",
|
||||
"scala_deps",
|
||||
)
|
||||
|
||||
scala_deps.scala()
|
||||
|
||||
scala_deps.scalatest()
|
||||
|
||||
scala_deps.scala_proto()
|
||||
|
||||
#
|
||||
# Language Support - C++
|
||||
#
|
||||
|
||||
bazel_dep(name = "toolchains_llvm", version = "1.4.0")
|
||||
bazel_dep(name = "toolchains_llvm", version = "1.6.0")
|
||||
|
||||
llvm = use_extension("@toolchains_llvm//toolchain/extensions:llvm.bzl", "llvm")
|
||||
|
||||
# Native toolchain (macOS -> macOS, Linux -> Linux)
|
||||
llvm.toolchain(
|
||||
name = "llvm_toolchain",
|
||||
llvm_version = "20.1.2",
|
||||
)
|
||||
|
||||
use_repo(llvm, "llvm_toolchain")
|
||||
# Cross-compilation toolchain (macOS -> Linux x86_64)
|
||||
# Uses the same LLVM distribution but with a Linux sysroot
|
||||
llvm.toolchain(
|
||||
name = "llvm_toolchain_linux",
|
||||
llvm_version = "20.1.2",
|
||||
)
|
||||
|
||||
# Linux sysroot for cross-compilation (Chromium's Debian sysroot)
|
||||
llvm.sysroot(
|
||||
name = "llvm_toolchain_linux",
|
||||
label = "@linux_sysroot//sysroot",
|
||||
targets = ["linux-x86_64"],
|
||||
)
|
||||
|
||||
use_repo(llvm, "llvm_toolchain", "llvm_toolchain_linux")
|
||||
|
||||
# Download the Linux sysroot (Ubuntu 24.04 Noble for C++23 support)
|
||||
# Built by: .github/workflows/build_sysroot.yml
|
||||
# To rebuild: Run the "Build Linux Sysroot" workflow with a new version, then update sha256 and URL
|
||||
sysroot = use_repo_rule("@toolchains_llvm//toolchain:sysroot.bzl", "sysroot")
|
||||
sysroot(
|
||||
name = "linux_sysroot",
|
||||
sha256 = "aadb60a2e2c776ac000bb2e29f3039fd81b5b031500cf4a1651b365712819d1f",
|
||||
urls = ["https://eagle0-windows.sfo3.digitaloceanspaces.com/sysroot/v2/ubuntu_noble_amd64_sysroot.tar.xz"],
|
||||
)
|
||||
|
||||
#
|
||||
# Language Support - Go
|
||||
#
|
||||
|
||||
bazel_dep(name = "rules_go", repo_name = "io_bazel_rules_go", version = "0.56.1")
|
||||
bazel_dep(name = "gazelle", repo_name = "bazel_gazelle", version = "0.45.0")
|
||||
bazel_dep(name = "rules_go", version = "0.56.1", repo_name = "io_bazel_rules_go")
|
||||
bazel_dep(name = "gazelle", version = "0.45.0", repo_name = "bazel_gazelle")
|
||||
|
||||
go_sdk = use_extension("@io_bazel_rules_go//go:extensions.bzl", "go_sdk")
|
||||
|
||||
go_sdk.download(version = "1.23.3")
|
||||
|
||||
go_deps = use_extension("@bazel_gazelle//:extensions.bzl", "go_deps")
|
||||
|
||||
go_deps.from_file(go_mod = "//:go.mod")
|
||||
|
||||
use_repo(
|
||||
go_deps,
|
||||
"com_github_aws_aws_sdk_go_v2",
|
||||
@@ -84,15 +102,15 @@ use_repo(
|
||||
# Platform Support - Apple/iOS
|
||||
#
|
||||
|
||||
bazel_dep(name = "apple_support", repo_name = "build_bazel_apple_support", version = "1.21.1")
|
||||
bazel_dep(name = "rules_apple", repo_name = "build_bazel_rules_apple", version = "3.16.1")
|
||||
bazel_dep(name = "rules_swift", repo_name = "build_bazel_rules_swift", version = "2.3.1")
|
||||
bazel_dep(name = "apple_support", version = "1.21.1", repo_name = "build_bazel_apple_support")
|
||||
bazel_dep(name = "rules_apple", version = "3.16.1", repo_name = "build_bazel_rules_apple")
|
||||
bazel_dep(name = "rules_swift", version = "2.3.1", repo_name = "build_bazel_rules_swift")
|
||||
|
||||
#
|
||||
# Protocol Buffers & RPC
|
||||
#
|
||||
|
||||
bazel_dep(name = "protobuf", repo_name = "com_google_protobuf", version = "29.2")
|
||||
bazel_dep(name = "protobuf", version = "29.2", repo_name = "com_google_protobuf")
|
||||
bazel_dep(name = "grpc", version = "1.71.0")
|
||||
bazel_dep(name = "grpc-java", version = "1.71.0")
|
||||
bazel_dep(name = "flatbuffers", version = "25.2.10")
|
||||
@@ -103,6 +121,32 @@ bazel_dep(name = "flatbuffers", version = "25.2.10")
|
||||
|
||||
bazel_dep(name = "googletest", version = "1.17.0")
|
||||
|
||||
#
|
||||
# Container Images (OCI)
|
||||
#
|
||||
|
||||
bazel_dep(name = "rules_oci", version = "2.2.6")
|
||||
bazel_dep(name = "aspect_bazel_lib", version = "2.16.0")
|
||||
|
||||
oci = use_extension("@rules_oci//oci:extensions.bzl", "oci")
|
||||
|
||||
# Base image for Eagle (Java 17)
|
||||
oci.pull(
|
||||
name = "eclipse_temurin_17",
|
||||
digest = "sha256:d286b5352d98777bbf727f54038b04f0145cd9b76ca83f38a67aa111d4303748",
|
||||
image = "docker.io/library/eclipse-temurin",
|
||||
platforms = ["linux/amd64"],
|
||||
)
|
||||
|
||||
# Base image for Shardok (Ubuntu 24.04 for C++ runtime)
|
||||
oci.pull(
|
||||
name = "ubuntu_24_04",
|
||||
image = "docker.io/library/ubuntu",
|
||||
platforms = ["linux/amd64"],
|
||||
tag = "24.04",
|
||||
)
|
||||
use_repo(oci, "eclipse_temurin_17", "eclipse_temurin_17_linux_amd64", "ubuntu_24_04", "ubuntu_24_04_linux_amd64")
|
||||
|
||||
#
|
||||
# Java/Scala Dependencies
|
||||
#
|
||||
@@ -110,7 +154,6 @@ bazel_dep(name = "googletest", version = "1.17.0")
|
||||
bazel_dep(name = "rules_jvm_external", version = "6.3")
|
||||
|
||||
maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")
|
||||
|
||||
maven.install(
|
||||
artifacts = [
|
||||
# Netty
|
||||
@@ -173,7 +216,6 @@ maven.install(
|
||||
"https://repo1.maven.org/maven2",
|
||||
],
|
||||
)
|
||||
|
||||
use_repo(maven, "maven", "unpinned_maven")
|
||||
|
||||
#
|
||||
@@ -221,5 +263,6 @@ register_toolchains(
|
||||
# Set dev_dependency so we can turn this off for swift MacOS builds
|
||||
register_toolchains(
|
||||
"@llvm_toolchain//:all",
|
||||
"@llvm_toolchain_linux//:all",
|
||||
dev_dependency = True,
|
||||
)
|
||||
|
||||
Generated
+275
-88
@@ -26,7 +26,11 @@
|
||||
"https://bcr.bazel.build/modules/aspect_bazel_lib/1.38.0/MODULE.bazel": "6307fec451ba9962c1c969eb516ebfe1e46528f7fa92e1c9ac8646bef4cdaa3f",
|
||||
"https://bcr.bazel.build/modules/aspect_bazel_lib/1.40.3/MODULE.bazel": "668e6bcb4d957fc0e284316dba546b705c8d43c857f87119619ee83c4555b859",
|
||||
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.11.0/MODULE.bazel": "cb1ba9f9999ed0bc08600c221f532c1ddd8d217686b32ba7d45b0713b5131452",
|
||||
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.11.0/source.json": "92494d5aa43b96665397dd13ee16023097470fa85e276b93674d62a244de47ee",
|
||||
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.14.0/MODULE.bazel": "2b31ffcc9bdc8295b2167e07a757dbbc9ac8906e7028e5170a3708cecaac119f",
|
||||
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.16.0/MODULE.bazel": "852f9ebbda017572a7c113a2434592dd3b2f55cd9a0faea3d4be5a09a59e4900",
|
||||
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.19.3/MODULE.bazel": "253d739ba126f62a5767d832765b12b59e9f8d2bc88cc1572f4a73e46eb298ca",
|
||||
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.19.3/source.json": "ffab9254c65ba945f8369297ad97ca0dec213d3adc6e07877e23a48624a8b456",
|
||||
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.7.2/MODULE.bazel": "780d1a6522b28f5edb7ea09630748720721dfe27690d65a2d33aa7509de77e07",
|
||||
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.7.7/MODULE.bazel": "491f8681205e31bb57892d67442ce448cda4f472a8e6b3dc062865e29a64f89c",
|
||||
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.8.1/MODULE.bazel": "812d2dd42f65dca362152101fbec418029cc8fd34cbad1a2fde905383d705838",
|
||||
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.9.3/MODULE.bazel": "66baf724dbae7aff4787bf2245cc188d50cb08e07789769730151c0943587c14",
|
||||
@@ -51,8 +55,11 @@
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.19.0/MODULE.bazel": "59adcdf28230d220f0067b1f435b8537dd033bfff8db21335ef9217919c7fb58",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.21.0/MODULE.bazel": "675642261665d8eea09989aa3b8afb5c37627f1be178382c320d1b46afba5e3b",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.27.0/MODULE.bazel": "621eeee06c4458a9121d1f104efb80f39d34deff4984e778359c60eaf1a8cb65",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.27.0/source.json": "ed8cf0ef05c858dce3661689d0a2b110ff398e63994e178e4f1f7555a8067fed",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.28.0/MODULE.bazel": "4b4200e6cbf8fa335b2c3f43e1d6ef3e240319c33d43d60cc0fbd4b87ece299d",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.3.0/MODULE.bazel": "cdcafe83ec318cda34e02948e81d790aab8df7a929cec6f6969f13a489ccecd9",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.34.0/MODULE.bazel": "e8475ad7c8965542e0c7aac8af68eb48c4af904be3d614b6aa6274c092c2ea1e",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.38.0/MODULE.bazel": "f9b8a9c890ebd216b4049fd12a31d3c2602e3403c7af636b04fbbd7453edc9c9",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.38.0/source.json": "31ba776c122b54a2885e23651642e32f087a87bf025465f8040751894b571277",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.4.1/MODULE.bazel": "e45b6bb2350aff3e442ae1111c555e27eac1d915e77775f6fdc4b351b758b5d7",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.9.0/MODULE.bazel": "885151d58d90d8d9c811eb75e3288c11f850e1d6b481a8c9f766adee4712358b",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.9.1/MODULE.bazel": "8f679097876a9b609ad1f60249c49d68bfab783dd9be012faf9d82547b14815a",
|
||||
@@ -69,7 +76,8 @@
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.7.0/MODULE.bazel": "0db596f4563de7938de764cc8deeabec291f55e8ec15299718b93c4423e9796d",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.7.1/MODULE.bazel": "3120d80c5861aa616222ec015332e5f8d3171e062e3e804a2a0253e1be26e59b",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.8.1/MODULE.bazel": "88ade7293becda963e0e3ea33e7d54d3425127e0a326e0d17da085a5f1f03ff6",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.8.1/source.json": "7ebaefba0b03efe59cac88ed5bbc67bcf59a3eff33af937345ede2a38b2d368a",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.8.2/MODULE.bazel": "69ad6927098316848b34a9142bcc975e018ba27f08c4ff403f50c1b6e646ca67",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.8.2/source.json": "34a3c8bcf233b835eb74be9d628899bb32999d3e0eadef1947a0a562a2b16ffb",
|
||||
"https://bcr.bazel.build/modules/bazel_worker_api/0.0.6/MODULE.bazel": "fd1f9432ca04c947e91b500df69ce7c5b6dbfe1bc45ab1820338205dae3383a6",
|
||||
"https://bcr.bazel.build/modules/bazel_worker_api/0.0.6/source.json": "5d68545f224904745a3cabd35aea6bc2b6cc5a78b7f49f3f69660eab2eeeb273",
|
||||
"https://bcr.bazel.build/modules/boringssl/0.0.0-20211025-d4f1ab9/MODULE.bazel": "6ee6353f8b1a701fe2178e1d925034294971350b6d3ac37e67e5a7d463267834",
|
||||
@@ -98,6 +106,8 @@
|
||||
"https://bcr.bazel.build/modules/envoy_api/0.0.0-20250128-4de3c74/source.json": "028519164a2e24563f4b43d810fdedc702daed90e71e7042d45ba82ad807b46f",
|
||||
"https://bcr.bazel.build/modules/flatbuffers/25.2.10/MODULE.bazel": "dab15cafe8512d2c4a8daa44c2d7968c5c79f01e220d40076cdc260bf58605e2",
|
||||
"https://bcr.bazel.build/modules/flatbuffers/25.2.10/source.json": "7eae7ea3eb913b9802426e4d5df11d6c6072a3573a548f8cabf1e965f5cca4d0",
|
||||
"https://bcr.bazel.build/modules/gawk/5.3.2.bcr.1/MODULE.bazel": "cdf8cbe5ee750db04b78878c9633cc76e80dcf4416cbe982ac3a9222f80713c8",
|
||||
"https://bcr.bazel.build/modules/gawk/5.3.2.bcr.1/source.json": "fa7b512dfcb5eafd90ce3959cf42a2a6fe96144ebbb4b3b3928054895f2afac2",
|
||||
"https://bcr.bazel.build/modules/gazelle/0.27.0/MODULE.bazel": "3446abd608295de6d90b4a8a118ed64a9ce11dcb3dda2dc3290a22056bd20996",
|
||||
"https://bcr.bazel.build/modules/gazelle/0.30.0/MODULE.bazel": "f888a1effe338491f35f0e0e85003b47bb9d8295ccba73c37e07702d8d31c65b",
|
||||
"https://bcr.bazel.build/modules/gazelle/0.32.0/MODULE.bazel": "b499f58a5d0d3537f3cf5b76d8ada18242f64ec474d8391247438bf04f58c7b8",
|
||||
@@ -137,6 +147,10 @@
|
||||
"https://bcr.bazel.build/modules/grpc/1.70.1/MODULE.bazel": "b800cd8e3e7555c1e61cba2e02d3a2fcf0e91f66e800db286d965d3b7a6a721a",
|
||||
"https://bcr.bazel.build/modules/grpc/1.71.0/MODULE.bazel": "7fcab2c05530373f1a442c362b17740dd0c75b6a2a975eec8f5bf4c70a37928a",
|
||||
"https://bcr.bazel.build/modules/grpc/1.71.0/source.json": "60ef8c4c72c8280ae94c05b4f38bf67785acb25477ab8dbac096a9604449ff90",
|
||||
"https://bcr.bazel.build/modules/helly25_bzl/0.3.1/MODULE.bazel": "3a4be20f6fc13be32ad44643b8252ef5af09eee936f1d943cd4fd7867fa92826",
|
||||
"https://bcr.bazel.build/modules/helly25_bzl/0.3.1/source.json": "b129ab1828492de2c163785bbeb4065c166de52d932524b4317beb5b7f917994",
|
||||
"https://bcr.bazel.build/modules/jq.bzl/0.1.0/MODULE.bazel": "2ce69b1af49952cd4121a9c3055faa679e748ce774c7f1fda9657f936cae902f",
|
||||
"https://bcr.bazel.build/modules/jq.bzl/0.1.0/source.json": "746bf13cac0860f091df5e4911d0c593971cd8796b5ad4e809b2f8e133eee3d5",
|
||||
"https://bcr.bazel.build/modules/jsoncpp/1.9.5/MODULE.bazel": "31271aedc59e815656f5736f282bb7509a97c7ecb43e927ac1a37966e0578075",
|
||||
"https://bcr.bazel.build/modules/jsoncpp/1.9.5/source.json": "4108ee5085dd2885a341c7fab149429db457b3169b86eb081fa245eadf69169d",
|
||||
"https://bcr.bazel.build/modules/libpfm/4.11.0/MODULE.bazel": "45061ff025b301940f1e30d2c16bea596c25b176c8b6b3087e92615adbd52902",
|
||||
@@ -161,6 +175,7 @@
|
||||
"https://bcr.bazel.build/modules/opentelemetry-proto/1.5.0/source.json": "046b721ce203e88cdaad44d7dd17a86b7200eab9388b663b234e72e13ff7b143",
|
||||
"https://bcr.bazel.build/modules/opentracing-cpp/1.6.0/MODULE.bazel": "b3925269f63561b8b880ae7cf62ccf81f6ece55b62cd791eda9925147ae116ec",
|
||||
"https://bcr.bazel.build/modules/opentracing-cpp/1.6.0/source.json": "da1cb1add160f5e5074b7272e9db6fd8f1b3336c15032cd0a653af9d2f484aed",
|
||||
"https://bcr.bazel.build/modules/package_metadata/0.0.2/MODULE.bazel": "fb8d25550742674d63d7b250063d4580ca530499f045d70748b1b142081ebb92",
|
||||
"https://bcr.bazel.build/modules/package_metadata/0.0.5/MODULE.bazel": "ef4f9439e3270fdd6b9fd4dbc3d2f29d13888e44c529a1b243f7a31dfbc2e8e4",
|
||||
"https://bcr.bazel.build/modules/package_metadata/0.0.5/source.json": "2326db2f6592578177751c3e1f74786b79382cd6008834c9d01ec865b9126a85",
|
||||
"https://bcr.bazel.build/modules/platforms/0.0.10/MODULE.bazel": "8cb8efaf200bdeb2150d93e162c40f388529a25852b332cec879373771e48ed5",
|
||||
@@ -225,12 +240,14 @@
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.0.15/MODULE.bazel": "6704c35f7b4a72502ee81f61bf88706b54f06b3cbe5558ac17e2e14666cd5dcc",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.0.16/MODULE.bazel": "7661303b8fc1b4d7f532e54e9d6565771fea666fbdf839e0a86affcd02defe87",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.0.17/MODULE.bazel": "2ae1d8f4238ec67d7185d8861cb0a2cdf4bc608697c331b95bf990e69b62e64a",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.0.17/source.json": "4db99b3f55c90ab28d14552aa0632533e3e8e5e9aea0f5c24ac0014282c2a7c5",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.0.2/MODULE.bazel": "6915987c90970493ab97393024c156ea8fb9f3bea953b2f3ec05c34f19b5695c",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.0.5/MODULE.bazel": "be41f87587998fe8890cd82ea4e848ed8eb799e053c224f78f3ff7fe1a1d9b74",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.0.6/MODULE.bazel": "abf360251023dfe3efcef65ab9d56beefa8394d4176dd29529750e1c57eaa33f",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.0.8/MODULE.bazel": "964c85c82cfeb6f3855e6a07054fdb159aced38e99a5eecf7bce9d53990afa3e",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.0.9/MODULE.bazel": "836e76439f354b89afe6a911a7adf59a6b2518fafb174483ad78a2a2fde7b1c5",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.1.1/MODULE.bazel": "2f0222a6f229f0bf44cd711dc13c858dad98c62d52bd51d8fc3a764a83125513",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.2.14/MODULE.bazel": "353c99ed148887ee89c54a17d4100ae7e7e436593d104b668476019023b58df8",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.2.14/source.json": "55d0a4587c5592fad350f6e698530f4faf0e7dd15e69d43f8d87e220c78bea54",
|
||||
"https://bcr.bazel.build/modules/rules_foreign_cc/0.10.1/MODULE.bazel": "b9527010e5fef060af92b6724edb3691970a5b1f76f74b21d39f7d433641be60",
|
||||
"https://bcr.bazel.build/modules/rules_foreign_cc/0.10.1/source.json": "9300e71df0cdde0952f10afff1401fa664e9fc5d9ae6204660ba1b158d90d6a6",
|
||||
"https://bcr.bazel.build/modules/rules_foreign_cc/0.9.0/MODULE.bazel": "c9e8c682bf75b0e7c704166d79b599f93b72cfca5ad7477df596947891feeef6",
|
||||
@@ -289,6 +306,8 @@
|
||||
"https://bcr.bazel.build/modules/rules_nodejs/6.3.0/MODULE.bazel": "45345e4aba35dd6e4701c1eebf5a4e67af4ed708def9ebcdc6027585b34ee52d",
|
||||
"https://bcr.bazel.build/modules/rules_nodejs/6.3.3/MODULE.bazel": "b66eadebd10f1f1b25f52f95ab5213a57e82c37c3f656fcd9a57ad04d2264ce7",
|
||||
"https://bcr.bazel.build/modules/rules_nodejs/6.3.3/source.json": "45bd343155bdfed2543f0e39b80ff3f6840efc31975da4b5795797f4c94147ad",
|
||||
"https://bcr.bazel.build/modules/rules_oci/2.2.6/MODULE.bazel": "2ba6ddd679269e00aeffe9ca04faa2d0ca4129650982c9246d0d459fe2da47d9",
|
||||
"https://bcr.bazel.build/modules/rules_oci/2.2.6/source.json": "94e7decb8f95d9465b0bbea71c65064cd16083be1350c7468f131818641dc4a5",
|
||||
"https://bcr.bazel.build/modules/rules_pkg/0.7.0/MODULE.bazel": "df99f03fc7934a4737122518bb87e667e62d780b610910f0447665a7e2be62dc",
|
||||
"https://bcr.bazel.build/modules/rules_pkg/1.0.1/MODULE.bazel": "5b1df97dbc29623bccdf2b0dcd0f5cb08e2f2c9050aab1092fd39a41e82686ff",
|
||||
"https://bcr.bazel.build/modules/rules_pkg/1.1.0/MODULE.bazel": "9db8031e71b6ef32d1846106e10dd0ee2deac042bd9a2de22b4761b0c3036453",
|
||||
@@ -321,7 +340,8 @@
|
||||
"https://bcr.bazel.build/modules/rules_scala/7.1.1/source.json": "5038cb231d4020c5965c920681cf961a7bf137b40315025e40f3a7b6a0ac1f0f",
|
||||
"https://bcr.bazel.build/modules/rules_shell/0.2.0/MODULE.bazel": "fda8a652ab3c7d8fee214de05e7a9916d8b28082234e8d2c0094505c5268ed3c",
|
||||
"https://bcr.bazel.build/modules/rules_shell/0.3.0/MODULE.bazel": "de4402cd12f4cc8fda2354fce179fdb068c0b9ca1ec2d2b17b3e21b24c1a937b",
|
||||
"https://bcr.bazel.build/modules/rules_shell/0.3.0/source.json": "c55ed591aa5009401ddf80ded9762ac32c358d2517ee7820be981e2de9756cf3",
|
||||
"https://bcr.bazel.build/modules/rules_shell/0.4.1/MODULE.bazel": "00e501db01bbf4e3e1dd1595959092c2fadf2087b2852d3f553b5370f5633592",
|
||||
"https://bcr.bazel.build/modules/rules_shell/0.4.1/source.json": "4757bd277fe1567763991c4425b483477bb82e35e777a56fd846eb5cceda324a",
|
||||
"https://bcr.bazel.build/modules/rules_swift/1.16.0/MODULE.bazel": "4a09f199545a60d09895e8281362b1ff3bb08bbde69c6fc87aff5b92fcc916ca",
|
||||
"https://bcr.bazel.build/modules/rules_swift/1.18.0/MODULE.bazel": "a6aba73625d0dc64c7b4a1e831549b6e375fbddb9d2dde9d80c9de6ec45b24c9",
|
||||
"https://bcr.bazel.build/modules/rules_swift/2.1.1/MODULE.bazel": "494900a80f944fc7aa61500c2073d9729dff0b764f0e89b824eb746959bc1046",
|
||||
@@ -339,14 +359,19 @@
|
||||
"https://bcr.bazel.build/modules/stardoc/0.7.2/source.json": "58b029e5e901d6802967754adf0a9056747e8176f017cfe3607c0851f4d42216",
|
||||
"https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.1/MODULE.bazel": "5e463fbfba7b1701d957555ed45097d7f984211330106ccd1352c6e0af0dcf91",
|
||||
"https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.1/source.json": "32bd87e5f4d7acc57c5b2ff7c325ae3061d5e242c0c4c214ae87e0f1c13e54cb",
|
||||
"https://bcr.bazel.build/modules/toolchains_llvm/1.4.0/MODULE.bazel": "05239402b7374293359c2f22806f420b75aa5d6f4b15a2eaa809a2c214d58b31",
|
||||
"https://bcr.bazel.build/modules/toolchains_llvm/1.4.0/source.json": "229a516d282b17a82be54c6e3ae220a1b750fb55a8495567e5c7a9d09423f3e2",
|
||||
"https://bcr.bazel.build/modules/tar.bzl/0.2.1/MODULE.bazel": "52d1c00a80a8cc67acbd01649e83d8dd6a9dc426a6c0b754a04fe8c219c76468",
|
||||
"https://bcr.bazel.build/modules/tar.bzl/0.6.0/MODULE.bazel": "a3584b4edcfafcabd9b0ef9819808f05b372957bbdff41601429d5fd0aac2e7c",
|
||||
"https://bcr.bazel.build/modules/tar.bzl/0.6.0/source.json": "4a620381df075a16cb3a7ed57bd1d05f7480222394c64a20fa51bdb636fda658",
|
||||
"https://bcr.bazel.build/modules/toolchains_llvm/1.6.0/MODULE.bazel": "39603859cafb1c6830160fcd6370552e836790e6abb2bfb8d13bff53c0c10a64",
|
||||
"https://bcr.bazel.build/modules/toolchains_llvm/1.6.0/source.json": "6bd3ef95a288dd2bb1582eca332af850c9a5428a23bb92cb1c57c2dfe6cb7369",
|
||||
"https://bcr.bazel.build/modules/upb/0.0.0-20211020-160625a/MODULE.bazel": "6cced416be2dc5b9c05efd5b997049ba795e5e4e6fafbe1624f4587767638928",
|
||||
"https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/MODULE.bazel": "7298990c00040a0e2f121f6c32544bab27d4452f80d9ce51349b1a28f3005c43",
|
||||
"https://bcr.bazel.build/modules/upb/0.0.0-20230516-61a97ef/MODULE.bazel": "c0df5e35ad55e264160417fd0875932ee3c9dda63d9fccace35ac62f45e1b6f9",
|
||||
"https://bcr.bazel.build/modules/upb/0.0.0-20230907-e7430e6/MODULE.bazel": "3a7dedadf70346e678dc059dbe44d05cbf3ab17f1ce43a1c7a42edc7cbf93fd9",
|
||||
"https://bcr.bazel.build/modules/xds/0.0.0-20240423-555b57e/MODULE.bazel": "cea509976a77e34131411684ef05a1d6ad194dd71a8d5816643bc5b0af16dc0f",
|
||||
"https://bcr.bazel.build/modules/xds/0.0.0-20240423-555b57e/source.json": "7227e1fcad55f3f3cab1a08691ecd753cb29cc6380a47bc650851be9f9ad6d20",
|
||||
"https://bcr.bazel.build/modules/yq.bzl/0.1.1/MODULE.bazel": "9039681f9bcb8958ee2c87ffc74bdafba9f4369096a2b5634b88abc0eaefa072",
|
||||
"https://bcr.bazel.build/modules/yq.bzl/0.1.1/source.json": "2d2bad780a9f2b9195a4a370314d2c17ae95eaa745cefc2e12fbc49759b15aa3",
|
||||
"https://bcr.bazel.build/modules/zlib/1.2.11/MODULE.bazel": "07b389abc85fdbca459b69e2ec656ae5622873af3f845e1c9d80fe179f3effa0",
|
||||
"https://bcr.bazel.build/modules/zlib/1.2.12/MODULE.bazel": "3b1a8834ada2a883674be8cbd36ede1b6ec481477ada359cd2d3ddc562340b27",
|
||||
"https://bcr.bazel.build/modules/zlib/1.2.13/MODULE.bazel": "aa6deb1b83c18ffecd940c4119aff9567cd0a671d7bba756741cb2ef043a29d5",
|
||||
@@ -388,7 +413,7 @@
|
||||
},
|
||||
"@@aspect_rules_esbuild~//esbuild:extensions.bzl%esbuild": {
|
||||
"general": {
|
||||
"bzlTransitiveDigest": "8iOqbPY5ve3DvjzaI1mJZ8XTiJypN2PeWvcKOvmZLy8=",
|
||||
"bzlTransitiveDigest": "8jv3p0xDR/oitFeH8y0+Y5xlyrUbfsTRlc9TSwYkwl8=",
|
||||
"usagesDigest": "iDVoyPxUeADmfK8ssoyG3Ehq1bj6p7A43LpEiE266os=",
|
||||
"recordedFileInputs": {},
|
||||
"recordedDirentsInputs": {},
|
||||
@@ -1265,6 +1290,247 @@
|
||||
"recordedRepoMappingEntries": []
|
||||
}
|
||||
},
|
||||
"@@rules_oci~//oci:extensions.bzl%oci": {
|
||||
"general": {
|
||||
"bzlTransitiveDigest": "FaY+7xb13bB3hmxqwAWaGp3Tf3Q4Nfdlr+F38CP5mcg=",
|
||||
"usagesDigest": "BuciKSozbpJMD9EP+j0RG5ZgrYMeDPsQyiOnLUni2V8=",
|
||||
"recordedFileInputs": {},
|
||||
"recordedDirentsInputs": {},
|
||||
"envVariables": {},
|
||||
"generatedRepoSpecs": {
|
||||
"eclipse_temurin_17_linux_amd64": {
|
||||
"bzlFile": "@@rules_oci~//oci/private:pull.bzl",
|
||||
"ruleClassName": "oci_pull",
|
||||
"attributes": {
|
||||
"www_authenticate_challenges": {},
|
||||
"scheme": "https",
|
||||
"registry": "index.docker.io",
|
||||
"repository": "library/eclipse-temurin",
|
||||
"identifier": "sha256:d286b5352d98777bbf727f54038b04f0145cd9b76ca83f38a67aa111d4303748",
|
||||
"platform": "linux/amd64",
|
||||
"target_name": "eclipse_temurin_17_linux_amd64",
|
||||
"bazel_tags": []
|
||||
}
|
||||
},
|
||||
"eclipse_temurin_17": {
|
||||
"bzlFile": "@@rules_oci~//oci/private:pull.bzl",
|
||||
"ruleClassName": "oci_alias",
|
||||
"attributes": {
|
||||
"target_name": "eclipse_temurin_17",
|
||||
"www_authenticate_challenges": {},
|
||||
"scheme": "https",
|
||||
"registry": "index.docker.io",
|
||||
"repository": "library/eclipse-temurin",
|
||||
"identifier": "sha256:d286b5352d98777bbf727f54038b04f0145cd9b76ca83f38a67aa111d4303748",
|
||||
"platforms": {
|
||||
"@@platforms//cpu:x86_64": "@eclipse_temurin_17_linux_amd64"
|
||||
},
|
||||
"bzlmod_repository": "eclipse_temurin_17",
|
||||
"reproducible": true
|
||||
}
|
||||
},
|
||||
"ubuntu_24_04_linux_amd64": {
|
||||
"bzlFile": "@@rules_oci~//oci/private:pull.bzl",
|
||||
"ruleClassName": "oci_pull",
|
||||
"attributes": {
|
||||
"www_authenticate_challenges": {},
|
||||
"scheme": "https",
|
||||
"registry": "index.docker.io",
|
||||
"repository": "library/ubuntu",
|
||||
"identifier": "24.04",
|
||||
"platform": "linux/amd64",
|
||||
"target_name": "ubuntu_24_04_linux_amd64",
|
||||
"bazel_tags": []
|
||||
}
|
||||
},
|
||||
"ubuntu_24_04": {
|
||||
"bzlFile": "@@rules_oci~//oci/private:pull.bzl",
|
||||
"ruleClassName": "oci_alias",
|
||||
"attributes": {
|
||||
"target_name": "ubuntu_24_04",
|
||||
"www_authenticate_challenges": {},
|
||||
"scheme": "https",
|
||||
"registry": "index.docker.io",
|
||||
"repository": "library/ubuntu",
|
||||
"identifier": "24.04",
|
||||
"platforms": {
|
||||
"@@platforms//cpu:x86_64": "@ubuntu_24_04_linux_amd64"
|
||||
},
|
||||
"bzlmod_repository": "ubuntu_24_04",
|
||||
"reproducible": true
|
||||
}
|
||||
},
|
||||
"oci_crane_darwin_amd64": {
|
||||
"bzlFile": "@@rules_oci~//oci:repositories.bzl",
|
||||
"ruleClassName": "crane_repositories",
|
||||
"attributes": {
|
||||
"platform": "darwin_amd64",
|
||||
"crane_version": "v0.18.0"
|
||||
}
|
||||
},
|
||||
"oci_crane_darwin_arm64": {
|
||||
"bzlFile": "@@rules_oci~//oci:repositories.bzl",
|
||||
"ruleClassName": "crane_repositories",
|
||||
"attributes": {
|
||||
"platform": "darwin_arm64",
|
||||
"crane_version": "v0.18.0"
|
||||
}
|
||||
},
|
||||
"oci_crane_linux_arm64": {
|
||||
"bzlFile": "@@rules_oci~//oci:repositories.bzl",
|
||||
"ruleClassName": "crane_repositories",
|
||||
"attributes": {
|
||||
"platform": "linux_arm64",
|
||||
"crane_version": "v0.18.0"
|
||||
}
|
||||
},
|
||||
"oci_crane_linux_armv6": {
|
||||
"bzlFile": "@@rules_oci~//oci:repositories.bzl",
|
||||
"ruleClassName": "crane_repositories",
|
||||
"attributes": {
|
||||
"platform": "linux_armv6",
|
||||
"crane_version": "v0.18.0"
|
||||
}
|
||||
},
|
||||
"oci_crane_linux_i386": {
|
||||
"bzlFile": "@@rules_oci~//oci:repositories.bzl",
|
||||
"ruleClassName": "crane_repositories",
|
||||
"attributes": {
|
||||
"platform": "linux_i386",
|
||||
"crane_version": "v0.18.0"
|
||||
}
|
||||
},
|
||||
"oci_crane_linux_s390x": {
|
||||
"bzlFile": "@@rules_oci~//oci:repositories.bzl",
|
||||
"ruleClassName": "crane_repositories",
|
||||
"attributes": {
|
||||
"platform": "linux_s390x",
|
||||
"crane_version": "v0.18.0"
|
||||
}
|
||||
},
|
||||
"oci_crane_linux_amd64": {
|
||||
"bzlFile": "@@rules_oci~//oci:repositories.bzl",
|
||||
"ruleClassName": "crane_repositories",
|
||||
"attributes": {
|
||||
"platform": "linux_amd64",
|
||||
"crane_version": "v0.18.0"
|
||||
}
|
||||
},
|
||||
"oci_crane_windows_armv6": {
|
||||
"bzlFile": "@@rules_oci~//oci:repositories.bzl",
|
||||
"ruleClassName": "crane_repositories",
|
||||
"attributes": {
|
||||
"platform": "windows_armv6",
|
||||
"crane_version": "v0.18.0"
|
||||
}
|
||||
},
|
||||
"oci_crane_windows_amd64": {
|
||||
"bzlFile": "@@rules_oci~//oci:repositories.bzl",
|
||||
"ruleClassName": "crane_repositories",
|
||||
"attributes": {
|
||||
"platform": "windows_amd64",
|
||||
"crane_version": "v0.18.0"
|
||||
}
|
||||
},
|
||||
"oci_crane_toolchains": {
|
||||
"bzlFile": "@@rules_oci~//oci/private:toolchains_repo.bzl",
|
||||
"ruleClassName": "toolchains_repo",
|
||||
"attributes": {
|
||||
"toolchain_type": "@rules_oci//oci:crane_toolchain_type",
|
||||
"toolchain": "@oci_crane_{platform}//:crane_toolchain"
|
||||
}
|
||||
},
|
||||
"oci_regctl_darwin_amd64": {
|
||||
"bzlFile": "@@rules_oci~//oci:repositories.bzl",
|
||||
"ruleClassName": "regctl_repositories",
|
||||
"attributes": {
|
||||
"platform": "darwin_amd64"
|
||||
}
|
||||
},
|
||||
"oci_regctl_darwin_arm64": {
|
||||
"bzlFile": "@@rules_oci~//oci:repositories.bzl",
|
||||
"ruleClassName": "regctl_repositories",
|
||||
"attributes": {
|
||||
"platform": "darwin_arm64"
|
||||
}
|
||||
},
|
||||
"oci_regctl_linux_arm64": {
|
||||
"bzlFile": "@@rules_oci~//oci:repositories.bzl",
|
||||
"ruleClassName": "regctl_repositories",
|
||||
"attributes": {
|
||||
"platform": "linux_arm64"
|
||||
}
|
||||
},
|
||||
"oci_regctl_linux_s390x": {
|
||||
"bzlFile": "@@rules_oci~//oci:repositories.bzl",
|
||||
"ruleClassName": "regctl_repositories",
|
||||
"attributes": {
|
||||
"platform": "linux_s390x"
|
||||
}
|
||||
},
|
||||
"oci_regctl_linux_amd64": {
|
||||
"bzlFile": "@@rules_oci~//oci:repositories.bzl",
|
||||
"ruleClassName": "regctl_repositories",
|
||||
"attributes": {
|
||||
"platform": "linux_amd64"
|
||||
}
|
||||
},
|
||||
"oci_regctl_windows_amd64": {
|
||||
"bzlFile": "@@rules_oci~//oci:repositories.bzl",
|
||||
"ruleClassName": "regctl_repositories",
|
||||
"attributes": {
|
||||
"platform": "windows_amd64"
|
||||
}
|
||||
},
|
||||
"oci_regctl_toolchains": {
|
||||
"bzlFile": "@@rules_oci~//oci/private:toolchains_repo.bzl",
|
||||
"ruleClassName": "toolchains_repo",
|
||||
"attributes": {
|
||||
"toolchain_type": "@rules_oci//oci:regctl_toolchain_type",
|
||||
"toolchain": "@oci_regctl_{platform}//:regctl_toolchain"
|
||||
}
|
||||
}
|
||||
},
|
||||
"moduleExtensionMetadata": {
|
||||
"explicitRootModuleDirectDeps": [
|
||||
"eclipse_temurin_17",
|
||||
"eclipse_temurin_17_linux_amd64",
|
||||
"ubuntu_24_04",
|
||||
"ubuntu_24_04_linux_amd64"
|
||||
],
|
||||
"explicitRootModuleDirectDevDeps": [],
|
||||
"useAllRepos": "NO",
|
||||
"reproducible": false
|
||||
},
|
||||
"recordedRepoMappingEntries": [
|
||||
[
|
||||
"aspect_bazel_lib~",
|
||||
"bazel_tools",
|
||||
"bazel_tools"
|
||||
],
|
||||
[
|
||||
"bazel_features~",
|
||||
"bazel_tools",
|
||||
"bazel_tools"
|
||||
],
|
||||
[
|
||||
"rules_oci~",
|
||||
"aspect_bazel_lib",
|
||||
"aspect_bazel_lib~"
|
||||
],
|
||||
[
|
||||
"rules_oci~",
|
||||
"bazel_features",
|
||||
"bazel_features~"
|
||||
],
|
||||
[
|
||||
"rules_oci~",
|
||||
"bazel_skylib",
|
||||
"bazel_skylib~"
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"@@rules_scala~//scala/extensions:config.bzl%scala_config": {
|
||||
"general": {
|
||||
"bzlTransitiveDigest": "TdBxhkZTM7VU6teIFS+KoonKU7wmb5BL7leCWWx7yX8=",
|
||||
@@ -1293,7 +1559,7 @@
|
||||
},
|
||||
"@@rules_scala~//scala/extensions:deps.bzl%scala_deps": {
|
||||
"general": {
|
||||
"bzlTransitiveDigest": "F2PMm61fmZ/IE+VSw1rigJ71hBDD7k3vqyYR1/GgXeA=",
|
||||
"bzlTransitiveDigest": "5SDZrXQHW6tI/VEw+La2OPOK4ZWm0LGTxnChXOXBCag=",
|
||||
"usagesDigest": "kwo8oolISmSSITnit4b4S0vBiUtHlHK0WLDUwScxmOg=",
|
||||
"recordedFileInputs": {},
|
||||
"recordedDirentsInputs": {},
|
||||
@@ -4904,85 +5170,6 @@
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"@@toolchains_llvm~//toolchain/extensions:llvm.bzl%llvm": {
|
||||
"general": {
|
||||
"bzlTransitiveDigest": "afRF0aFOIUrkYl3o040WQ606ep1qciEXzjnAxT3Kek8=",
|
||||
"usagesDigest": "sYVuhiCAQehFTnGTv0bNtTBR4WorebpWBNxF0mRusyw=",
|
||||
"recordedFileInputs": {},
|
||||
"recordedDirentsInputs": {},
|
||||
"envVariables": {},
|
||||
"generatedRepoSpecs": {
|
||||
"llvm_toolchain_llvm": {
|
||||
"bzlFile": "@@toolchains_llvm~//toolchain:rules.bzl",
|
||||
"ruleClassName": "llvm",
|
||||
"attributes": {
|
||||
"alternative_llvm_sources": [],
|
||||
"auth_patterns": {},
|
||||
"distribution": "auto",
|
||||
"exec_arch": "",
|
||||
"exec_os": "",
|
||||
"libclang_rt": {},
|
||||
"llvm_mirror": "",
|
||||
"llvm_version": "20.1.2",
|
||||
"llvm_versions": {},
|
||||
"netrc": "",
|
||||
"sha256": {},
|
||||
"strip_prefix": {},
|
||||
"urls": {}
|
||||
}
|
||||
},
|
||||
"llvm_toolchain": {
|
||||
"bzlFile": "@@toolchains_llvm~//toolchain:rules.bzl",
|
||||
"ruleClassName": "toolchain",
|
||||
"attributes": {
|
||||
"absolute_paths": false,
|
||||
"archive_flags": {},
|
||||
"compile_flags": {},
|
||||
"conly_flags": {},
|
||||
"coverage_compile_flags": {},
|
||||
"coverage_link_flags": {},
|
||||
"cxx_builtin_include_directories": {},
|
||||
"cxx_flags": {},
|
||||
"cxx_standard": {},
|
||||
"dbg_compile_flags": {},
|
||||
"exec_arch": "",
|
||||
"exec_os": "",
|
||||
"extra_exec_compatible_with": {},
|
||||
"extra_target_compatible_with": {},
|
||||
"link_flags": {},
|
||||
"link_libs": {},
|
||||
"llvm_versions": {
|
||||
"": "20.1.2"
|
||||
},
|
||||
"opt_compile_flags": {},
|
||||
"opt_link_flags": {},
|
||||
"stdlib": {},
|
||||
"target_settings": {},
|
||||
"unfiltered_compile_flags": {},
|
||||
"toolchain_roots": {},
|
||||
"sysroot": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"recordedRepoMappingEntries": [
|
||||
[
|
||||
"toolchains_llvm~",
|
||||
"bazel_skylib",
|
||||
"bazel_skylib~"
|
||||
],
|
||||
[
|
||||
"toolchains_llvm~",
|
||||
"bazel_tools",
|
||||
"bazel_tools"
|
||||
],
|
||||
[
|
||||
"toolchains_llvm~",
|
||||
"toolchains_llvm",
|
||||
"toolchains_llvm~"
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
load("@rules_oci//oci:defs.bzl", "oci_image", "oci_load", "oci_push")
|
||||
load("@rules_pkg//pkg:tar.bzl", "pkg_tar")
|
||||
|
||||
#
|
||||
# Eagle Server Docker Image
|
||||
#
|
||||
# Build: bazel build //ci:eagle_server_image
|
||||
# Load: bazel run //ci:eagle_server_load
|
||||
# Push: bazel run //ci:eagle_server_push
|
||||
#
|
||||
|
||||
# Package the deploy JAR
|
||||
pkg_tar(
|
||||
name = "eagle_server_jar_layer",
|
||||
srcs = ["//src/main/scala/net/eagle0/eagle:eagle_server_deploy.jar"],
|
||||
package_dir = "/app",
|
||||
)
|
||||
|
||||
# Package the game resources needed at runtime
|
||||
pkg_tar(
|
||||
name = "eagle_resources_layer",
|
||||
srcs = [
|
||||
"//src/main/resources/net/eagle0/eagle:beasts",
|
||||
"//src/main/resources/net/eagle0/eagle:game_parameters",
|
||||
"//src/main/resources/net/eagle0/eagle:headshots",
|
||||
"//src/main/resources/net/eagle0/eagle:heroes",
|
||||
"//src/main/resources/net/eagle0/eagle:province_map",
|
||||
"//src/main/resources/net/eagle0/eagle:settings",
|
||||
],
|
||||
package_dir = "/app/resources",
|
||||
)
|
||||
|
||||
oci_image(
|
||||
name = "eagle_server_image",
|
||||
base = "@eclipse_temurin_17_linux_amd64",
|
||||
entrypoint = [
|
||||
"java",
|
||||
"-Xmx4g",
|
||||
"-XX:+UseG1GC",
|
||||
"-jar",
|
||||
"/app/eagle_server_deploy.jar",
|
||||
],
|
||||
env = {
|
||||
"JAVA_OPTS": "-Xmx4g -XX:+UseG1GC",
|
||||
},
|
||||
exposed_ports = ["40032/tcp"],
|
||||
tars = [
|
||||
":eagle_server_jar_layer",
|
||||
":eagle_resources_layer",
|
||||
],
|
||||
workdir = "/app",
|
||||
)
|
||||
|
||||
# Load into Docker locally: bazel run //ci:eagle_server_load
|
||||
oci_load(
|
||||
name = "eagle_server_load",
|
||||
image = ":eagle_server_image",
|
||||
repo_tags = ["eagle0/eagle-server:latest"],
|
||||
)
|
||||
|
||||
# Push to DigitalOcean Container Registry
|
||||
oci_push(
|
||||
name = "eagle_server_push",
|
||||
image = ":eagle_server_image",
|
||||
repository = "registry.digitalocean.com/eagle0/eagle-server",
|
||||
)
|
||||
|
||||
#
|
||||
# Shardok Server Docker Image
|
||||
#
|
||||
# Build: bazel build //ci:shardok_server_image
|
||||
# Load: bazel run //ci:shardok_server_load
|
||||
# Push: bazel run //ci:shardok_server_push
|
||||
#
|
||||
|
||||
# Package the Shardok binary
|
||||
pkg_tar(
|
||||
name = "shardok_binary_layer",
|
||||
srcs = ["//src/main/cpp/net/eagle0/shardok:shardok-server"],
|
||||
package_dir = "/app",
|
||||
)
|
||||
|
||||
# Package the Shardok resources (battalion types, settings)
|
||||
pkg_tar(
|
||||
name = "shardok_resources_layer",
|
||||
srcs = [
|
||||
"//src/main/resources/net/eagle0/shardok:battalion_types",
|
||||
"//src/main/resources/net/eagle0/shardok:settings",
|
||||
],
|
||||
package_dir = "/app/resources",
|
||||
)
|
||||
|
||||
# Package the converted maps
|
||||
pkg_tar(
|
||||
name = "shardok_maps_layer",
|
||||
srcs = ["//src/main/resources/net/eagle0/shardok/maps"],
|
||||
package_dir = "/app/resources/maps",
|
||||
)
|
||||
|
||||
oci_image(
|
||||
name = "shardok_server_image",
|
||||
base = "@ubuntu_24_04_linux_amd64",
|
||||
entrypoint = ["/app/shardok-server"],
|
||||
exposed_ports = [
|
||||
"40042/tcp",
|
||||
"40052/tcp",
|
||||
],
|
||||
tars = [
|
||||
":shardok_binary_layer",
|
||||
":shardok_resources_layer",
|
||||
":shardok_maps_layer",
|
||||
],
|
||||
workdir = "/app",
|
||||
)
|
||||
|
||||
# Load into Docker locally: bazel run //ci:shardok_server_load
|
||||
oci_load(
|
||||
name = "shardok_server_load",
|
||||
image = ":shardok_server_image",
|
||||
repo_tags = ["eagle0/shardok-server:latest"],
|
||||
)
|
||||
|
||||
# Push to DigitalOcean Container Registry
|
||||
oci_push(
|
||||
name = "shardok_server_push",
|
||||
image = ":shardok_server_image",
|
||||
repository = "registry.digitalocean.com/eagle0/shardok-server",
|
||||
)
|
||||
@@ -0,0 +1,47 @@
|
||||
# Docker Compose for local testing of production images
|
||||
# Build images: bazel run //ci:eagle_server_load && bazel run //ci:shardok_server_load
|
||||
# Run: docker compose -f docker-compose.prod.yml up
|
||||
|
||||
services:
|
||||
eagle:
|
||||
image: eagle0/eagle-server:latest
|
||||
container_name: eagle-server
|
||||
ports:
|
||||
- "40032:40032"
|
||||
environment:
|
||||
# Eagle server configuration
|
||||
EAGLE_GRPC_PORT: "40032"
|
||||
SHARDOK_HOST: "shardok"
|
||||
SHARDOK_PORT: "40042"
|
||||
# Resource paths (relative to /app in container)
|
||||
EAGLE_RESOURCES_PATH: "/app/resources"
|
||||
volumes:
|
||||
# Mount saves directory for persistence
|
||||
- ./saves:/app/saves
|
||||
depends_on:
|
||||
- shardok
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "nc", "-z", "localhost", "40032"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
|
||||
shardok:
|
||||
image: eagle0/shardok-server:latest
|
||||
container_name: shardok-server
|
||||
ports:
|
||||
- "40042:40042"
|
||||
- "40052:40052"
|
||||
environment:
|
||||
# Shardok server configuration
|
||||
SHARDOK_RESOURCES_PATH: "/app/resources"
|
||||
SHARDOK_MAPS_PATH: "/app/resources/maps"
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "nc", "-z", "localhost", "40042"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
+59
-31
@@ -197,34 +197,57 @@ Change APIs to vend Scala `GameState` and `ActionResultT` instead of proto versi
|
||||
|
||||
### Remaining Proto Usage in Actions
|
||||
|
||||
The following actions still have proto usage, blocked by utility dependencies:
|
||||
**Progress: 47 of 52 action files (90%) are fully protoless.**
|
||||
|
||||
| Action | Proto Usage | Blocker |
|
||||
|--------|-------------|---------|
|
||||
| `EndBattleAftermathPhaseAction` | 1 `toProto` call | `ProvinceViewFilter` needs Scala types |
|
||||
| `NewRoundAction` | 1 `fromProto` call | `ChronicleEventGenerator` returns proto |
|
||||
| `EndHandleRiotsPhaseAction` | 1 `toProto` call | `CommandChoiceHelpers` takes proto GameState |
|
||||
| `PerformVassalCommandsPhaseAction` | 1 `toProto` call | `CommandChoiceHelpers` takes proto GameState |
|
||||
| `PerformVassalDefenseDecisionsAction` | 1 `toProto` call | `CommandChoiceHelpers` takes proto GameState |
|
||||
| `EndVassalCommandsPhaseAction` | 1 `toProto` call | `CommandChoiceHelpers` takes proto GameState |
|
||||
| `PerformReconResolutionAction` | Uses `ProvinceViewFilter` | `ProvinceViewFilter` needs Scala types |
|
||||
| `ResolveBattleAction` | Heavy proto usage | Large refactor needed |
|
||||
The following 5 actions still have proto usage:
|
||||
|
||||
| Action | Proto Usages | Blocker | Effort |
|
||||
|--------|--------------|---------|--------|
|
||||
| `ResolveBattleAction` | 24 | Shardok interface, complex battle logic | High |
|
||||
| `PerformVassalCommandsPhaseAction` | 3 | `CommandChoiceHelpers` takes proto GameState | Medium |
|
||||
| `EndHandleRiotsPhaseAction` | 2 | `CommandChoiceHelpers` takes proto GameState | Medium |
|
||||
| `PerformVassalDefenseDecisionsAction` | 2 | `CommandChoiceHelpers` takes proto GameState | Medium |
|
||||
| `EndVassalCommandsPhaseAction` | 1 | `CommandChoiceHelpers` takes proto GameState | Medium |
|
||||
|
||||
**Note:** `NewRoundAction` is now fully protoless after converting `ChronicleEventGenerator` to return Scala `ChronicleEvent` types directly.
|
||||
|
||||
**Deleted Dead Code:**
|
||||
- `UnaffiliatedHeroMovedAction` - Was never called; `PerformUnaffiliatedHeroesAction.heroMovedResult` constructs `ActionResultC` directly
|
||||
- `HeroBackstoryUpdateActionGenerator.fromGameState` - Dead method that converted proto to Scala; only `apply(GameState)` is used
|
||||
|
||||
**Note**: `PerformReconResolutionAction` and `EndBattleAftermathPhaseAction` are now fully protoless after:
|
||||
1. Migrating `FactionT.reconnedProvinces` and `ChangedFactionC.updatedReconnedProvinces` to use Scala `ProvinceView`
|
||||
2. Adding Scala overload of `ProvinceViewFilter.withdrawnFromProvinceView`
|
||||
|
||||
### Estimated Effort (Remaining)
|
||||
|
||||
| Component | Lines | Complexity |
|
||||
|-----------|-------|------------|
|
||||
| `ProvinceViewFilter` to Scala | ~150 | Medium |
|
||||
| `CommandChoiceHelpers` to Scala | ~2000 | High |
|
||||
| `ChronicleEventGenerator` to Scala | ~400 | Medium |
|
||||
| History API updates | ~100 | Low |
|
||||
| **Total Remaining** | **~2650** | |
|
||||
| Component | Lines | Complexity | Blocks |
|
||||
|-----------|-------|------------|--------|
|
||||
| `CommandChoiceHelpers` to Scala | ~2000 | High | 4 vassal actions |
|
||||
| `ResolveBattleAction` refactor | ~500 | High | 1 action (complex) |
|
||||
| History API updates | ~100 | Low | - |
|
||||
| **Total Remaining** | **~2600** | | |
|
||||
|
||||
**Completed:**
|
||||
- `ChronicleEventGenerator` converted to return Scala `ChronicleEvent` types directly
|
||||
|
||||
### Progress Summary
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Action files fully protoless | 47 / 52 (90%) |
|
||||
| Proto usages in remaining actions | 32 total |
|
||||
| Biggest blocker | `ResolveBattleAction` (24 usages) |
|
||||
| Second biggest blocker | `CommandChoiceHelpers` (blocks 4 actions) |
|
||||
|
||||
### Validation
|
||||
- [x] `ActionResultApplier` created and tested
|
||||
- [x] `RandomStateSequencer` threads Scala GameState throughout
|
||||
- [x] `RoundPhaseAdvancer` uses T-types internally
|
||||
- [ ] `ProvinceViewFilter` uses Scala types
|
||||
- [x] `ProvinceViewFilter` has Scala overload for server-side use (PR #4752)
|
||||
- [x] `FactionT.reconnedProvinces` and `ChangedFactionC.updatedReconnedProvinces` use Scala `ProvinceView`
|
||||
- [x] `ProvinceViewFilter.withdrawnFromProvinceView` has Scala overload
|
||||
- [ ] `ProvinceViewFilter` faction-filtered views use Scala types
|
||||
- [ ] `CommandChoiceHelpers` uses Scala types
|
||||
- [ ] History APIs vend Scala types
|
||||
- [ ] No `ActionResultProtoConverter.toProto()` calls except at persistence/gRPC boundaries
|
||||
@@ -248,22 +271,27 @@ Remove remaining direct proto imports from utility classes.
|
||||
| `BattalionTypeLoader.scala` | Keep proto for file loading, convert immediately after |
|
||||
| `BeastUtils.scala` | **Complete** - now uses Scala `BeastInfo` only |
|
||||
|
||||
### View Filters (Blocking Full Deproto)
|
||||
### View Filters (Partially Complete)
|
||||
|
||||
The `ProvinceViewFilter` utility currently works entirely with proto types, blocking full deproto of actions that generate province views:
|
||||
The view filter utilities now have Scala overloads for server-side use:
|
||||
|
||||
| File | Issue | Needed |
|
||||
|------|-------|--------|
|
||||
| `ProvinceViewFilter.scala` | Takes proto `Province`/`GameState`, returns proto `ProvinceView` | Scala `ProvinceViewT` model |
|
||||
| `GameStateViewFilter.scala` | Uses proto types throughout | Depends on `ProvinceViewT` |
|
||||
| `GameStateViewDiffer.scala` | Works with view protos | Depends on `ProvinceViewT` |
|
||||
| File | Status | Notes |
|
||||
|------|--------|-------|
|
||||
| `ProvinceViewFilter.scala` | **Partial** | `filteredProvinceView(ProvinceT, ScalaGameState)` added (PR #4752) |
|
||||
| `ArmyFilter.scala` | **Partial** | `filterArmy(ScalaArmy, Map[BattalionId, BattalionT], Option[FactionId])` added |
|
||||
| `BattalionViewFilter.scala` | **Complete** | Uses Scala `BattalionT` throughout |
|
||||
| `GameStateViewFilter.scala` | Pending | Uses proto types throughout |
|
||||
| `GameStateViewDiffer.scala` | Pending | Works with view protos |
|
||||
|
||||
**Blocked Actions**:
|
||||
- `EndBattleAftermathPhaseAction` - uses `ProvinceViewFilter` for `revelationChange`, requires lazy proto conversion
|
||||
- `PerformReconResolutionAction` - uses `ProvinceViewFilter` for reconned provinces
|
||||
- `GameStateFactionExtensions` - uses `ProvinceViewFilter` for `updatedReconnedProvinces`
|
||||
**Unblocked Actions** (PR #4752):
|
||||
- `EndBattleAftermathPhaseAction` - can now use `filteredProvinceView(province, scalaGameState)`
|
||||
- `PerformReconResolutionAction` - can now use Scala overload
|
||||
- `GameStateFactionExtensions` - can now use `updatedReconnedProvinces` with Scala types
|
||||
|
||||
**Solution**: Create Scala `ProvinceViewT` (and possibly `ProvinceViewC`) that mirrors the proto `ProvinceView`. Then create a protoless `ProvinceViewFilter` that operates on Scala types. The proto version can delegate to the Scala version + convert, or we maintain both during transition.
|
||||
**Remaining Work**:
|
||||
- Faction-filtered `filteredProvinceView(Province, GameState, FactionId)` still uses proto types
|
||||
- `withdrawnFromProvinceView` still uses proto types
|
||||
- These are needed for client-facing views with visibility restrictions
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,940 @@
|
||||
# Eagle0 Productionization Plan
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This document outlines a plan to move Eagle0's Eagle and Shardok servers from a home Mac to cloud infrastructure while maintaining a QA environment on the Mac. The architecture uses DigitalOcean (already integrated via Spaces) with containerized deployments and GitHub Actions CI/CD.
|
||||
|
||||
## Current Architecture
|
||||
|
||||
```
|
||||
Unity Client
|
||||
│
|
||||
│ gRPC/TLS (eagle0.net:443)
|
||||
▼
|
||||
nginx (home Mac, via router port forward)
|
||||
│
|
||||
├─► Eagle Server (Scala/JVM, port 40032)
|
||||
│ │
|
||||
│ │ internal gRPC (port 40042)
|
||||
│ ▼
|
||||
└─► Shardok Server (C++, port 40042/40052)
|
||||
|
||||
Storage: DigitalOcean Spaces (sfo3.digitaloceanspaces.com)
|
||||
DNS: eagle0.net → home IP
|
||||
```
|
||||
|
||||
**Key observations:**
|
||||
- Already using DigitalOcean Spaces for S3-compatible storage
|
||||
- Eagle server is a deployable JAR (`eagle_server_deploy.jar`)
|
||||
- Shardok server is a native C++ binary
|
||||
- nginx handles TLS termination and gRPC routing
|
||||
- Self-hosted GitHub Actions runner on Mac
|
||||
|
||||
---
|
||||
|
||||
## Target Architecture
|
||||
|
||||
### Production Environment (DigitalOcean)
|
||||
|
||||
```
|
||||
Unity Client
|
||||
│
|
||||
├─► eagle0.net (Production)
|
||||
│ │
|
||||
│ ▼
|
||||
│ DigitalOcean Load Balancer (TLS termination)
|
||||
│ │
|
||||
│ ▼
|
||||
│ ┌─────────────────────────────────────┐
|
||||
│ │ DigitalOcean Droplet(s) │
|
||||
│ │ ┌─────────────┬─────────────────┐ │
|
||||
│ │ │ Eagle │ Shardok │ │
|
||||
│ │ │ (Docker) │ (Docker) │ │
|
||||
│ │ │ :40032 │ :40042 │ │
|
||||
│ │ └─────────────┴─────────────────┘ │
|
||||
│ └─────────────────────────────────────┘
|
||||
│
|
||||
└─► qa.eagle0.net (QA - home Mac, unchanged)
|
||||
│
|
||||
▼
|
||||
nginx → Eagle/Shardok (current setup)
|
||||
```
|
||||
|
||||
### Environment Switching
|
||||
|
||||
The Unity client already supports configurable server URLs via the connection screen:
|
||||
- **Production:** `eagle0.net` (default)
|
||||
- **QA:** `qa.eagle0.net`
|
||||
|
||||
No client code changes needed - users can simply type the desired URL.
|
||||
|
||||
---
|
||||
|
||||
## Cloud Provider: DigitalOcean
|
||||
|
||||
### Why DigitalOcean
|
||||
|
||||
1. **Already integrated** - S3 Spaces storage at `sfo3.digitaloceanspaces.com` with credentials configured
|
||||
2. **Simple pricing** - Predictable monthly costs, no surprise bills
|
||||
3. **Good performance** - SFO3 datacenter is geographically close
|
||||
4. **Managed services** - Load balancers, managed databases if needed later
|
||||
5. **Not AWS/GCP** - Per your requirements
|
||||
|
||||
### Alternative Considered: Hetzner
|
||||
|
||||
- Cheaper for compute ($3.29/mo for 2 vCPU/4GB vs DO's $24/mo)
|
||||
- Good EU presence but less US coverage
|
||||
- No managed load balancer (need to run HAProxy/nginx yourself)
|
||||
- **Recommendation:** Start with DigitalOcean for simplicity; migrate to Hetzner later if cost becomes a concern
|
||||
|
||||
---
|
||||
|
||||
## Infrastructure Components
|
||||
|
||||
### 1. Compute: DigitalOcean Droplets
|
||||
|
||||
#### Resource Characteristics
|
||||
|
||||
- **Eagle server:** CPU-light, possibly RAM-heavy (JVM). Should be always available.
|
||||
- **Shardok server:** Very CPU-heavy (AI algorithms). Only needed during tactical battles. Startup time <1 second.
|
||||
|
||||
#### Recommended: On-Demand Shardok (Same Droplet)
|
||||
|
||||
For current low player count, optimize for cost while maintaining availability:
|
||||
|
||||
| Component | Config | Monthly Cost |
|
||||
|-----------|--------|--------------|
|
||||
| Droplet | s-2vcpu-4gb | $24/mo |
|
||||
| Eagle | Always running | - |
|
||||
| Shardok | Started on-demand by Eagle, stopped after idle | - |
|
||||
|
||||
**How it works:**
|
||||
1. Eagle server runs 24/7 - game is always available
|
||||
2. When battle starts, Eagle launches Shardok container/process (<1s startup)
|
||||
3. After battle ends, idle timer starts (e.g., 5 minutes)
|
||||
4. On timeout, Eagle stops Shardok to free CPU
|
||||
5. If new battle starts during idle period, Shardok is already warm
|
||||
|
||||
**Shardok lifecycle management** (implement in Eagle):
|
||||
```scala
|
||||
// Pseudocode for Eagle's Shardok management
|
||||
object ShardokManager {
|
||||
private var process: Option[Process] = None
|
||||
private var idleTimer: Option[Timer] = None
|
||||
|
||||
def ensureRunning(): Unit = {
|
||||
cancelIdleTimer()
|
||||
if (process.isEmpty) {
|
||||
process = Some(startShardokProcess())
|
||||
waitForHealthCheck()
|
||||
}
|
||||
}
|
||||
|
||||
def onBattleEnd(): Unit = {
|
||||
idleTimer = Some(scheduleShutdown(5.minutes))
|
||||
}
|
||||
|
||||
private def shutdown(): Unit = {
|
||||
process.foreach(_.destroy())
|
||||
process = None
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Future Scaling Options
|
||||
|
||||
As player count grows and Shardok needs more CPU:
|
||||
|
||||
**Option A: Upgrade Droplet (Simplest)**
|
||||
|
||||
| Droplet | vCPU | RAM | Cost | Use Case |
|
||||
|---------|------|-----|------|----------|
|
||||
| s-2vcpu-4gb | 2 shared | 4 GB | $24/mo | Current (few players) |
|
||||
| s-4vcpu-8gb | 4 shared | 8 GB | $48/mo | Moderate usage |
|
||||
| c-4 | 4 dedicated | 8 GB | $84/mo | CPU-intensive battles |
|
||||
| c-8 | 8 dedicated | 16 GB | $168/mo | Multiple concurrent battles |
|
||||
|
||||
**Option B: Separate Shardok Droplet (API-driven)**
|
||||
|
||||
For heavy Shardok usage with cost optimization:
|
||||
|
||||
- **Eagle droplet:** s-1vcpu-2gb ($12/mo) - always on
|
||||
- **Shardok droplet:** Created on-demand via DigitalOcean API
|
||||
- c-4 CPU-optimized: $0.125/hour
|
||||
- c-8 CPU-optimized: $0.25/hour
|
||||
- Created when battle starts, destroyed after idle
|
||||
- 30-60s droplet creation time (acceptable if battles are requested in advance)
|
||||
|
||||
**Option C: Fly.io for Shardok (Scale-to-Zero)**
|
||||
|
||||
For true pay-per-use with fast cold starts:
|
||||
|
||||
- **Eagle:** DigitalOcean or Fly.io (~$10/mo)
|
||||
- **Shardok:** Fly.io with auto-scaling
|
||||
- Performance VMs: ~$0.0000022/second when running
|
||||
- Cold start: 2-5 seconds
|
||||
- Scales to zero when idle
|
||||
|
||||
Requires learning Fly.io but offers best cost efficiency for sporadic usage.
|
||||
|
||||
**Option D: Multiple Shardok Instances (High Scale)**
|
||||
|
||||
For many concurrent battles:
|
||||
|
||||
- **Eagle:** Dedicated droplet with more RAM
|
||||
- **Shardok pool:** Multiple Shardok containers/droplets
|
||||
- Eagle routes battles to available Shardok instances
|
||||
- Could use Kubernetes or Docker Swarm for orchestration
|
||||
|
||||
This is overkill for now but documented for future reference.
|
||||
|
||||
### 2. Load Balancer
|
||||
|
||||
**DigitalOcean Load Balancer:** $12/mo
|
||||
|
||||
- TLS termination with Let's Encrypt
|
||||
- Health checks
|
||||
- Sticky sessions (if needed)
|
||||
- Can add more droplets later for HA
|
||||
|
||||
**Alternative:** Run nginx on the droplet for $0 extra, but lose automatic failover.
|
||||
|
||||
### 3. Storage
|
||||
|
||||
**Already configured:** DigitalOcean Spaces
|
||||
- Bucket: `eagle0`
|
||||
- Region: `sfo3`
|
||||
- Used for game saves and assets
|
||||
- Cost: $5/mo base + $0.02/GB storage + $0.01/GB transfer
|
||||
|
||||
### 4. DNS
|
||||
|
||||
**Option A: DigitalOcean DNS (Recommended)**
|
||||
- Free with droplets
|
||||
- Easy integration
|
||||
- API for automated updates
|
||||
|
||||
**Option B: Keep current DNS provider**
|
||||
- Update A records manually or via script
|
||||
|
||||
**DNS Records:**
|
||||
```
|
||||
eagle0.net A <DO Load Balancer IP>
|
||||
qa.eagle0.net A <Home IP> (unchanged)
|
||||
*.eagle0.net A <DO Load Balancer IP> (wildcard for future)
|
||||
```
|
||||
|
||||
### 5. Firewall
|
||||
|
||||
**DigitalOcean Cloud Firewall:** Free
|
||||
|
||||
```
|
||||
Inbound Rules:
|
||||
- TCP 443 (HTTPS/gRPC) from anywhere → Load Balancer
|
||||
- TCP 22 (SSH) from your IP only → Droplets
|
||||
- TCP 40032 (Eagle) from Load Balancer only
|
||||
- TCP 40042 (Shardok) from Load Balancer only
|
||||
|
||||
Outbound Rules:
|
||||
- All traffic allowed (for external APIs, Spaces, etc.)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Container Strategy
|
||||
|
||||
### Dockerfiles
|
||||
|
||||
**Eagle Server** (`ci/eagle_run.Dockerfile` - already exists, needs enhancement):
|
||||
|
||||
```dockerfile
|
||||
FROM eclipse-temurin:17-jre-alpine
|
||||
|
||||
# Add non-root user
|
||||
RUN addgroup -S eagle && adduser -S eagle -G eagle
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy the deploy JAR
|
||||
COPY --chown=eagle:eagle deploy/eagle_server_deploy.jar ./
|
||||
|
||||
# Copy game resources
|
||||
COPY --chown=eagle:eagle src/main/resources/net/eagle0/eagle/ ./resources/
|
||||
|
||||
USER eagle
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
|
||||
CMD wget -q --spider http://localhost:40032/health || exit 1
|
||||
|
||||
EXPOSE 40032
|
||||
|
||||
ENTRYPOINT ["java", "-Xmx4g", "-jar", "eagle_server_deploy.jar"]
|
||||
CMD ["--eagle-grpc-port", "40032", "--shardok-interface-remote-address", "shardok:40042", "--gpt-model-name", "gpt-4"]
|
||||
```
|
||||
|
||||
**Shardok Server** (new: `ci/shardok_run.Dockerfile`):
|
||||
|
||||
```dockerfile
|
||||
FROM ubuntu:22.04
|
||||
|
||||
# Install runtime dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
libstdc++6 \
|
||||
ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Add non-root user
|
||||
RUN useradd -r -s /bin/false shardok
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy the Shardok binary and dependencies
|
||||
COPY --chown=shardok:shardok bazel-bin/src/main/cpp/net/eagle0/shardok/shardok-server ./
|
||||
COPY --chown=shardok:shardok src/main/resources/net/eagle0/shardok/maps/ ./maps/
|
||||
|
||||
USER shardok
|
||||
|
||||
# Health check (need to implement gRPC health endpoint)
|
||||
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
|
||||
CMD ./shardok-server --health-check || exit 1
|
||||
|
||||
EXPOSE 40042 40052
|
||||
|
||||
ENTRYPOINT ["./shardok-server"]
|
||||
```
|
||||
|
||||
**Docker Compose** (new: `docker-compose.prod.yml`):
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
eagle:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ci/eagle_run.Dockerfile
|
||||
image: eagle0/eagle-server:${VERSION:-latest}
|
||||
ports:
|
||||
- "40032:40032"
|
||||
environment:
|
||||
- SHARDOK_ADDRESS=shardok:40042
|
||||
- GPT_MODEL_NAME=${GPT_MODEL_NAME:-gpt-4}
|
||||
- JAVA_OPTS=-Xmx4g -XX:+UseG1GC
|
||||
depends_on:
|
||||
- shardok
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "100m"
|
||||
max-file: "5"
|
||||
|
||||
shardok:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ci/shardok_run.Dockerfile
|
||||
image: eagle0/shardok-server:${VERSION:-latest}
|
||||
ports:
|
||||
- "40042:40042"
|
||||
- "40052:40052"
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "100m"
|
||||
max-file: "5"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Build Pipeline
|
||||
|
||||
### Build Artifacts
|
||||
|
||||
The build process produces:
|
||||
1. **Eagle JAR:** `bazel-bin/src/main/scala/net/eagle0/eagle/eagle_server_deploy.jar`
|
||||
2. **Shardok binary:** `bazel-bin/src/main/cpp/net/eagle0/shardok/shardok-server`
|
||||
|
||||
### Cross-Compilation for Linux
|
||||
|
||||
Current builds target macOS (self-hosted runner). For production Linux deployment:
|
||||
|
||||
**Option A: Build in Docker (Recommended)**
|
||||
```bash
|
||||
# Build Eagle (JVM - platform independent)
|
||||
bazel build //src/main/scala/net/eagle0/eagle:eagle_server_deploy.jar
|
||||
|
||||
# Build Shardok in Linux container
|
||||
docker run --rm -v $(pwd):/workspace -w /workspace \
|
||||
ubuntu:22.04 \
|
||||
bash -c "apt-get update && apt-get install -y build-essential && bazel build -c opt //src/main/cpp/net/eagle0/shardok:shardok-server"
|
||||
```
|
||||
|
||||
**Option B: Use GitHub-hosted Linux runner**
|
||||
- Add `runs-on: ubuntu-latest` workflow
|
||||
- Builds directly on Linux
|
||||
- May need to cache Bazel to avoid long build times
|
||||
|
||||
**Option C: Cross-compile on Mac**
|
||||
- Configure Bazel for Linux cross-compilation
|
||||
- More complex setup but faster iteration
|
||||
|
||||
**Recommendation:** Option A (Docker build) for Shardok, since Eagle's JAR is platform-independent.
|
||||
|
||||
---
|
||||
|
||||
## CI/CD Pipeline
|
||||
|
||||
### GitHub Actions Workflows
|
||||
|
||||
**New workflow:** `.github/workflows/deploy_production.yml`
|
||||
|
||||
```yaml
|
||||
name: Deploy to Production
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'src/main/scala/**'
|
||||
- 'src/main/cpp/**'
|
||||
- 'src/main/protobuf/**'
|
||||
- 'ci/*.Dockerfile'
|
||||
- 'docker-compose.prod.yml'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
environment:
|
||||
description: 'Deployment environment'
|
||||
required: true
|
||||
default: 'production'
|
||||
type: choice
|
||||
options:
|
||||
- production
|
||||
- staging
|
||||
|
||||
env:
|
||||
REGISTRY: registry.digitalocean.com
|
||||
EAGLE_IMAGE: eagle0/eagle-server
|
||||
SHARDOK_IMAGE: eagle0/shardok-server
|
||||
|
||||
jobs:
|
||||
build-eagle:
|
||||
runs-on: self-hosted
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: false
|
||||
|
||||
- name: Set version
|
||||
id: version
|
||||
run: echo "version=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build Eagle server JAR
|
||||
run: bazel build //src/main/scala/net/eagle0/eagle:eagle_server_deploy.jar
|
||||
|
||||
- name: Copy artifacts
|
||||
run: |
|
||||
mkdir -p deploy
|
||||
cp bazel-bin/src/main/scala/net/eagle0/eagle/eagle_server_deploy.jar deploy/
|
||||
|
||||
- name: Build Docker image
|
||||
run: |
|
||||
docker build -f ci/eagle_run.Dockerfile -t ${{ env.REGISTRY }}/${{ env.EAGLE_IMAGE }}:${{ steps.version.outputs.version }} .
|
||||
docker tag ${{ env.REGISTRY }}/${{ env.EAGLE_IMAGE }}:${{ steps.version.outputs.version }} ${{ env.REGISTRY }}/${{ env.EAGLE_IMAGE }}:latest
|
||||
|
||||
- name: Push to registry
|
||||
run: |
|
||||
echo "${{ secrets.DO_REGISTRY_TOKEN }}" | docker login ${{ env.REGISTRY }} -u ${{ secrets.DO_REGISTRY_TOKEN }} --password-stdin
|
||||
docker push ${{ env.REGISTRY }}/${{ env.EAGLE_IMAGE }}:${{ steps.version.outputs.version }}
|
||||
docker push ${{ env.REGISTRY }}/${{ env.EAGLE_IMAGE }}:latest
|
||||
|
||||
build-shardok:
|
||||
runs-on: ubuntu-latest
|
||||
needs: []
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: false
|
||||
|
||||
- name: Set version
|
||||
id: version
|
||||
run: echo "version=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set up Bazel
|
||||
uses: bazelbuild/setup-bazelisk@v2
|
||||
|
||||
- name: Cache Bazel
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.cache/bazel
|
||||
key: bazel-linux-${{ hashFiles('MODULE.bazel', 'WORKSPACE') }}
|
||||
|
||||
- name: Build Shardok server
|
||||
run: bazel build -c opt //src/main/cpp/net/eagle0/shardok:shardok-server
|
||||
|
||||
- name: Build Docker image
|
||||
run: |
|
||||
mkdir -p bazel-bin/src/main/cpp/net/eagle0/shardok/
|
||||
cp bazel-bin/src/main/cpp/net/eagle0/shardok/shardok-server bazel-bin/src/main/cpp/net/eagle0/shardok/
|
||||
docker build -f ci/shardok_run.Dockerfile -t ${{ env.REGISTRY }}/${{ env.SHARDOK_IMAGE }}:${{ steps.version.outputs.version }} .
|
||||
docker tag ${{ env.REGISTRY }}/${{ env.SHARDOK_IMAGE }}:${{ steps.version.outputs.version }} ${{ env.REGISTRY }}/${{ env.SHARDOK_IMAGE }}:latest
|
||||
|
||||
- name: Push to registry
|
||||
run: |
|
||||
echo "${{ secrets.DO_REGISTRY_TOKEN }}" | docker login ${{ env.REGISTRY }} -u ${{ secrets.DO_REGISTRY_TOKEN }} --password-stdin
|
||||
docker push ${{ env.REGISTRY }}/${{ env.SHARDOK_IMAGE }}:${{ steps.version.outputs.version }}
|
||||
docker push ${{ env.REGISTRY }}/${{ env.SHARDOK_IMAGE }}:latest
|
||||
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-eagle, build-shardok]
|
||||
environment: production
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Deploy to DigitalOcean
|
||||
uses: appleboy/ssh-action@v1.0.0
|
||||
with:
|
||||
host: ${{ secrets.DO_DROPLET_IP }}
|
||||
username: deploy
|
||||
key: ${{ secrets.DO_SSH_KEY }}
|
||||
script: |
|
||||
cd /opt/eagle0
|
||||
|
||||
# Pull latest images
|
||||
docker-compose -f docker-compose.prod.yml pull
|
||||
|
||||
# Rolling restart (zero-downtime if load balancer configured)
|
||||
docker-compose -f docker-compose.prod.yml up -d --remove-orphans
|
||||
|
||||
# Wait for health checks
|
||||
sleep 30
|
||||
docker-compose -f docker-compose.prod.yml ps
|
||||
|
||||
# Cleanup old images
|
||||
docker image prune -f
|
||||
|
||||
- name: Verify deployment
|
||||
run: |
|
||||
# Health check endpoint
|
||||
curl -f https://eagle0.net/health || exit 1
|
||||
|
||||
- name: Notify on failure
|
||||
if: failure()
|
||||
run: |
|
||||
# Add Slack/Discord notification here
|
||||
echo "Deployment failed!"
|
||||
```
|
||||
|
||||
### Deployment Steps
|
||||
|
||||
1. **On push to main:**
|
||||
- Build Eagle JAR (self-hosted Mac runner - for consistency)
|
||||
- Build Shardok binary (GitHub-hosted Ubuntu runner)
|
||||
- Build Docker images
|
||||
- Push to DigitalOcean Container Registry
|
||||
|
||||
2. **Deploy to droplet:**
|
||||
- SSH to production server
|
||||
- Pull new images
|
||||
- `docker-compose up -d` (rolling update)
|
||||
- Verify health checks
|
||||
|
||||
3. **Rollback:**
|
||||
```bash
|
||||
# On server
|
||||
docker-compose -f docker-compose.prod.yml down
|
||||
docker-compose -f docker-compose.prod.yml pull eagle0/eagle-server:<previous-version>
|
||||
docker-compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Server Setup
|
||||
|
||||
### Initial Droplet Setup
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Run on fresh DigitalOcean droplet (Ubuntu 22.04)
|
||||
|
||||
# Update system
|
||||
apt-get update && apt-get upgrade -y
|
||||
|
||||
# Install Docker
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
systemctl enable docker
|
||||
systemctl start docker
|
||||
|
||||
# Install Docker Compose
|
||||
apt-get install -y docker-compose-plugin
|
||||
|
||||
# Create deploy user
|
||||
useradd -m -s /bin/bash -G docker deploy
|
||||
mkdir -p /home/deploy/.ssh
|
||||
# Add your SSH public key to /home/deploy/.ssh/authorized_keys
|
||||
|
||||
# Create app directory
|
||||
mkdir -p /opt/eagle0
|
||||
chown deploy:deploy /opt/eagle0
|
||||
|
||||
# Configure Docker to use DigitalOcean Container Registry
|
||||
docker login registry.digitalocean.com
|
||||
|
||||
# Create systemd service for auto-start
|
||||
cat > /etc/systemd/system/eagle0.service << 'EOF'
|
||||
[Unit]
|
||||
Description=Eagle0 Game Servers
|
||||
Requires=docker.service
|
||||
After=docker.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
WorkingDirectory=/opt/eagle0
|
||||
ExecStart=/usr/bin/docker compose -f docker-compose.prod.yml up -d
|
||||
ExecStop=/usr/bin/docker compose -f docker-compose.prod.yml down
|
||||
User=deploy
|
||||
Group=deploy
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
systemctl enable eagle0
|
||||
```
|
||||
|
||||
### Load Balancer Configuration
|
||||
|
||||
**DigitalOcean Load Balancer settings:**
|
||||
|
||||
- **Forwarding Rules:**
|
||||
- HTTPS 443 → HTTP 40032 (Eagle)
|
||||
- gRPC is HTTP/2, handled automatically
|
||||
|
||||
- **Health Checks:**
|
||||
- Protocol: HTTP
|
||||
- Port: 40032
|
||||
- Path: `/health` (need to implement)
|
||||
|
||||
- **SSL:**
|
||||
- Let's Encrypt certificate for `eagle0.net`
|
||||
|
||||
- **Settings:**
|
||||
- Sticky sessions: Disabled (gRPC streams handle this)
|
||||
- Proxy protocol: Disabled
|
||||
|
||||
### nginx on Droplet (Alternative to LB)
|
||||
|
||||
If using nginx instead of managed LB:
|
||||
|
||||
```nginx
|
||||
# /etc/nginx/sites-available/eagle0
|
||||
upstream eagle_backend {
|
||||
server 127.0.0.1:40032;
|
||||
keepalive 100;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name eagle0.net;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/eagle0.net/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/eagle0.net/privkey.pem;
|
||||
|
||||
# gRPC settings
|
||||
location /net.eagle0.eagle.api.Eagle {
|
||||
grpc_pass grpc://eagle_backend;
|
||||
grpc_read_timeout 1200s;
|
||||
grpc_send_timeout 1200s;
|
||||
grpc_socket_keepalive on;
|
||||
|
||||
# Rate limiting
|
||||
limit_req zone=eagle burst=50 nodelay;
|
||||
}
|
||||
|
||||
# Health check endpoint
|
||||
location /health {
|
||||
proxy_pass http://127.0.0.1:40032/health;
|
||||
}
|
||||
}
|
||||
|
||||
# Rate limit zone
|
||||
limit_req_zone $binary_remote_addr zone=eagle:10m rate=100r/s;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Environment Configuration
|
||||
|
||||
### Secrets Management
|
||||
|
||||
**GitHub Secrets (for CI/CD):**
|
||||
- `DO_REGISTRY_TOKEN` - DigitalOcean Container Registry token
|
||||
- `DO_DROPLET_IP` - Production server IP
|
||||
- `DO_SSH_KEY` - SSH private key for deployment
|
||||
- `OPENAI_API_KEY` - For LLM integration (if used in production)
|
||||
- `DO_SPACES_KEY` - Already exists for S3
|
||||
|
||||
**On Server (environment variables):**
|
||||
```bash
|
||||
# /opt/eagle0/.env
|
||||
GPT_MODEL_NAME=gpt-4
|
||||
OPENAI_API_KEY=sk-...
|
||||
DO_SPACES_KEY=...
|
||||
DO_SPACES_SECRET=...
|
||||
JAVA_OPTS=-Xmx4g -XX:+UseG1GC -XX:MaxGCPauseMillis=200
|
||||
```
|
||||
|
||||
### Configuration Files
|
||||
|
||||
**Production config** (`/opt/eagle0/config/eagle0.conf`):
|
||||
```
|
||||
monteCarloIterations = 150000
|
||||
monteCarloThreads = 4
|
||||
grpcAddress = 0.0.0.0:40042
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Monitoring & Observability
|
||||
|
||||
### Logging
|
||||
|
||||
**Docker logging driver:** json-file with rotation
|
||||
- Logs stored in `/var/lib/docker/containers/<id>/`
|
||||
- Max 5 files of 100MB each
|
||||
|
||||
**Log aggregation options:**
|
||||
1. **DigitalOcean Logs** - $0 for basic, integrates with Droplets
|
||||
2. **Papertrail** - $7/mo for 1GB, good search
|
||||
3. **Self-hosted Loki** - Free, more complex
|
||||
|
||||
### Metrics
|
||||
|
||||
**Prometheus + Grafana** (optional, for advanced monitoring):
|
||||
```yaml
|
||||
# Add to docker-compose.prod.yml
|
||||
prometheus:
|
||||
image: prom/prometheus
|
||||
volumes:
|
||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
||||
ports:
|
||||
- "9090:9090"
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD}
|
||||
```
|
||||
|
||||
**Key metrics to track:**
|
||||
- Connection count
|
||||
- Request latency (p50, p95, p99)
|
||||
- Error rate
|
||||
- CPU/memory usage
|
||||
- Shardok AI search depth/time
|
||||
|
||||
### Alerting
|
||||
|
||||
**DigitalOcean Monitoring Alerts:**
|
||||
- CPU > 80% for 5 minutes
|
||||
- Memory > 90%
|
||||
- Disk > 85%
|
||||
- Droplet unreachable
|
||||
|
||||
**Uptime monitoring:**
|
||||
- Use UptimeRobot (free tier) or Better Uptime
|
||||
- Check `https://eagle0.net/health` every minute
|
||||
|
||||
---
|
||||
|
||||
## Cost Estimate
|
||||
|
||||
### Recommended Starting Configuration
|
||||
|
||||
| Component | Monthly Cost | Notes |
|
||||
|-----------|-------------|-------|
|
||||
| Droplet (s-2vcpu-4gb) | $24 | Eagle always-on, Shardok on-demand |
|
||||
| Spaces | ~$5 | Already paying |
|
||||
| Container Registry | $5 | For Docker images |
|
||||
| DNS | $0 | Included |
|
||||
| Bandwidth | ~$0 | 1TB free, then $0.01/GB |
|
||||
| **Total** | **~$34/mo** | |
|
||||
|
||||
### Optional Add-ons
|
||||
|
||||
| Component | Monthly Cost | Notes |
|
||||
|-----------|-------------|-------|
|
||||
| Load Balancer | +$12 | Only if need HA/failover |
|
||||
| Monitoring (Papertrail) | +$7 | Better log search |
|
||||
|
||||
### Scaling Costs
|
||||
|
||||
| Scenario | Droplet | Monthly Cost |
|
||||
|----------|---------|--------------|
|
||||
| Current (few players) | s-2vcpu-4gb | $24 |
|
||||
| Growing usage | s-4vcpu-8gb | $48 |
|
||||
| CPU-intensive battles | c-4 (dedicated) | $84 |
|
||||
| High concurrency | c-8 (dedicated) | $168 |
|
||||
| Separate Shardok (on-demand) | s-1vcpu-2gb + c-4 hourly | $12 + usage |
|
||||
|
||||
---
|
||||
|
||||
## Migration Plan
|
||||
|
||||
### Phase 1: Infrastructure Setup (Day 1)
|
||||
|
||||
1. Create DigitalOcean resources:
|
||||
- Droplet in SFO3 region
|
||||
- Container Registry
|
||||
- (Optional) Load Balancer
|
||||
|
||||
2. Configure DNS:
|
||||
- Point `eagle0.net` to new infrastructure
|
||||
- Keep `qa.eagle0.net` pointing to home IP
|
||||
|
||||
3. Set up server:
|
||||
- Run initial setup script
|
||||
- Configure Docker and docker-compose
|
||||
- Test SSH access
|
||||
|
||||
### Phase 2: Build Pipeline (Day 2)
|
||||
|
||||
1. Create Dockerfiles:
|
||||
- Enhance `ci/eagle_run.Dockerfile`
|
||||
- Create `ci/shardok_run.Dockerfile`
|
||||
|
||||
2. Create `docker-compose.prod.yml`
|
||||
|
||||
3. Set up GitHub Actions:
|
||||
- Add deployment workflow
|
||||
- Configure secrets
|
||||
- Test build pipeline
|
||||
|
||||
### Phase 3: Deployment (Day 3)
|
||||
|
||||
1. Deploy to production:
|
||||
- Push first images
|
||||
- Start containers
|
||||
- Verify health checks
|
||||
|
||||
2. Configure TLS:
|
||||
- Set up Let's Encrypt
|
||||
- Update nginx/LB configuration
|
||||
|
||||
3. Update DNS:
|
||||
- Switch `eagle0.net` to production
|
||||
- Verify client can connect
|
||||
|
||||
### Phase 4: Validation (Day 4)
|
||||
|
||||
1. Test gameplay:
|
||||
- Connect from Unity client
|
||||
- Play through Eagle gameplay
|
||||
- Test Shardok combat
|
||||
|
||||
2. Monitor:
|
||||
- Check logs for errors
|
||||
- Verify resource usage
|
||||
- Test reconnection behavior
|
||||
|
||||
3. Document:
|
||||
- Update runbooks
|
||||
- Document rollback procedures
|
||||
|
||||
### Phase 5: QA Environment (Day 5)
|
||||
|
||||
1. Configure `qa.eagle0.net`:
|
||||
- Keep pointing to home Mac
|
||||
- Ensure nginx routes correctly
|
||||
|
||||
2. Test environment switching:
|
||||
- Connect to production
|
||||
- Switch to QA
|
||||
- Verify different game states
|
||||
|
||||
---
|
||||
|
||||
## Rollback Procedure
|
||||
|
||||
### Quick Rollback (< 5 minutes)
|
||||
|
||||
```bash
|
||||
# SSH to production server
|
||||
ssh deploy@<droplet-ip>
|
||||
|
||||
# Roll back to previous version
|
||||
cd /opt/eagle0
|
||||
docker-compose -f docker-compose.prod.yml down
|
||||
docker pull registry.digitalocean.com/eagle0/eagle-server:<previous-tag>
|
||||
docker pull registry.digitalocean.com/eagle0/shardok-server:<previous-tag>
|
||||
VERSION=<previous-tag> docker-compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
### Full Rollback to Home Mac
|
||||
|
||||
1. Update DNS: Point `eagle0.net` back to home IP
|
||||
2. Ensure home Mac servers are running
|
||||
3. Wait for DNS propagation (5-30 minutes)
|
||||
|
||||
---
|
||||
|
||||
## Security Checklist
|
||||
|
||||
- [ ] SSH key authentication only (disable password)
|
||||
- [ ] Firewall configured (only 443, 22 from trusted IPs)
|
||||
- [ ] TLS 1.3 enforced
|
||||
- [ ] Secrets in environment variables, not in code
|
||||
- [ ] Container runs as non-root user
|
||||
- [ ] Rate limiting on gRPC endpoints
|
||||
- [ ] Regular security updates (`unattended-upgrades`)
|
||||
- [ ] Remove Shardok internal interface from public nginx (per CONNECTION_ARCHITECTURE.md)
|
||||
|
||||
---
|
||||
|
||||
## Resolved Questions
|
||||
|
||||
1. **Game state migration:** No migration needed. Saves are currently local only. The codebase has a `Persister` pattern (with existing AWS support) that could be adapted to save to DO Spaces in the future.
|
||||
|
||||
2. **LLM API keys:** Yes, OpenAI API keys are needed on the Eagle server. Add `OPENAI_API_KEY` to the `.env` file on the production droplet.
|
||||
|
||||
3. **Multiple regions:** Not needed for now. Single SFO3 region is sufficient.
|
||||
|
||||
## Remaining Open Questions
|
||||
|
||||
1. **Backup strategy:** Should we add automated backups for local persistence, or migrate to DO Spaces persistence first?
|
||||
|
||||
2. **Autoscaling:** Is traffic predictable enough to use fixed instance, or need autoscaling later?
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
### Phase 1: Infrastructure
|
||||
1. **Approve this plan** - Review and discuss any changes
|
||||
2. **Create DigitalOcean resources** - Droplet (s-2vcpu-4gb), Container Registry
|
||||
3. **Set up droplet** - Docker, deploy user, firewall, nginx with TLS
|
||||
|
||||
### Phase 2: Containerization
|
||||
4. **Implement Dockerfiles** - Eagle and Shardok containers
|
||||
5. **Implement Shardok lifecycle management** - Eagle starts/stops Shardok on-demand
|
||||
- Add `ShardokProcessManager` to Eagle server
|
||||
- Start Shardok when battle requested
|
||||
- Stop after idle timeout (5 min)
|
||||
- Health check before routing traffic
|
||||
|
||||
### Phase 3: CI/CD
|
||||
6. **Set up GitHub Actions** - Build and push Docker images
|
||||
7. **Add deployment workflow** - SSH deploy to droplet
|
||||
|
||||
### Phase 4: Migration
|
||||
8. **Deploy to production** - Push images, start services
|
||||
9. **Update DNS** - Point eagle0.net to droplet
|
||||
10. **Validate** - Test gameplay, monitor logs
|
||||
11. **Configure QA** - Ensure qa.eagle0.net still works (home Mac)
|
||||
Executable
+473
@@ -0,0 +1,473 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# generate_changelog.sh
|
||||
#
|
||||
# Generates a weekly changelog from merged PRs, uses Claude to create a synopsis,
|
||||
# and sends an HTML email via Fastmail JMAP API.
|
||||
#
|
||||
# Usage: ./scripts/generate_changelog.sh [--dry-run]
|
||||
#
|
||||
# Configuration files (in ~/.config/eagle0/):
|
||||
# fastmail_token - API token (required)
|
||||
# changelog_recipient - Email addresses, one per line (optional, defaults to sender)
|
||||
#
|
||||
# To set up:
|
||||
# mkdir -p ~/.config/eagle0
|
||||
# echo 'your-token' > ~/.config/eagle0/fastmail_token
|
||||
# chmod 600 ~/.config/eagle0/fastmail_token
|
||||
#
|
||||
# # Optional: configure recipients (one per line, # for comments)
|
||||
# cat > ~/.config/eagle0/changelog_recipient << EOF
|
||||
# alice@example.com
|
||||
# bob@example.com
|
||||
# EOF
|
||||
#
|
||||
# The script tracks its last run using a git tag 'changelog-last-run'.
|
||||
# On first run (no tag), it defaults to the previous Friday at 4pm.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Ensure homebrew binaries are in PATH
|
||||
export PATH="/opt/homebrew/bin:$PATH"
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
TAG_NAME="changelog-last-run"
|
||||
DRY_RUN=false
|
||||
FASTMAIL_API="https://api.fastmail.com/jmap/api/"
|
||||
CONFIG_DIR="$HOME/.config/eagle0"
|
||||
TOKEN_FILE="$CONFIG_DIR/fastmail_token"
|
||||
RECIPIENT_FILE="$CONFIG_DIR/changelog_recipient"
|
||||
|
||||
# Load API token from file or environment
|
||||
load_api_token() {
|
||||
# Environment variable takes precedence
|
||||
if [[ -n "${FASTMAIL_API_TOKEN:-}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Try loading from config file
|
||||
if [[ -f "$TOKEN_FILE" ]]; then
|
||||
FASTMAIL_API_TOKEN=$(cat "$TOKEN_FILE" | tr -d '[:space:]')
|
||||
if [[ -n "$FASTMAIL_API_TOKEN" ]]; then
|
||||
echo "Loaded API token from $TOKEN_FILE"
|
||||
export FASTMAIL_API_TOKEN
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Load recipient emails from config file (one per line)
|
||||
# Returns JSON array fragment like: {"email": "a@b.com"}, {"email": "c@d.com"}
|
||||
load_recipients_json() {
|
||||
local recipients=""
|
||||
if [[ -f "$RECIPIENT_FILE" ]]; then
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
# Skip empty lines and comments
|
||||
line=$(echo "$line" | tr -d '[:space:]')
|
||||
[[ -z "$line" || "$line" == \#* ]] && continue
|
||||
|
||||
if [[ -n "$recipients" ]]; then
|
||||
recipients="$recipients, "
|
||||
fi
|
||||
recipients="$recipients{\"email\": \"$line\"}"
|
||||
done < "$RECIPIENT_FILE"
|
||||
fi
|
||||
echo "$recipients"
|
||||
}
|
||||
|
||||
# Get human-readable list of recipients
|
||||
load_recipients_display() {
|
||||
if [[ -f "$RECIPIENT_FILE" ]]; then
|
||||
grep -v '^#' "$RECIPIENT_FILE" | grep -v '^[[:space:]]*$' | tr '\n' ', ' | sed 's/, $//'
|
||||
fi
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--dry-run)
|
||||
DRY_RUN=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
echo "Usage: $0 [--dry-run]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
# Get the cutoff date - either from tag or previous Friday 4pm
|
||||
get_cutoff_date() {
|
||||
# Try to get the date from the tag
|
||||
if git rev-parse "$TAG_NAME" >/dev/null 2>&1; then
|
||||
# Get the commit date of the tagged commit
|
||||
git log -1 --format="%aI" "$TAG_NAME"
|
||||
else
|
||||
# Calculate previous Friday at 4pm
|
||||
# Get current day of week (1=Monday, 7=Sunday)
|
||||
local dow=$(date +%u)
|
||||
local days_since_friday
|
||||
|
||||
if [[ $dow -ge 5 ]]; then
|
||||
# Friday (5), Saturday (6), or Sunday (7)
|
||||
days_since_friday=$((dow - 5))
|
||||
else
|
||||
# Monday (1) through Thursday (4)
|
||||
days_since_friday=$((dow + 2))
|
||||
fi
|
||||
|
||||
# Get previous Friday at 4pm in ISO format
|
||||
if [[ "$(uname)" == "Darwin" ]]; then
|
||||
date -v-"${days_since_friday}d" -v16H -v0M -v0S +"%Y-%m-%dT%H:%M:%S%z"
|
||||
else
|
||||
date -d "$days_since_friday days ago 16:00:00" --iso-8601=seconds
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Fetch merged PRs since the cutoff date
|
||||
fetch_merged_prs() {
|
||||
local since_date="$1"
|
||||
local output_file="$2"
|
||||
|
||||
echo "Fetching PRs merged since: $since_date"
|
||||
|
||||
# Use gh to search for merged PRs
|
||||
gh pr list \
|
||||
--state merged \
|
||||
--base main \
|
||||
--json number,title,body,mergedAt,author \
|
||||
--jq ".[] | select(.mergedAt >= \"$since_date\")" \
|
||||
> "$output_file.json"
|
||||
|
||||
# Format the output nicely
|
||||
echo "# Merged PRs since $since_date" > "$output_file"
|
||||
echo "" >> "$output_file"
|
||||
|
||||
# Process each PR
|
||||
jq -r '
|
||||
"## PR #\(.number): \(.title)\n" +
|
||||
"Author: \(.author.login)\n" +
|
||||
"Merged: \(.mergedAt)\n\n" +
|
||||
"### Description\n" +
|
||||
(.body // "(No description)") +
|
||||
"\n\n---\n"
|
||||
' "$output_file.json" >> "$output_file"
|
||||
|
||||
# Count PRs
|
||||
local pr_count=$(jq -s 'length' "$output_file.json")
|
||||
echo "Found $pr_count merged PRs"
|
||||
|
||||
rm -f "$output_file.json"
|
||||
|
||||
if [[ $pr_count -eq 0 ]]; then
|
||||
echo "No PRs found since $since_date"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Generate synopsis using Claude
|
||||
generate_synopsis() {
|
||||
local input_file="$1"
|
||||
local output_file="$2"
|
||||
|
||||
echo "Generating synopsis with Claude..."
|
||||
|
||||
# Create a prompt file to avoid shell escaping issues
|
||||
local prompt_file="/tmp/eagle0_prompt_$$.txt"
|
||||
# Get repo URL for PR links
|
||||
local repo_url=$(gh repo view --json url -q '.url')
|
||||
|
||||
cat > "$prompt_file" <<PROMPT_HEADER
|
||||
You are summarizing changes for a weekly engineering update email.
|
||||
|
||||
Read the following list of merged PRs and create a concise synopsis grouped by theme/feature/area of the codebase.
|
||||
|
||||
Structure:
|
||||
1. <h1> title (e.g., "Eagle0 Weekly Update")
|
||||
2. <h2>BLUF</h2> (Bottom Line Up Front) - A short prose paragraph (2-4 sentences) highlighting the 1-3 most important changes this week and what to look for when testing. This should be conversational and help readers quickly understand what matters most.
|
||||
3. Synopsis sections (<h2> headings with bullet point summaries)
|
||||
4. <hr> divider
|
||||
5. <h2>PR Details</h2> with the same groupings, but smaller (<h3> headings) and listing PR links
|
||||
- Format each PR as: <a href="${repo_url}/pull/NUMBER">#NUMBER</a>: Title
|
||||
|
||||
Guidelines for the SYNOPSIS sections:
|
||||
- Group related changes together under clear headings (use <h2> tags)
|
||||
- Use bullet points (<ul><li>) for individual changes
|
||||
- Highlight any significant new features, breaking changes, or important fixes
|
||||
- Keep the tone professional but accessible
|
||||
- Don't include PR numbers in the synopsis - focus on what changed and why it matters
|
||||
|
||||
IMPORTANT: Output valid HTML that can be used directly in an email body. Do NOT wrap in \`\`\`html code blocks - just output the raw HTML.
|
||||
|
||||
Here are the merged PRs:
|
||||
|
||||
PROMPT_HEADER
|
||||
|
||||
cat "$input_file" >> "$prompt_file"
|
||||
echo "" >> "$prompt_file"
|
||||
echo "Generate the synopsis now:" >> "$prompt_file"
|
||||
|
||||
# Use Claude CLI to generate the synopsis, wrapped in proper HTML with charset
|
||||
local raw_output="/tmp/eagle0_raw_$$.html"
|
||||
cat "$prompt_file" | claude --print > "$raw_output"
|
||||
|
||||
# Wrap in HTML document with UTF-8 charset
|
||||
cat > "$output_file" <<'HTML_HEAD'
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
<body>
|
||||
HTML_HEAD
|
||||
cat "$raw_output" >> "$output_file"
|
||||
echo "</body></html>" >> "$output_file"
|
||||
|
||||
rm -f "$prompt_file" "$raw_output"
|
||||
echo "Synopsis generated at: $output_file"
|
||||
}
|
||||
|
||||
# Get Fastmail session info (account ID, identity ID, drafts mailbox ID)
|
||||
get_fastmail_session() {
|
||||
echo "Fetching Fastmail session info..." >&2
|
||||
|
||||
# Get session
|
||||
local session=$(curl -s \
|
||||
-H "Authorization: Bearer $FASTMAIL_API_TOKEN" \
|
||||
"https://api.fastmail.com/jmap/session")
|
||||
|
||||
# Extract account ID (first account)
|
||||
FASTMAIL_ACCOUNT_ID=$(echo "$session" | jq -r '.primaryAccounts["urn:ietf:params:jmap:mail"]')
|
||||
|
||||
if [[ -z "$FASTMAIL_ACCOUNT_ID" || "$FASTMAIL_ACCOUNT_ID" == "null" ]]; then
|
||||
echo "Error: Could not get Fastmail account ID. Check your API token." >&2
|
||||
return 1
|
||||
fi
|
||||
echo "Account ID: $FASTMAIL_ACCOUNT_ID" >&2
|
||||
|
||||
# Get identity ID
|
||||
local identity_response=$(curl -s \
|
||||
-H "Authorization: Bearer $FASTMAIL_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-X POST \
|
||||
-d "{
|
||||
\"using\": [\"urn:ietf:params:jmap:core\", \"urn:ietf:params:jmap:mail\", \"urn:ietf:params:jmap:submission\"],
|
||||
\"methodCalls\": [
|
||||
[\"Identity/get\", {\"accountId\": \"$FASTMAIL_ACCOUNT_ID\"}, \"0\"]
|
||||
]
|
||||
}" \
|
||||
"$FASTMAIL_API")
|
||||
|
||||
FASTMAIL_IDENTITY_ID=$(echo "$identity_response" | jq -r '.methodResponses[0][1].list[0].id')
|
||||
FASTMAIL_FROM_EMAIL=$(echo "$identity_response" | jq -r '.methodResponses[0][1].list[0].email')
|
||||
|
||||
if [[ -z "$FASTMAIL_IDENTITY_ID" || "$FASTMAIL_IDENTITY_ID" == "null" ]]; then
|
||||
echo "Error: Could not get Fastmail identity ID." >&2
|
||||
return 1
|
||||
fi
|
||||
echo "Identity ID: $FASTMAIL_IDENTITY_ID (${FASTMAIL_FROM_EMAIL})" >&2
|
||||
|
||||
# Get drafts mailbox ID
|
||||
local mailbox_response=$(curl -s \
|
||||
-H "Authorization: Bearer $FASTMAIL_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-X POST \
|
||||
-d "{
|
||||
\"using\": [\"urn:ietf:params:jmap:core\", \"urn:ietf:params:jmap:mail\"],
|
||||
\"methodCalls\": [
|
||||
[\"Mailbox/query\", {\"accountId\": \"$FASTMAIL_ACCOUNT_ID\", \"filter\": {\"role\": \"drafts\"}}, \"0\"]
|
||||
]
|
||||
}" \
|
||||
"$FASTMAIL_API")
|
||||
|
||||
FASTMAIL_DRAFTS_ID=$(echo "$mailbox_response" | jq -r '.methodResponses[0][1].ids[0]')
|
||||
|
||||
if [[ -z "$FASTMAIL_DRAFTS_ID" || "$FASTMAIL_DRAFTS_ID" == "null" ]]; then
|
||||
echo "Error: Could not get Fastmail drafts mailbox ID." >&2
|
||||
return 1
|
||||
fi
|
||||
echo "Drafts mailbox ID: $FASTMAIL_DRAFTS_ID" >&2
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Send email via Fastmail JMAP API
|
||||
send_email_fastmail() {
|
||||
local synopsis_file="$1"
|
||||
local recipients_json="$2" # JSON array fragment: {"email": "a@b.com"}, {"email": "c@d.com"}
|
||||
|
||||
local subject="Eagle0 Weekly Changelog - $(date +%Y-%m-%d)"
|
||||
local html_body=$(cat "$synopsis_file" | jq -Rs .)
|
||||
|
||||
echo "Sending email via Fastmail JMAP API..."
|
||||
|
||||
# Create the email and send it in one request
|
||||
local response=$(curl -s \
|
||||
-H "Authorization: Bearer $FASTMAIL_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-X POST \
|
||||
-d "{
|
||||
\"using\": [
|
||||
\"urn:ietf:params:jmap:core\",
|
||||
\"urn:ietf:params:jmap:mail\",
|
||||
\"urn:ietf:params:jmap:submission\"
|
||||
],
|
||||
\"methodCalls\": [
|
||||
[\"Email/set\", {
|
||||
\"accountId\": \"$FASTMAIL_ACCOUNT_ID\",
|
||||
\"create\": {
|
||||
\"draft\": {
|
||||
\"from\": [{\"email\": \"$FASTMAIL_FROM_EMAIL\"}],
|
||||
\"to\": [$recipients_json],
|
||||
\"subject\": \"$subject\",
|
||||
\"mailboxIds\": {\"$FASTMAIL_DRAFTS_ID\": true},
|
||||
\"keywords\": {\"\$draft\": true},
|
||||
\"htmlBody\": [{\"partId\": \"body\", \"type\": \"text/html\"}],
|
||||
\"bodyValues\": {
|
||||
\"body\": {
|
||||
\"charset\": \"utf-8\",
|
||||
\"value\": $html_body
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, \"0\"],
|
||||
[\"EmailSubmission/set\", {
|
||||
\"accountId\": \"$FASTMAIL_ACCOUNT_ID\",
|
||||
\"onSuccessDestroyEmail\": [\"#sendIt\"],
|
||||
\"create\": {
|
||||
\"sendIt\": {
|
||||
\"emailId\": \"#draft\",
|
||||
\"identityId\": \"$FASTMAIL_IDENTITY_ID\"
|
||||
}
|
||||
}
|
||||
}, \"1\"]
|
||||
]
|
||||
}" \
|
||||
"$FASTMAIL_API")
|
||||
|
||||
# Check for errors
|
||||
local error=$(echo "$response" | jq -r '.methodResponses[0][1].notCreated.draft.description // empty')
|
||||
if [[ -n "$error" ]]; then
|
||||
echo "Error creating email: $error" >&2
|
||||
echo "Full response: $response" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local send_error=$(echo "$response" | jq -r '.methodResponses[1][1].notCreated.sendIt.description // empty')
|
||||
if [[ -n "$send_error" ]]; then
|
||||
echo "Error sending email: $send_error" >&2
|
||||
echo "Full response: $response" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "Email sent successfully"
|
||||
}
|
||||
|
||||
# Update the tag to mark this run
|
||||
update_tag() {
|
||||
echo "Updating $TAG_NAME tag..."
|
||||
|
||||
# Delete existing tag if present
|
||||
git tag -d "$TAG_NAME" 2>/dev/null || true
|
||||
git push origin --delete "$TAG_NAME" 2>/dev/null || true
|
||||
|
||||
# Create new tag at HEAD
|
||||
git tag "$TAG_NAME"
|
||||
git push origin "$TAG_NAME"
|
||||
|
||||
echo "Tag updated to current HEAD"
|
||||
}
|
||||
|
||||
# Main
|
||||
main() {
|
||||
echo "=== Eagle0 Weekly Changelog Generator ==="
|
||||
echo ""
|
||||
|
||||
# Load API token (only required for actual send)
|
||||
if [[ "$DRY_RUN" != "true" ]]; then
|
||||
if ! load_api_token; then
|
||||
echo "Error: No Fastmail API token found."
|
||||
echo ""
|
||||
echo "To create a token:"
|
||||
echo "1. Go to Fastmail Settings -> Password & Security -> API tokens"
|
||||
echo "2. Create a new token with 'Email submission' scope"
|
||||
echo "3. Save it using one of these methods:"
|
||||
echo ""
|
||||
echo " Option A (recommended): Store in config file"
|
||||
echo " mkdir -p ~/.config/eagle0"
|
||||
echo " echo 'your-token' > ~/.config/eagle0/fastmail_token"
|
||||
echo " chmod 600 ~/.config/eagle0/fastmail_token"
|
||||
echo ""
|
||||
echo " Option B: Set environment variable"
|
||||
echo " export FASTMAIL_API_TOKEN='your-token'"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Get cutoff date
|
||||
local cutoff_date=$(get_cutoff_date)
|
||||
echo "Cutoff date: $cutoff_date"
|
||||
|
||||
# Create temp files
|
||||
local pr_file="/tmp/eagle0_prs_$(date +%s).md"
|
||||
local synopsis_file="/tmp/eagle0_synopsis_$(date +%s).html"
|
||||
|
||||
# Fetch PRs
|
||||
if ! fetch_merged_prs "$cutoff_date" "$pr_file"; then
|
||||
echo "No changes to report. Exiting."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "PR details saved to: $pr_file"
|
||||
|
||||
# Generate synopsis
|
||||
generate_synopsis "$pr_file" "$synopsis_file"
|
||||
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
echo ""
|
||||
echo "=== DRY RUN - Synopsis content ==="
|
||||
cat "$synopsis_file"
|
||||
echo ""
|
||||
echo "=== DRY RUN - Skipping email send and tag update ==="
|
||||
else
|
||||
# Get Fastmail session info
|
||||
if ! get_fastmail_session; then
|
||||
echo "Failed to get Fastmail session info. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Determine recipients (from config file, or default to sender)
|
||||
local recipients_json=$(load_recipients_json)
|
||||
if [[ -z "$recipients_json" ]]; then
|
||||
recipients_json="{\"email\": \"$FASTMAIL_FROM_EMAIL\"}"
|
||||
echo "No recipients configured, sending to self ($FASTMAIL_FROM_EMAIL)"
|
||||
else
|
||||
local recipients_display=$(load_recipients_display)
|
||||
echo "Sending to: $recipients_display"
|
||||
fi
|
||||
|
||||
# Send email
|
||||
send_email_fastmail "$synopsis_file" "$recipients_json"
|
||||
|
||||
# Update tag for next run
|
||||
update_tag
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Done!"
|
||||
echo "PR details: $pr_file"
|
||||
echo "Synopsis: $synopsis_file"
|
||||
}
|
||||
|
||||
main
|
||||
@@ -65,11 +65,6 @@ private:
|
||||
const GameStateW& guessedState,
|
||||
const CommandListSPtr& realAvailableCommands) const -> CommandChoiceResults;
|
||||
|
||||
[[nodiscard]] auto ChooseCommandIndex(
|
||||
const GameSettingsSPtr& settings,
|
||||
const net::eagle0::shardok::api::GameStateView& gsv,
|
||||
const CommandListSPtr& realAvailableCommands) const -> CommandChoiceResults;
|
||||
|
||||
public:
|
||||
explicit ShardokAIClient(
|
||||
PlayerId playerId,
|
||||
@@ -86,6 +81,12 @@ public:
|
||||
[[nodiscard]] auto ChooseCommandIndex(const ShardokEngine& engine) const
|
||||
-> CommandChoiceResults;
|
||||
|
||||
// Overload that works on copies of state - allows caller to release lock during AI thinking
|
||||
[[nodiscard]] auto ChooseCommandIndex(
|
||||
const GameSettingsSPtr& settings,
|
||||
const net::eagle0::shardok::api::GameStateView& gsv,
|
||||
const CommandListSPtr& realAvailableCommands) const -> CommandChoiceResults;
|
||||
|
||||
// MCTS configuration methods (only relevant when using MCTS algorithm)
|
||||
[[nodiscard]] auto GetMCTSConfig() const -> const mcts::MCTSConfig& { return mctsConfig; }
|
||||
void SetMCTSConfig(const mcts::MCTSConfig& config) { mctsConfig = config; }
|
||||
|
||||
@@ -111,17 +111,67 @@ void ShardokGameController::DoAIThread() {
|
||||
if (aiClients.empty()) { printf("No AI players, exiting AI thread.\n"); }
|
||||
|
||||
while (aiThreadKeepGoing) {
|
||||
while (incomingRegistrations > 0) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
// Phase 1: Gather data for AI decision (brief lock)
|
||||
shared_ptr<ShardokAIClient> aiClient;
|
||||
PlayerId playerId;
|
||||
GameSettingsSPtr settings;
|
||||
net::eagle0::shardok::api::GameStateView gsv;
|
||||
CommandListSPtr availableCommands;
|
||||
size_t expectedHistoryCount;
|
||||
|
||||
{
|
||||
unique_lock lk(masterLock);
|
||||
|
||||
if (engine->GameIsOver()) {
|
||||
aiThreadKeepGoing = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
playerId = engine->GetCurrentPlayerId();
|
||||
aiClient = LockedAIClientForPid(playerId);
|
||||
|
||||
if (!aiClient) {
|
||||
// Not an AI player's turn - wait for signal
|
||||
aiCondition.wait(lk);
|
||||
aiThreadKeepGoing = aiThreadKeepGoing && !engine->GameIsOver();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get copies of everything the AI needs
|
||||
settings = engine->GetGameSettings();
|
||||
gsv = engine->GetGameStateView(playerId);
|
||||
availableCommands = engine->GetAvailableCommandsForAIPlayer(playerId);
|
||||
expectedHistoryCount = engine->GetUnfilteredHistoryCount();
|
||||
}
|
||||
// Lock released - polls can now get through
|
||||
|
||||
if (availableCommands->empty()) {
|
||||
printf("no commands for player %d\n", playerId);
|
||||
continue;
|
||||
}
|
||||
|
||||
unique_lock lk(masterLock);
|
||||
if (LockedCheckOneAICommand()) {
|
||||
// Phase 2: AI thinks (NO LOCK - this is the slow part)
|
||||
const auto results = aiClient->ChooseCommandIndex(settings, gsv, availableCommands);
|
||||
|
||||
// Phase 3: Post the command (brief lock)
|
||||
{
|
||||
unique_lock lk(masterLock);
|
||||
|
||||
// Verify state hasn't changed while we were thinking
|
||||
if (engine->GetUnfilteredHistoryCount() != expectedHistoryCount) {
|
||||
// State changed (e.g., human posted command) - re-evaluate
|
||||
printf("AI: State changed while thinking, re-evaluating\n");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (engine->GameIsOver()) {
|
||||
aiThreadKeepGoing = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
engine->PostCommand(playerId, results.chosenIndex);
|
||||
LockedNotifyClients();
|
||||
aiThreadKeepGoing = aiThreadKeepGoing && !engine->GameIsOver();
|
||||
} else {
|
||||
aiCondition.wait(lk);
|
||||
aiThreadKeepGoing = aiThreadKeepGoing && !engine->GameIsOver();
|
||||
aiThreadKeepGoing = !engine->GameIsOver();
|
||||
}
|
||||
}
|
||||
printf("Exiting AI thread.\n");
|
||||
@@ -136,24 +186,6 @@ ShardokGameController::~ShardokGameController() {
|
||||
aiThread.join();
|
||||
}
|
||||
|
||||
auto ShardokGameController::LockedCheckOneAICommand() -> bool {
|
||||
if (engine->GameIsOver()) {
|
||||
printf("Game is over!\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
const PlayerId currentPid = engine->GetCurrentPlayerId();
|
||||
if (const shared_ptr<ShardokAIClient> currentPlayerClient = LockedAIClientForPid(currentPid)) {
|
||||
const int index = currentPlayerClient->ChooseCommandIndex(*engine).chosenIndex;
|
||||
|
||||
engine->PostCommand(currentPid, index);
|
||||
LockedNotifyClients();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void CheckFactionId(
|
||||
const unique_ptr<ShardokEngine> &engine,
|
||||
const PlayerId shardokPlayerId,
|
||||
@@ -229,6 +261,7 @@ void ShardokGameController::PostPlacementCommands(
|
||||
}
|
||||
|
||||
auto ShardokGameController::GetCurrentGameStateBytes() -> byte_vector {
|
||||
scoped_lock<mutex> guard(masterLock);
|
||||
return engine->GetCurrentGameStateBytes();
|
||||
}
|
||||
|
||||
@@ -292,6 +325,9 @@ auto ShardokGameController::GetUpdates(const int64_t startingActionId) -> AllUpd
|
||||
-1,
|
||||
engine->FilterNewResults(-1, startingActionId),
|
||||
nullptr);
|
||||
|
||||
auto gameStateBytes = engine->GetCurrentGameStateBytes();
|
||||
updates.currentGameState.swap(gameStateBytes);
|
||||
}
|
||||
|
||||
return updates;
|
||||
@@ -321,4 +357,74 @@ auto ShardokGameController::ResolvedPlayerInfos()
|
||||
return engine->GetPlayerInfos();
|
||||
}
|
||||
|
||||
void ShardokGameController::RegisterSubscriber(std::shared_ptr<StreamSubscriber> subscriber) {
|
||||
scoped_lock<mutex> guard(subscriberLock);
|
||||
subscribers.push_back(subscriber);
|
||||
}
|
||||
|
||||
void ShardokGameController::UnregisterSubscriber(const StreamSubscriber *subscriber) {
|
||||
scoped_lock<mutex> guard(subscriberLock);
|
||||
subscribers.erase(
|
||||
std::remove_if(
|
||||
subscribers.begin(),
|
||||
subscribers.end(),
|
||||
[subscriber](const std::weak_ptr<StreamSubscriber> &weakSub) {
|
||||
auto sub = weakSub.lock();
|
||||
return !sub || sub.get() == subscriber;
|
||||
}),
|
||||
subscribers.end());
|
||||
}
|
||||
|
||||
auto ShardokGameController::WaitForUpdatesAndPush(
|
||||
std::shared_ptr<StreamSubscriber> subscriber,
|
||||
int64_t startingActionId) -> bool {
|
||||
int64_t lastPushedActionId = startingActionId;
|
||||
|
||||
while (subscriber->IsActive()) {
|
||||
bool gameOver = false;
|
||||
GameOverInfo gameOverInfo{};
|
||||
|
||||
{
|
||||
unique_lock<mutex> guard(masterLock);
|
||||
|
||||
// Wait for updates or game over
|
||||
updateCondition.wait(guard, [this, lastPushedActionId] {
|
||||
return engine->GetUnfilteredHistoryCount() >
|
||||
static_cast<size_t>(lastPushedActionId) ||
|
||||
engine->GameIsOver();
|
||||
});
|
||||
|
||||
if (!subscriber->IsActive()) { return false; }
|
||||
|
||||
gameOver = engine->GameIsOver();
|
||||
|
||||
if (gameOver) {
|
||||
gameOverInfo.gameStatus = fb::ToProto(engine->GetGameStatus());
|
||||
gameOverInfo.playerInfos = engine->GetPlayerInfos();
|
||||
gameOverInfo.endGameUnits = engine->EndGameUnits();
|
||||
}
|
||||
}
|
||||
// Lock released - GetUpdates will acquire its own lock
|
||||
|
||||
if (gameOver) {
|
||||
subscriber->OnGameOver(gameOverInfo);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get updates outside the lock (GetUpdates acquires masterLock internally)
|
||||
AllUpdates updates = GetUpdates(lastPushedActionId);
|
||||
lastPushedActionId = updates.newUnfilteredCount;
|
||||
|
||||
if (!updates.mainResults.empty()) {
|
||||
subscriber->OnUpdate(
|
||||
updates.mainResults,
|
||||
updates.filteredResults,
|
||||
updates.newUnfilteredCount,
|
||||
updates.currentGameState);
|
||||
}
|
||||
}
|
||||
|
||||
return false; // Subscriber disconnected
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#define ShardokGameController_hpp
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
@@ -29,6 +30,50 @@ using std::shared_ptr;
|
||||
using std::unique_ptr;
|
||||
using std::weak_ptr;
|
||||
|
||||
// Forward declaration
|
||||
class ShardokGameController;
|
||||
|
||||
/// Info about a game that has ended, for notifying subscribers
|
||||
struct GameOverInfo {
|
||||
vector<net::eagle0::shardok::common::PlayerInfo> playerInfos;
|
||||
vector<net::eagle0::shardok::storage::ResolvedUnit> endGameUnits;
|
||||
net::eagle0::shardok::common::GameStatus gameStatus;
|
||||
};
|
||||
|
||||
/// Updates for a single player (includes faction ID for client routing)
|
||||
struct OnePlayerUpdates {
|
||||
int32_t eagleFactionId;
|
||||
vector<ActionResultView> resultViews;
|
||||
shared_ptr<AvailableCommands> availableCommands;
|
||||
|
||||
OnePlayerUpdates(
|
||||
const int32_t fid,
|
||||
const vector<ActionResultView>& arvs,
|
||||
const shared_ptr<AvailableCommands>& acs)
|
||||
: eagleFactionId(fid),
|
||||
resultViews(arvs),
|
||||
availableCommands(acs) {}
|
||||
};
|
||||
|
||||
/// Interface for subscribers that receive streaming updates from a game
|
||||
class StreamSubscriber {
|
||||
public:
|
||||
virtual ~StreamSubscriber() = default;
|
||||
|
||||
/// Called when new game updates are available
|
||||
virtual void OnUpdate(
|
||||
const vector<ActionResult>& mainResults,
|
||||
const vector<OnePlayerUpdates>& filteredResults,
|
||||
int32_t newUnfilteredCount,
|
||||
const byte_vector& currentGameState) = 0;
|
||||
|
||||
/// Called when the game ends
|
||||
virtual void OnGameOver(const GameOverInfo& info) = 0;
|
||||
|
||||
/// Returns true if this subscriber is still active and should receive updates
|
||||
[[nodiscard]] virtual auto IsActive() const -> bool = 0;
|
||||
};
|
||||
|
||||
class ShardokGameController {
|
||||
private:
|
||||
// This lock should be held any time we call into engine or modify clients.
|
||||
@@ -39,10 +84,17 @@ private:
|
||||
// Fires whenever there is a new game state update.
|
||||
mutable std::condition_variable updateCondition{};
|
||||
|
||||
// Stream subscribers - protected by separate lock to avoid deadlock with masterLock
|
||||
mutable std::mutex subscriberLock{};
|
||||
std::vector<std::weak_ptr<StreamSubscriber>> subscribers{};
|
||||
|
||||
string serializedRequest;
|
||||
|
||||
unique_ptr<ShardokEngine> engine;
|
||||
|
||||
// Cached immutable data - safe to access without lock since it never changes after construction
|
||||
const GameId cachedGameId;
|
||||
|
||||
std::atomic_int incomingRegistrations = 0;
|
||||
|
||||
const string mapName;
|
||||
@@ -60,11 +112,9 @@ private:
|
||||
|
||||
void LockedNotifyClients() const;
|
||||
|
||||
auto LockedCheckOneAICommand() -> bool;
|
||||
|
||||
auto LockedAIClientForPid(PlayerId pid) const -> shared_ptr<ShardokAIClient>;
|
||||
|
||||
static vector<shared_ptr<ShardokAIClient>> MakeAIClients(const unique_ptr<ShardokEngine> &e);
|
||||
static vector<shared_ptr<ShardokAIClient>> MakeAIClients(const unique_ptr<ShardokEngine>& e);
|
||||
|
||||
void DoAIThread();
|
||||
|
||||
@@ -75,6 +125,7 @@ public:
|
||||
string serializedRequest = "")
|
||||
: serializedRequest(std::move(serializedRequest)),
|
||||
engine(std::move(e)),
|
||||
cachedGameId(engine->GetGameId()),
|
||||
mapName(std::move(mapName)),
|
||||
logFilePath(MakeLogFilePath()),
|
||||
aiClients(MakeAIClients(engine)),
|
||||
@@ -98,26 +149,13 @@ public:
|
||||
PlayerId shardokPlayerId,
|
||||
int eagleFactionId,
|
||||
int64_t token,
|
||||
const vector<UnitPlacementInfo> &infos);
|
||||
|
||||
struct OnePlayerUpdates {
|
||||
int32_t eagleFactionId;
|
||||
vector<ActionResultView> resultViews;
|
||||
shared_ptr<AvailableCommands> availableCommands;
|
||||
|
||||
OnePlayerUpdates(
|
||||
const int32_t fid,
|
||||
const vector<ActionResultView> &arvs,
|
||||
const shared_ptr<AvailableCommands> &acs)
|
||||
: eagleFactionId(fid),
|
||||
resultViews(arvs),
|
||||
availableCommands(acs) {}
|
||||
};
|
||||
const vector<UnitPlacementInfo>& infos);
|
||||
|
||||
struct AllUpdates {
|
||||
vector<ActionResult> mainResults;
|
||||
vector<OnePlayerUpdates> filteredResults;
|
||||
int32_t newUnfilteredCount;
|
||||
byte_vector currentGameState;
|
||||
};
|
||||
auto GetUpdates(int64_t startingActionId) -> AllUpdates;
|
||||
auto GetCurrentGameStateBytes() -> byte_vector;
|
||||
@@ -127,13 +165,23 @@ public:
|
||||
auto ResolvedPlayerInfos() -> vector<net::eagle0::shardok::common::PlayerInfo>;
|
||||
auto EndGameUnits() -> vector<net::eagle0::shardok::storage::ResolvedUnit>;
|
||||
|
||||
[[nodiscard]] auto GetGameId() const -> GameId { return engine->GetGameId(); }
|
||||
|
||||
[[nodiscard]] auto GetHexMap() const -> const HexMap * {
|
||||
return engine->GetCurrentGameState()->hex_map();
|
||||
}
|
||||
[[nodiscard]] auto GetGameId() const -> GameId { return cachedGameId; }
|
||||
|
||||
[[nodiscard]] auto GetLogFilePath() const -> string { return logFilePath; }
|
||||
|
||||
/// Register a subscriber to receive streaming updates for this game.
|
||||
/// The subscriber will receive updates until it becomes inactive or is unregistered.
|
||||
void RegisterSubscriber(std::shared_ptr<StreamSubscriber> subscriber);
|
||||
|
||||
/// Unregister a subscriber. Safe to call even if the subscriber was never registered.
|
||||
void UnregisterSubscriber(const StreamSubscriber* subscriber);
|
||||
|
||||
/// Wait for game updates, pushing them to the given subscriber.
|
||||
/// Blocks until the game ends or the subscriber becomes inactive.
|
||||
/// Returns true if the game ended normally, false if subscriber disconnected.
|
||||
auto WaitForUpdatesAndPush(
|
||||
std::shared_ptr<StreamSubscriber> subscriber,
|
||||
int64_t startingActionId) -> bool;
|
||||
};
|
||||
} // namespace shardok
|
||||
|
||||
|
||||
@@ -529,6 +529,130 @@ auto FromInternalStatus(
|
||||
throw ShardokInternalErrorException("Bad unit status on resolved unit");
|
||||
}
|
||||
|
||||
auto EagleInterfaceImpl::SubscribeToGame(
|
||||
ServerContext *context,
|
||||
const GameSubscriptionRequest *request,
|
||||
grpc::ServerWriter<GameStatusResponse> *writer) -> Status {
|
||||
shared_ptr<ShardokGameController> controller;
|
||||
try {
|
||||
controller = ControllerForGame(request->game_id(), request->game_setup_info());
|
||||
} catch (NewGameException &e) { return e.GetStatus(); }
|
||||
|
||||
if (!controller) { return Status(StatusCode::NOT_FOUND, "Game not found"); }
|
||||
|
||||
// Send initial state
|
||||
GameStatusResponse initialResponse;
|
||||
PopulateGameStatusResponse(
|
||||
controller,
|
||||
request->game_setup_info().known_result_count(),
|
||||
&initialResponse);
|
||||
if (!writer->Write(initialResponse)) {
|
||||
return Status::OK; // Client disconnected
|
||||
}
|
||||
|
||||
// If game was already over, we're done
|
||||
if (initialResponse.has_game_over_response()) { return Status::OK; }
|
||||
|
||||
// Track the actual count after initial response to avoid duplicate sends
|
||||
const int64_t countAfterInitialResponse =
|
||||
initialResponse.has_game_update_response()
|
||||
? initialResponse.game_update_response().total_action_result_count()
|
||||
: request->game_setup_info().known_result_count();
|
||||
|
||||
// Create a subscriber that writes to the gRPC stream
|
||||
class GrpcStreamSubscriber : public StreamSubscriber {
|
||||
private:
|
||||
grpc::ServerWriter<GameStatusResponse> *writer_;
|
||||
ServerContext *context_;
|
||||
std::atomic<bool> active_{true};
|
||||
std::string gameId_;
|
||||
|
||||
public:
|
||||
GrpcStreamSubscriber(
|
||||
grpc::ServerWriter<GameStatusResponse> *w,
|
||||
ServerContext *ctx,
|
||||
std::string gameId)
|
||||
: writer_(w),
|
||||
context_(ctx),
|
||||
gameId_(std::move(gameId)) {}
|
||||
|
||||
void OnUpdate(
|
||||
const vector<ActionResult> &mainResults,
|
||||
const vector<OnePlayerUpdates> &filteredResults,
|
||||
int32_t newUnfilteredCount,
|
||||
const byte_vector ¤tGameState) override {
|
||||
if (!active_) return;
|
||||
|
||||
GameStatusResponse response;
|
||||
response.set_game_id(gameId_);
|
||||
|
||||
response.mutable_game_update_response()->mutable_update_responses()->Add(
|
||||
begin(mainResults),
|
||||
end(mainResults));
|
||||
|
||||
// Add filtered results for each player with their faction IDs
|
||||
for (const auto &playerUpdate : filteredResults) {
|
||||
auto *filtered =
|
||||
response.mutable_game_update_response()->add_filtered_update_responses();
|
||||
filtered->set_eagle_faction_id(playerUpdate.eagleFactionId);
|
||||
filtered->mutable_action_result_views()->Add(
|
||||
begin(playerUpdate.resultViews),
|
||||
end(playerUpdate.resultViews));
|
||||
if (playerUpdate.availableCommands) {
|
||||
*filtered->mutable_available_commands() = *playerUpdate.availableCommands;
|
||||
}
|
||||
}
|
||||
|
||||
response.mutable_game_update_response()->set_total_action_result_count(
|
||||
newUnfilteredCount);
|
||||
*response.mutable_game_update_response()->mutable_current_game_state() =
|
||||
std::string(currentGameState.begin(), currentGameState.end());
|
||||
|
||||
if (!writer_->Write(response)) { active_ = false; }
|
||||
}
|
||||
|
||||
void OnGameOver(const GameOverInfo &info) override {
|
||||
if (!active_) return;
|
||||
|
||||
GameStatusResponse response;
|
||||
response.set_game_id(gameId_);
|
||||
|
||||
PopulateGameOverResponse(
|
||||
gameId_,
|
||||
info.gameStatus,
|
||||
info.playerInfos,
|
||||
info.endGameUnits,
|
||||
response.mutable_game_over_response());
|
||||
|
||||
writer_->Write(response);
|
||||
active_ = false;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto IsActive() const -> bool override {
|
||||
return active_ && !context_->IsCancelled();
|
||||
}
|
||||
};
|
||||
|
||||
auto subscriber =
|
||||
std::make_shared<GrpcStreamSubscriber>(writer, context, controller->GetGameId());
|
||||
|
||||
controller->RegisterSubscriber(subscriber);
|
||||
|
||||
// Wait for updates and push them until game ends or subscriber disconnects
|
||||
// Use countAfterInitialResponse to avoid re-sending results already in initial response
|
||||
bool gameEnded = controller->WaitForUpdatesAndPush(subscriber, countAfterInitialResponse);
|
||||
|
||||
controller->UnregisterSubscriber(subscriber.get());
|
||||
|
||||
if (gameEnded) {
|
||||
printf("SubscribeToGame: Game ended normally\n");
|
||||
} else {
|
||||
printf("SubscribeToGame: Subscriber disconnected\n");
|
||||
}
|
||||
|
||||
return Status::OK;
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
#ifndef NDEBUG
|
||||
|
||||
@@ -33,6 +33,7 @@ using grpc::Status;
|
||||
using net::eagle0::common::GameSetupInfo;
|
||||
using net::eagle0::common::GameStatusRequest;
|
||||
using net::eagle0::common::GameStatusResponse;
|
||||
using net::eagle0::common::GameSubscriptionRequest;
|
||||
using net::eagle0::common::HexMapNamesRequest;
|
||||
using net::eagle0::common::HexMapNamesResponse;
|
||||
using net::eagle0::common::HexMapRequest;
|
||||
@@ -78,6 +79,11 @@ public:
|
||||
ServerContext* context,
|
||||
const HexMapNamesRequest* request,
|
||||
HexMapNamesResponse* response) -> Status override;
|
||||
|
||||
auto SubscribeToGame(
|
||||
ServerContext* context,
|
||||
const GameSubscriptionRequest* request,
|
||||
grpc::ServerWriter<GameStatusResponse>* writer) -> Status override;
|
||||
};
|
||||
} // namespace shardok
|
||||
|
||||
|
||||
+14
-1
@@ -120,6 +120,19 @@ namespace eagle {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Force the circuit breaker to allow an immediate reconnect attempt.
|
||||
/// Resets the state to HalfOpen so the next connection will be a test.
|
||||
/// </summary>
|
||||
public void ForceReconnect() {
|
||||
lock (this) {
|
||||
if (_state == State.Open) {
|
||||
_state = State.HalfOpen;
|
||||
_logger.LogLine("[CIRCUIT] OPEN → HALF_OPEN (forced by user)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get human-readable status for UI display.
|
||||
/// </summary>
|
||||
@@ -138,7 +151,7 @@ namespace eagle {
|
||||
var timeUntilTest = OpenTimeoutSeconds -
|
||||
(DateTime.UtcNow - _openedAt.Value).TotalSeconds;
|
||||
if (timeUntilTest > 0) {
|
||||
return $"Server unavailable. Retrying in {(int)timeUntilTest}s";
|
||||
return $"Server unavailable. Retrying in {Math.Min(10, (int)timeUntilTest)}s";
|
||||
}
|
||||
}
|
||||
return "Server unavailable. Testing...";
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using Net.Eagle0.Eagle.Api;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace eagle {
|
||||
/// <summary>
|
||||
@@ -26,6 +27,9 @@ namespace eagle {
|
||||
private PersistentClientConnection _connection;
|
||||
private IGameStateProvider _gameStateProvider;
|
||||
|
||||
[Tooltip("Optional button to force immediate reconnection when server is down")]
|
||||
public Button retryButton;
|
||||
|
||||
// Update interval in seconds
|
||||
private const float UpdateInterval = 0.5f;
|
||||
private float _timeSinceLastUpdate = 0f;
|
||||
@@ -39,9 +43,10 @@ namespace eagle {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the PersistentClientConnection - this assumes it's accessible
|
||||
// In production, this would need proper dependency injection
|
||||
// For now, the connection will be set externally or found via another method
|
||||
if (retryButton != null) {
|
||||
retryButton.onClick.AddListener(OnRetryClicked);
|
||||
retryButton.gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -73,11 +78,12 @@ namespace eagle {
|
||||
// Check circuit breaker state first - it takes precedence
|
||||
var circuitState = _connection.CircuitBreaker.CurrentState;
|
||||
if (circuitState == ConnectionCircuitBreaker.State.Open) {
|
||||
SetRetryButtonVisible(true);
|
||||
var nextTest = _connection.CircuitBreaker.NextTestAttempt;
|
||||
if (nextTest.HasValue) {
|
||||
var timeUntilTest = nextTest.Value - DateTime.UtcNow;
|
||||
if (timeUntilTest.TotalSeconds > 0) {
|
||||
int seconds = (int)Math.Ceiling(timeUntilTest.TotalSeconds);
|
||||
int seconds = Math.Min(10, (int)Math.Ceiling(timeUntilTest.TotalSeconds));
|
||||
_textComponent.text =
|
||||
$"<color=red>●</color> Server down. Retry in {seconds}s";
|
||||
return;
|
||||
@@ -86,6 +92,7 @@ namespace eagle {
|
||||
_textComponent.text = "<color=red>●</color> Server unavailable";
|
||||
return;
|
||||
} else if (circuitState == ConnectionCircuitBreaker.State.HalfOpen) {
|
||||
SetRetryButtonVisible(false);
|
||||
_textComponent.text = "<color=yellow>●</color> Testing connection...";
|
||||
return;
|
||||
}
|
||||
@@ -94,6 +101,11 @@ namespace eagle {
|
||||
var state = _connection.CurrentState;
|
||||
var nextAttempt = _connection.NextReconnectAttempt;
|
||||
|
||||
// Show retry button if we're counting down to a reconnect attempt
|
||||
bool isCountingDown = state == ConnectionState.Reconnecting && nextAttempt.HasValue &&
|
||||
(nextAttempt.Value - DateTime.UtcNow).TotalSeconds > 0;
|
||||
SetRetryButtonVisible(isCountingDown);
|
||||
|
||||
string statusText = state switch {
|
||||
ConnectionState.Connected => GetConnectedStatusText(),
|
||||
ConnectionState.Connecting => "<color=yellow>●</color> Connecting...",
|
||||
@@ -142,8 +154,16 @@ namespace eagle {
|
||||
return "<color=yellow>●</color> Reconnecting...";
|
||||
}
|
||||
|
||||
int secondsRemaining = (int)Math.Ceiling(timeUntilRetry.TotalSeconds);
|
||||
int secondsRemaining = Math.Min(10, (int)Math.Ceiling(timeUntilRetry.TotalSeconds));
|
||||
return $"<color=orange>●</color> Retry in {secondsRemaining}s";
|
||||
}
|
||||
|
||||
private void SetRetryButtonVisible(bool visible) {
|
||||
if (retryButton != null) { retryButton.gameObject.SetActive(visible); }
|
||||
}
|
||||
|
||||
private void OnRetryClicked() {
|
||||
if (_connection != null) { _connection.ForceReconnect(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,12 +229,11 @@ namespace eagle {
|
||||
|
||||
void OnApplicationPause(bool pause) {
|
||||
if (ModelUpdater == null) { return; }
|
||||
if (pause) {
|
||||
ModelUpdater.StopListeningForUpdates();
|
||||
} else {
|
||||
// Fire-and-forget - subscription is awaited internally and failures are logged
|
||||
_ = ModelUpdater.StartListeningForUpdates();
|
||||
}
|
||||
// Don't unsubscribe on pause - this caused a race condition where the subscriber
|
||||
// could be lost if pause happened before the async StartListeningForUpdates completed.
|
||||
// With MainQueue rate-limiting, keeping the subscription during pause is safe.
|
||||
// Reconnects will continue to work, and updates will queue up and be processed on
|
||||
// resume.
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
@@ -43,7 +43,7 @@ namespace eagle {
|
||||
TokenId? CommandToken { get; }
|
||||
TokenId LastPostedToken { get; }
|
||||
|
||||
Dictionary<String, ShardokGameModel> ShardokGameModels { get; }
|
||||
ConcurrentDictionary<String, ShardokGameModel> ShardokGameModels { get; }
|
||||
|
||||
private bool ShardokGameModelIsRunning(ShardokGameModel sgm) =>
|
||||
sgm.GameStatus.State == GameStatus.Types.State.GameRunning
|
||||
@@ -105,10 +105,9 @@ namespace eagle {
|
||||
_currentModel.ShardokGameModels
|
||||
.Select(sgm => {
|
||||
var needsResync = _shardokNeedsResync.GetValueOrDefault(sgm.Key, false);
|
||||
// Use thread-safe count from gRPC thread updates, fall back to model
|
||||
var count = _shardokResultCounts.GetValueOrDefault(
|
||||
sgm.Key,
|
||||
sgm.Value.History.Count());
|
||||
// Use thread-safe count from gRPC thread updates. Fall back to 0 if
|
||||
// not yet tracked (avoids accessing non-thread-safe History.Count).
|
||||
var count = _shardokResultCounts.GetValueOrDefault(sgm.Key, 0);
|
||||
return new IClientConnectionSubscriber.ShardokViewStatus {
|
||||
shardokGameId = sgm.Key,
|
||||
filteredResultCount = needsResync ? 0 : count,
|
||||
@@ -199,7 +198,8 @@ namespace eagle {
|
||||
return new List<AvailableCommand>();
|
||||
}
|
||||
|
||||
public Dictionary<String, ShardokGameModel> ShardokGameModels { get; set; }
|
||||
// Thread-safe: accessed from heartbeat timer thread via ShardokViewStatuses
|
||||
public ConcurrentDictionary<String, ShardokGameModel> ShardokGameModels { get; set; }
|
||||
|
||||
public FactionView MaybeDestroyedFaction(FactionId factionId) {
|
||||
if (ActiveFactions.TryGetValue(factionId, out var factionView)) {
|
||||
@@ -241,7 +241,8 @@ namespace eagle {
|
||||
new Dictionary<ProvinceId, OneProvinceAvailableCommands>();
|
||||
|
||||
_currentModel.GsView = new GameStateView();
|
||||
_currentModel.ShardokGameModels = new Dictionary<ShardokGameId, ShardokGameModel>();
|
||||
_currentModel.ShardokGameModels =
|
||||
new ConcurrentDictionary<ShardokGameId, ShardokGameModel>();
|
||||
|
||||
_currentModel.BattalionTypes = new Dictionary<BattalionTypeId, BattalionType>();
|
||||
|
||||
@@ -312,8 +313,10 @@ namespace eagle {
|
||||
ServerStatus = updateItem.ActionResultResponse.ServerGameStatus;
|
||||
}
|
||||
|
||||
_lastUnfilteredResultCount =
|
||||
updateItem.ActionResultResponse.UnfilteredResultCountAfter;
|
||||
// Note: _lastUnfilteredResultCount is updated on the gRPC thread in
|
||||
// UpdateResultCounts() before enqueueing. We don't update it here to avoid
|
||||
// race conditions where a backlogged MainQueue update overwrites a newer count.
|
||||
|
||||
if (updateItem.ActionResultResponse.ActionResultViews.Any() ||
|
||||
updateItem.ActionResultResponse.AvailableCommands == null ||
|
||||
updateItem.ActionResultResponse.AvailableCommands.Token !=
|
||||
@@ -339,7 +342,9 @@ namespace eagle {
|
||||
// Battle may have ended before we could create the model - remove
|
||||
// any stale reference and skip this update
|
||||
if (shardokGameModel == null) {
|
||||
_currentModel.ShardokGameModels.Remove(oneResponse.ShardokGameId);
|
||||
_currentModel.ShardokGameModels.TryRemove(
|
||||
oneResponse.ShardokGameId,
|
||||
out _);
|
||||
_shardokResultCounts.TryRemove(oneResponse.ShardokGameId, out _);
|
||||
continue;
|
||||
}
|
||||
@@ -368,7 +373,9 @@ namespace eagle {
|
||||
shardokGameModel;
|
||||
} else {
|
||||
// Game ended - remove from active models so UI knows battle is over
|
||||
_currentModel.ShardokGameModels.Remove(oneResponse.ShardokGameId);
|
||||
_currentModel.ShardokGameModels.TryRemove(
|
||||
oneResponse.ShardokGameId,
|
||||
out _);
|
||||
_shardokResultCounts.TryRemove(oneResponse.ShardokGameId, out _);
|
||||
}
|
||||
}
|
||||
@@ -478,6 +485,31 @@ namespace eagle {
|
||||
_currentModel.BattalionTypes =
|
||||
startingState.BattalionTypes.ToDictionary(bt => bt.TypeId, bt => bt);
|
||||
_currentModel.ChronicleEntries = startingState.ChronicleEntries.ToList();
|
||||
|
||||
// For any outstanding battles not in ShardokGameModels, create models and mark for
|
||||
// resync. This ensures fresh clients get Shardok state for ongoing battles.
|
||||
bool needsResubscribe = false;
|
||||
foreach (var battle in _currentModel.ShardokBattles) {
|
||||
if (!_currentModel.ShardokGameModels.ContainsKey(battle.ShardokGameId)) {
|
||||
var model = MakeGameModel(battle.ShardokGameId);
|
||||
if (model != null) {
|
||||
_currentModel.ShardokGameModels[battle.ShardokGameId] = model;
|
||||
MarkShardokForResync(battle.ShardokGameId);
|
||||
_connectionLogger.LogLine(
|
||||
$"[STATE_RESYNC] Created ShardokGameModel for outstanding battle {battle.ShardokGameId}");
|
||||
needsResubscribe = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we created new ShardokGameModels, re-subscribe to request their full state.
|
||||
// The original subscribe didn't include these battles since we didn't know about them
|
||||
// yet.
|
||||
if (needsResubscribe) {
|
||||
_connectionLogger.LogLine(
|
||||
"[STATE_RESYNC] Re-subscribing to request full Shardok state for new battles");
|
||||
_ = StartListeningForUpdates();
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleUpdates(List<ActionResultView> results) {
|
||||
@@ -780,7 +812,7 @@ namespace eagle {
|
||||
// the Eagle update removing the battle arrives before the Shardok Victory update.
|
||||
if (_currentModel.ShardokGameModels.TryGetValue(rb, out var sgm)) {
|
||||
sgm.MarkBattleEnded("Battle has ended.");
|
||||
_currentModel.ShardokGameModels.Remove(rb);
|
||||
_currentModel.ShardokGameModels.TryRemove(rb, out _);
|
||||
}
|
||||
_shardokResultCounts.TryRemove(rb, out _);
|
||||
|
||||
|
||||
+3
@@ -132,7 +132,10 @@ namespace eagle {
|
||||
|
||||
public void ProvinceHovered(ProvinceId? pid) {
|
||||
// Highlight moving armies table
|
||||
// Check row count to avoid index out of range if data changed after table was built
|
||||
var rowCount = movingArmiesTable.RowCount;
|
||||
MovingArmies.Each((army, i) => {
|
||||
if (i >= rowCount) return;
|
||||
var row = movingArmiesTable.ComponentAt<MovingArmyTableRow>(i);
|
||||
if (army.DestinationProvinceId == pid || army.OriginProvinceId == pid) {
|
||||
row.ShadeOn();
|
||||
|
||||
+135
-24
@@ -1,30 +1,106 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Net.Eagle0.Eagle.Common;
|
||||
using Net.Eagle0.Eagle.Views;
|
||||
|
||||
namespace eagle.Notifications.ARNNotifications {
|
||||
using ProvinceId = Int32;
|
||||
using HeroId = Int32;
|
||||
|
||||
public static class ProfessionGainedDetailsNotificationGenerator {
|
||||
public static readonly ARNNotificationGenerator Generator = GenerateNotifications;
|
||||
|
||||
private static readonly Dictionary<Profession, string> ProfessionNames =
|
||||
new Dictionary<Profession, string> {
|
||||
{ Profession.Mage, "Mage" },
|
||||
{ Profession.Necromancer, "Necromancer" },
|
||||
{ Profession.Engineer, "Engineer" },
|
||||
{ Profession.Paladin, "Paladin" },
|
||||
{ Profession.Ranger, "Ranger" },
|
||||
{ Profession.Champion, "Champion" }
|
||||
};
|
||||
private static readonly Random Random = new();
|
||||
|
||||
private static readonly Dictionary<Profession, string> ProfessionTitles =
|
||||
new Dictionary<Profession, string> {
|
||||
{ Profession.Mage, "Arcane Awakening" },
|
||||
{ Profession.Necromancer, "Dark Pact Sealed" },
|
||||
{ Profession.Engineer, "Genius Unleashed" },
|
||||
{ Profession.Paladin, "Divine Calling" },
|
||||
{ Profession.Ranger, "One with the Wild" },
|
||||
{ Profession.Champion, "Born for Battle" }
|
||||
};
|
||||
private static readonly Dictionary<Profession, string> ProfessionNames = new() {
|
||||
{ Profession.Mage, "Mage" },
|
||||
{ Profession.Necromancer, "Necromancer" },
|
||||
{ Profession.Engineer, "Engineer" },
|
||||
{ Profession.Paladin, "Paladin" },
|
||||
{ Profession.Ranger, "Ranger" },
|
||||
{ Profession.Champion, "Champion" }
|
||||
};
|
||||
|
||||
private static readonly Dictionary<Profession, List<string>> ProfessionTitles = new() {
|
||||
{ Profession.Mage,
|
||||
new List<string> {
|
||||
"Arcane Awakening",
|
||||
"The Gift Revealed",
|
||||
"Touched by Magic",
|
||||
"Mystical Ascension",
|
||||
"Power Unbound",
|
||||
"The Arcane Path",
|
||||
"Secrets of the Weave",
|
||||
"Spellborn",
|
||||
"Wielder of the Unseen",
|
||||
"Flames of Knowledge"
|
||||
} },
|
||||
{ Profession.Necromancer,
|
||||
new List<string> {
|
||||
"Dark Pact Sealed",
|
||||
"Beyond the Veil",
|
||||
"Death's Apprentice",
|
||||
"Whispers from Beyond",
|
||||
"The Forbidden Art",
|
||||
"Shadow Covenant",
|
||||
"Secrets of the Grave",
|
||||
"Communion with Darkness",
|
||||
"The Deathless Path",
|
||||
"Embrace of Shadow"
|
||||
} },
|
||||
{ Profession.Engineer,
|
||||
new List<string> {
|
||||
"Genius Unleashed",
|
||||
"Master of Mechanisms",
|
||||
"The Inventor's Spark",
|
||||
"Gears of Progress",
|
||||
"Mind of Innovation",
|
||||
"Builder of Wonders",
|
||||
"The Tinkerer's Art",
|
||||
"Siege Mastery",
|
||||
"Architect of War",
|
||||
"Mechanical Brilliance"
|
||||
} },
|
||||
{ Profession.Paladin,
|
||||
new List<string> {
|
||||
"Divine Calling",
|
||||
"Holy Vows Taken",
|
||||
"Blessed Champion",
|
||||
"The Righteous Path",
|
||||
"Shield of the Faith",
|
||||
"Anointed Warrior",
|
||||
"Oath of Light",
|
||||
"Heaven's Chosen",
|
||||
"Sacred Duty",
|
||||
"Defender of the Realm"
|
||||
} },
|
||||
{ Profession.Ranger,
|
||||
new List<string> {
|
||||
"One with the Wild",
|
||||
"Voice of the Forest",
|
||||
"The Untamed Path",
|
||||
"Nature's Guardian",
|
||||
"Shadow of the Woods",
|
||||
"Hunter's Instinct",
|
||||
"Wild Heart",
|
||||
"The Wanderer's Way",
|
||||
"Beast Companion",
|
||||
"Eyes of the Hawk"
|
||||
} },
|
||||
{ Profession.Champion,
|
||||
new List<string> {
|
||||
"Born for Battle",
|
||||
"Blade Mastery",
|
||||
"The Warrior's Edge",
|
||||
"Forged in Combat",
|
||||
"Unmatched Prowess",
|
||||
"Heart of Steel",
|
||||
"The Victor's Path",
|
||||
"Legend in the Making",
|
||||
"Master of Arms",
|
||||
"Glory Awaits"
|
||||
} }
|
||||
};
|
||||
|
||||
private static string GetProfessionName(Profession profession) {
|
||||
return ProfessionNames.TryGetValue(profession, out var name) ? name : "Unknown";
|
||||
@@ -40,8 +116,19 @@ namespace eagle.Notifications.ARNNotifications {
|
||||
}
|
||||
|
||||
private static string GetProfessionTitle(Profession profession) {
|
||||
return ProfessionTitles.TryGetValue(profession, out var title) ? title
|
||||
: "Profession Gained";
|
||||
if (ProfessionTitles.TryGetValue(profession, out var titles) && titles.Count > 0) {
|
||||
return titles[Random.Next(titles.Count)];
|
||||
}
|
||||
return "Profession Gained";
|
||||
}
|
||||
|
||||
private static ProvinceId? FindProvinceForHero(HeroId heroId, IGameModel model) {
|
||||
foreach (var province in model.Provinces.Values) {
|
||||
if (province.FullInfo?.RulingFactionHeroIds.Contains(heroId) == true) {
|
||||
return province.Id;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static IEnumerable<Notification> GenerateNotifications(
|
||||
@@ -49,14 +136,38 @@ namespace eagle.Notifications.ARNNotifications {
|
||||
IGameModel currentModel) {
|
||||
var details = notification.Details.ProfessionGainedDetails;
|
||||
var hero = currentModel.Heroes[details.HeroId];
|
||||
var factionName = currentModel.FactionName(details.FactionId);
|
||||
var professionName = GetProfessionName(details.NewProfession);
|
||||
var article = GetArticle(professionName);
|
||||
|
||||
var affectedProvinces = currentModel.ProvincesForFaction(details.FactionId);
|
||||
string textTemplate;
|
||||
List<ProvinceId> affectedProvinces;
|
||||
|
||||
string textTemplate =
|
||||
$"{{Hero}} of {factionName} became {article} {professionName}.\n\n";
|
||||
if (details.FactionId == currentModel.PlayerId) {
|
||||
// Player's own hero - vary text based on faction leader status
|
||||
string heroDescription;
|
||||
if (hero.IsFactionLeader) {
|
||||
heroDescription =
|
||||
$"Your sworn {DisplayNames.SiblingDescription(hero.PronounGender)}";
|
||||
} else {
|
||||
heroDescription = "Your vassal";
|
||||
}
|
||||
var heroProvinceId = FindProvinceForHero(details.HeroId, currentModel);
|
||||
if (heroProvinceId.HasValue) {
|
||||
var provinceName = currentModel.Provinces[heroProvinceId.Value].Name;
|
||||
textTemplate =
|
||||
$"{heroDescription} {{Hero}} in {provinceName} became {article} {professionName}.\n\n";
|
||||
affectedProvinces = new List<ProvinceId> { heroProvinceId.Value };
|
||||
} else {
|
||||
textTemplate =
|
||||
$"{heroDescription} {{Hero}} became {article} {professionName}.\n\n";
|
||||
affectedProvinces = currentModel.ProvincesForFaction(details.FactionId);
|
||||
}
|
||||
} else {
|
||||
// Another faction's hero
|
||||
var factionName = currentModel.FactionName(details.FactionId);
|
||||
textTemplate = $"{{Hero}} of {factionName} became {article} {professionName}.\n\n";
|
||||
affectedProvinces = currentModel.ProvincesForFaction(details.FactionId);
|
||||
}
|
||||
|
||||
var heroPlaceholders = new Dictionary<string, (string nameTextId, string fallback)> {
|
||||
{ "Hero", (hero.NameTextId, "A hero") }
|
||||
|
||||
+7
-1
@@ -31,7 +31,13 @@ namespace eagle.Notifications.ARNNotifications {
|
||||
};
|
||||
|
||||
if (playerId.HasValue && playerId.Value == paidToFactionId) {
|
||||
// no notification
|
||||
yield return DynamicTextNotification.StreamingDynamicNotification(
|
||||
title: "Ransom Accepted",
|
||||
textTemplate: $"We have accepted the ransom from {currentModel.FactionName(paidByFactionId)} for {{RansomedHero}}.\n\n",
|
||||
heroPlaceholders: heroPlaceholders,
|
||||
llmId: notification.LlmId,
|
||||
provinceIds: new List<ProvinceId>(),
|
||||
displayedHeroes: new List<HeroView> { ransomedHero, offeringFactionHead });
|
||||
} else if (playerId.HasValue && playerId.Value == paidByFactionId) {
|
||||
yield return DynamicTextNotification.StreamingDynamicNotification(
|
||||
title: "Ransom Accepted",
|
||||
|
||||
+13
@@ -9,6 +9,7 @@ namespace eagle.Notifications {
|
||||
private List<GeneratedTextListener> textListeners = new();
|
||||
private string textTemplate;
|
||||
private Dictionary<string, string> placeholderValues = new();
|
||||
private Dictionary<string, string> fallbackValues = new();
|
||||
|
||||
public DynamicTextNotification(
|
||||
string title,
|
||||
@@ -72,6 +73,9 @@ namespace eagle.Notifications {
|
||||
string nameTextId = kvp.Value.nameTextId;
|
||||
string fallback = kvp.Value.fallback;
|
||||
|
||||
// Store fallback so UpdateText can use it for missing placeholders
|
||||
fallbackValues[placeholder] = fallback;
|
||||
|
||||
if (!string.IsNullOrEmpty(nameTextId)) {
|
||||
var listener = new GeneratedTextListener(
|
||||
nameTextId,
|
||||
@@ -95,6 +99,15 @@ namespace eagle.Notifications {
|
||||
|
||||
private void UpdateText() {
|
||||
string result = textTemplate;
|
||||
|
||||
// First apply fallbacks for any placeholder without a loaded value
|
||||
foreach (var kvp in fallbackValues) {
|
||||
if (!placeholderValues.ContainsKey(kvp.Key)) {
|
||||
result = result.Replace($"{{{kvp.Key}}}", kvp.Value);
|
||||
}
|
||||
}
|
||||
|
||||
// Then apply actual loaded values
|
||||
foreach (var kvp in placeholderValues) {
|
||||
result = result.Replace($"{{{kvp.Key}}}", kvp.Value);
|
||||
}
|
||||
|
||||
+64
-35
@@ -69,6 +69,17 @@ namespace eagle {
|
||||
private ConnectionCircuitBreaker _circuitBreaker = new ConnectionCircuitBreaker();
|
||||
public ConnectionCircuitBreaker CircuitBreaker => _circuitBreaker;
|
||||
|
||||
/// <summary>
|
||||
/// Force an immediate reconnection attempt, bypassing the circuit breaker timeout.
|
||||
/// </summary>
|
||||
public void ForceReconnect() {
|
||||
_circuitBreaker.ForceReconnect();
|
||||
_retryTimer?.Dispose();
|
||||
NextReconnectAttempt = null;
|
||||
LogConnectionEvent("force_reconnect", "User requested immediate reconnect");
|
||||
Task.Run(() => Connect());
|
||||
}
|
||||
|
||||
private DateTime? GetDeadlineFromNow() {
|
||||
return DateTime.UtcNow.AddSeconds(TimeoutSeconds);
|
||||
}
|
||||
@@ -1060,51 +1071,69 @@ namespace eagle {
|
||||
}
|
||||
}
|
||||
|
||||
// Grace period after connect before sync mismatch triggers reconnect.
|
||||
// Allows time to receive and process historical results after fresh subscribe.
|
||||
private const double SyncMismatchGracePeriodSeconds = 60.0;
|
||||
|
||||
private void HandleHeartbeatResponse(HeartbeatResponse response) {
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"[HEARTBEAT] Got response, server_timestamp={response.ServerTimestamp}");
|
||||
|
||||
// Check for sync mismatches reported by server
|
||||
if (response.GameSyncResults.Count > 0) {
|
||||
foreach (var syncResult in response.GameSyncResults) {
|
||||
if (!syncResult.EagleInSync) {
|
||||
bool hasMismatch = false;
|
||||
foreach (var syncResult in response.GameSyncResults) {
|
||||
if (!syncResult.EagleInSync) {
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"[SYNC_MISMATCH] Game {syncResult.GameId}: Eagle out of sync, " +
|
||||
$"server has {syncResult.ServerUnfilteredResultCount} results");
|
||||
LogConnectionEvent(
|
||||
"sync_mismatch_eagle",
|
||||
$"game={syncResult.GameId}, server_count={syncResult.ServerUnfilteredResultCount}");
|
||||
hasMismatch = true;
|
||||
}
|
||||
|
||||
foreach (var shardokResult in syncResult.ShardokSyncResults) {
|
||||
if (!shardokResult.InSync) {
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"[SYNC_MISMATCH] Game {syncResult.GameId}: Eagle out of sync, " +
|
||||
$"server has {syncResult.ServerUnfilteredResultCount} results");
|
||||
$"[SYNC_MISMATCH] Game {syncResult.GameId}, Shardok {shardokResult.ShardokGameId}: " +
|
||||
$"out of sync, server has {shardokResult.ServerFilteredResultCount} results");
|
||||
LogConnectionEvent(
|
||||
"sync_mismatch_eagle",
|
||||
$"game={syncResult.GameId}, server_count={syncResult.ServerUnfilteredResultCount}");
|
||||
}
|
||||
|
||||
foreach (var shardokResult in syncResult.ShardokSyncResults) {
|
||||
if (!shardokResult.InSync) {
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"[SYNC_MISMATCH] Game {syncResult.GameId}, Shardok {shardokResult.ShardokGameId}: " +
|
||||
$"out of sync, server has {shardokResult.ServerFilteredResultCount} results");
|
||||
LogConnectionEvent(
|
||||
"sync_mismatch_shardok",
|
||||
$"game={syncResult.GameId}, shardok={shardokResult.ShardokGameId}, " +
|
||||
$"server_count={shardokResult.ServerFilteredResultCount}");
|
||||
}
|
||||
"sync_mismatch_shardok",
|
||||
$"game={syncResult.GameId}, shardok={shardokResult.ShardokGameId}, " +
|
||||
$"server_count={shardokResult.ServerFilteredResultCount}");
|
||||
hasMismatch = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger resync by reconnecting - this will re-subscribe with current counts
|
||||
// and the server will send missing updates
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
"[SYNC_MISMATCH] Detected sync mismatch, triggering reconnect to resync");
|
||||
LogConnectionEvent("sync_mismatch_reconnect", "Triggering reconnect to resync");
|
||||
|
||||
// Schedule reconnect to resync
|
||||
lock (this) {
|
||||
_streamingCall?.Dispose();
|
||||
_streamingCall = null;
|
||||
_threadCancellationTokenSource?.Cancel();
|
||||
}
|
||||
MarkAllShardokGamesForResync();
|
||||
CancelAllPendingSubscriptionAcks();
|
||||
ScheduleReconnect("SyncMismatch");
|
||||
}
|
||||
|
||||
if (!hasMismatch) return;
|
||||
|
||||
// Grace period after connect - allow time to receive historical results
|
||||
if (_lastSuccessfulConnect.HasValue) {
|
||||
var timeSinceConnect =
|
||||
(DateTime.UtcNow - _lastSuccessfulConnect.Value).TotalSeconds;
|
||||
if (timeSinceConnect < SyncMismatchGracePeriodSeconds) {
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"[SYNC_MISMATCH] Ignoring mismatch during grace period ({timeSinceConnect:F1}s < {SyncMismatchGracePeriodSeconds}s)");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger resync by reconnecting - this will re-subscribe with current counts
|
||||
// and the server will send missing updates
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
"[SYNC_MISMATCH] Detected sync mismatch, triggering reconnect to resync");
|
||||
LogConnectionEvent("sync_mismatch_reconnect", "Triggering reconnect to resync");
|
||||
|
||||
// Schedule reconnect to resync
|
||||
lock (this) {
|
||||
_streamingCall?.Dispose();
|
||||
_streamingCall = null;
|
||||
_threadCancellationTokenSource?.Cancel();
|
||||
}
|
||||
MarkAllShardokGamesForResync();
|
||||
CancelAllPendingSubscriptionAcks();
|
||||
ScheduleReconnect("SyncMismatch");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32467,7 +32467,7 @@ GameObject:
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 2147483647
|
||||
m_IsActive: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &300036923
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -33904,6 +33904,7 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: 727a520b2deac4a28ae9c6a00e54c2e7, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::eagle.ConnectionStatusUI
|
||||
retryButton: {fileID: 0}
|
||||
--- !u!114 &320759869
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -50851,6 +50852,148 @@ CanvasRenderer:
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 464655241}
|
||||
m_CullTransparentMesh: 0
|
||||
--- !u!1 &465641331
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 465641332}
|
||||
- component: {fileID: 465641336}
|
||||
- component: {fileID: 465641335}
|
||||
- component: {fileID: 465641334}
|
||||
- component: {fileID: 465641333}
|
||||
m_Layer: 0
|
||||
m_Name: Button
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 0
|
||||
--- !u!224 &465641332
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 465641331}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 1405807865}
|
||||
m_Father: {fileID: 1012849519}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 210.00002, y: -27.5}
|
||||
m_SizeDelta: {x: 80, y: 25}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &465641333
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 465641331}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
|
||||
m_IgnoreLayout: 0
|
||||
m_MinWidth: 80
|
||||
m_MinHeight: 25
|
||||
m_PreferredWidth: 80
|
||||
m_PreferredHeight: -1
|
||||
m_FlexibleWidth: -1
|
||||
m_FlexibleHeight: -1
|
||||
m_LayoutPriority: 1
|
||||
--- !u!114 &465641334
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 465641331}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Button
|
||||
m_Navigation:
|
||||
m_Mode: 3
|
||||
m_WrapAround: 0
|
||||
m_SelectOnUp: {fileID: 0}
|
||||
m_SelectOnDown: {fileID: 0}
|
||||
m_SelectOnLeft: {fileID: 0}
|
||||
m_SelectOnRight: {fileID: 0}
|
||||
m_Transition: 1
|
||||
m_Colors:
|
||||
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
|
||||
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
|
||||
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
|
||||
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
|
||||
m_ColorMultiplier: 1
|
||||
m_FadeDuration: 0.1
|
||||
m_SpriteState:
|
||||
m_HighlightedSprite: {fileID: 0}
|
||||
m_PressedSprite: {fileID: 0}
|
||||
m_SelectedSprite: {fileID: 0}
|
||||
m_DisabledSprite: {fileID: 0}
|
||||
m_AnimationTriggers:
|
||||
m_NormalTrigger: Normal
|
||||
m_HighlightedTrigger: Highlighted
|
||||
m_PressedTrigger: Pressed
|
||||
m_SelectedTrigger: Selected
|
||||
m_DisabledTrigger: Disabled
|
||||
m_Interactable: 1
|
||||
m_TargetGraphic: {fileID: 465641335}
|
||||
m_OnClick:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
--- !u!114 &465641335
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 465641331}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0}
|
||||
m_Type: 1
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
m_FillMethod: 4
|
||||
m_FillAmount: 1
|
||||
m_FillClockwise: 1
|
||||
m_FillOrigin: 0
|
||||
m_UseSpriteMesh: 0
|
||||
m_PixelsPerUnitMultiplier: 1
|
||||
--- !u!222 &465641336
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 465641331}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!1 &466028234
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -56271,9 +56414,9 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_IgnoreLayout: 0
|
||||
m_MinWidth: 176
|
||||
m_MinWidth: 250
|
||||
m_MinHeight: -1
|
||||
m_PreferredWidth: 176
|
||||
m_PreferredWidth: 250
|
||||
m_PreferredHeight: -1
|
||||
m_FlexibleWidth: -1
|
||||
m_FlexibleHeight: 1
|
||||
@@ -121559,6 +121702,7 @@ RectTransform:
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 1453223014}
|
||||
- {fileID: 465641332}
|
||||
m_Father: {fileID: 512260365}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
@@ -170015,6 +170159,144 @@ CanvasRenderer:
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1405279533}
|
||||
m_CullTransparentMesh: 0
|
||||
--- !u!1 &1405807864
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 1405807865}
|
||||
- component: {fileID: 1405807867}
|
||||
- component: {fileID: 1405807866}
|
||||
m_Layer: 0
|
||||
m_Name: Text (TMP)
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &1405807865
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1405807864}
|
||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 465641332}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1405807866
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1405807864}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.TextMeshPro::TMPro.TextMeshProUGUI
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_text: Reconnect
|
||||
m_isRightToLeft: 0
|
||||
m_fontAsset: {fileID: 11400000, guid: ec2736df0379a45bfa7349b652fd07d6, type: 2}
|
||||
m_sharedMaterial: {fileID: 1026247389042360723, guid: ec2736df0379a45bfa7349b652fd07d6,
|
||||
type: 2}
|
||||
m_fontSharedMaterials: []
|
||||
m_fontMaterial: {fileID: 0}
|
||||
m_fontMaterials: []
|
||||
m_fontColor32:
|
||||
serializedVersion: 2
|
||||
rgba: 4281479730
|
||||
m_fontColor: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
|
||||
m_enableVertexGradient: 0
|
||||
m_colorMode: 3
|
||||
m_fontColorGradient:
|
||||
topLeft: {r: 1, g: 1, b: 1, a: 1}
|
||||
topRight: {r: 1, g: 1, b: 1, a: 1}
|
||||
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
|
||||
bottomRight: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_fontColorGradientPreset: {fileID: 0}
|
||||
m_spriteAsset: {fileID: 0}
|
||||
m_tintAllSprites: 0
|
||||
m_StyleSheet: {fileID: 0}
|
||||
m_TextStyleHashCode: -1183493901
|
||||
m_overrideHtmlColors: 0
|
||||
m_faceColor:
|
||||
serializedVersion: 2
|
||||
rgba: 4294967295
|
||||
m_fontSize: 12
|
||||
m_fontSizeBase: 12
|
||||
m_fontWeight: 400
|
||||
m_enableAutoSizing: 0
|
||||
m_fontSizeMin: 18
|
||||
m_fontSizeMax: 72
|
||||
m_fontStyle: 0
|
||||
m_HorizontalAlignment: 2
|
||||
m_VerticalAlignment: 512
|
||||
m_textAlignment: 65535
|
||||
m_characterSpacing: 0
|
||||
m_characterHorizontalScale: 1
|
||||
m_wordSpacing: 0
|
||||
m_lineSpacing: 0
|
||||
m_lineSpacingMax: 0
|
||||
m_paragraphSpacing: 0
|
||||
m_charWidthMaxAdj: 0
|
||||
m_TextWrappingMode: 1
|
||||
m_wordWrappingRatios: 0.4
|
||||
m_overflowMode: 0
|
||||
m_linkedTextComponent: {fileID: 0}
|
||||
parentLinkedComponent: {fileID: 0}
|
||||
m_enableKerning: 0
|
||||
m_ActiveFontFeatures: 6e72656b
|
||||
m_enableExtraPadding: 0
|
||||
checkPaddingRequired: 0
|
||||
m_isRichText: 1
|
||||
m_EmojiFallbackSupport: 1
|
||||
m_parseCtrlCharacters: 1
|
||||
m_isOrthographic: 1
|
||||
m_isCullingEnabled: 0
|
||||
m_horizontalMapping: 0
|
||||
m_verticalMapping: 0
|
||||
m_uvLineOffset: 0
|
||||
m_geometrySortingOrder: 0
|
||||
m_IsTextObjectScaleStatic: 0
|
||||
m_VertexBufferAutoSizeReduction: 0
|
||||
m_useMaxVisibleDescender: 1
|
||||
m_pageToDisplay: 1
|
||||
m_margin: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_isUsingLegacyAnimationComponent: 0
|
||||
m_isVolumetricText: 0
|
||||
m_hasFontAssetChanged: 0
|
||||
m_baseMaterial: {fileID: 0}
|
||||
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
|
||||
--- !u!222 &1405807867
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1405807864}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!1 &1406178529
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -174532,7 +174814,7 @@ MonoBehaviour:
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_text: 3440
|
||||
m_text: Server down. Reconnecting in 10s
|
||||
m_isRightToLeft: 0
|
||||
m_fontAsset: {fileID: 11400000, guid: ec2736df0379a45bfa7349b652fd07d6, type: 2}
|
||||
m_sharedMaterial: {fileID: 1026247389042360723, guid: ec2736df0379a45bfa7349b652fd07d6,
|
||||
@@ -174560,8 +174842,8 @@ MonoBehaviour:
|
||||
m_faceColor:
|
||||
serializedVersion: 2
|
||||
rgba: 4294967295
|
||||
m_fontSize: 18
|
||||
m_fontSizeBase: 18
|
||||
m_fontSize: 12
|
||||
m_fontSizeBase: 12
|
||||
m_fontWeight: 400
|
||||
m_enableAutoSizing: 0
|
||||
m_fontSizeMin: 18
|
||||
@@ -174645,6 +174927,7 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: 727a520b2deac4a28ae9c6a00e54c2e7, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::eagle.ConnectionStatusUI
|
||||
retryButton: {fileID: 465641334}
|
||||
--- !u!1 &1453601446
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -190489,7 +190772,7 @@ GameObject:
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 2147483647
|
||||
m_IsActive: 1
|
||||
m_IsActive: 0
|
||||
--- !u!224 &1566150938
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
|
||||
+94
-94
@@ -385,98 +385,99 @@ namespace Shardok {
|
||||
Model.MyUncommittedUnits.Where(uv => uv.Location.Row == -1).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the ShardokGameModel is updated. This is invoked from UpdateAction,
|
||||
/// which is called from ShardokGameModel.HandleUpdates, which runs on MainQueue.
|
||||
/// No need to re-enqueue - we're already on the main thread.
|
||||
/// </summary>
|
||||
void ModelUpdated() {
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
if (Model == null) { return; }
|
||||
SetHeroLabels();
|
||||
SetModifiers();
|
||||
UpdateReserves();
|
||||
if (Model == null) { return; }
|
||||
SetHeroLabels();
|
||||
SetModifiers();
|
||||
UpdateReserves();
|
||||
|
||||
HandleEnemyStartingPositionOverlays();
|
||||
HandleEnemyStartingPositionOverlays();
|
||||
|
||||
endTurnButton.interactable = false;
|
||||
endTurnButton.interactable = false;
|
||||
|
||||
if (Model.GameStatus != null &&
|
||||
(Model.GameStatus.State == GameStatus.Types.State.Victory)) {
|
||||
turnStatusLabel.text = "Game Over!";
|
||||
if (Model.GameStatus != null &&
|
||||
(Model.GameStatus.State == GameStatus.Types.State.Victory)) {
|
||||
turnStatusLabel.text = "Game Over!";
|
||||
|
||||
gameOverText.text = Model.GameStatus.Description;
|
||||
gameOverCanvas.gameObject.SetActive(true);
|
||||
gameOverText.text = Model.GameStatus.Description;
|
||||
gameOverCanvas.gameObject.SetActive(true);
|
||||
|
||||
endTurnButton.GetComponentInChildren<TMP_Text>().text = "Back to Eagle";
|
||||
endTurnButton.interactable = true;
|
||||
} else if (Model.GameStatus != null && Model.MyTurn) {
|
||||
gameOverCanvas.gameObject.SetActive(false);
|
||||
turnStatusLabel.text = "Your Turn";
|
||||
endTurnButton.GetComponentInChildren<TMP_Text>().text = "Back to Eagle";
|
||||
endTurnButton.interactable = true;
|
||||
} else if (Model.GameStatus != null && Model.MyTurn) {
|
||||
gameOverCanvas.gameObject.SetActive(false);
|
||||
turnStatusLabel.text = "Your Turn";
|
||||
|
||||
if (Model.InSetUp) {
|
||||
endTurnButton.GetComponentInChildren<TMP_Text>().text = "Commit";
|
||||
if (Model.InSetUp) {
|
||||
endTurnButton.GetComponentInChildren<TMP_Text>().text = "Commit";
|
||||
|
||||
var unplacedUnitsWithLocations =
|
||||
Model.MyUncommittedUnits
|
||||
.Where(u => u.Location.Row >= 0 && u.Location.Column >= 0)
|
||||
.ToList();
|
||||
var unplacedUnitsWithLocations =
|
||||
Model.MyUncommittedUnits
|
||||
.Where(u => u.Location.Row >= 0 && u.Location.Column >= 0)
|
||||
.ToList();
|
||||
|
||||
if (!Model.MyTurn) {
|
||||
endTurnButton.interactable = false;
|
||||
} else if (unplacedUnitsWithLocations.Count() < 1) {
|
||||
endTurnButton.GetComponentInChildren<TMP_Text>().text =
|
||||
"No units placed";
|
||||
endTurnButton.interactable = false;
|
||||
} else if (unplacedUnitsWithLocations.Count() > 10) {
|
||||
endTurnButton.GetComponentInChildren<TMP_Text>().text =
|
||||
"Too many units";
|
||||
endTurnButton.interactable = false;
|
||||
} else {
|
||||
endTurnButton.interactable = true;
|
||||
}
|
||||
|
||||
SetDisplayedCommandGroup(0);
|
||||
if (!Model.MyTurn) {
|
||||
endTurnButton.interactable = false;
|
||||
} else if (unplacedUnitsWithLocations.Count() < 1) {
|
||||
endTurnButton.GetComponentInChildren<TMP_Text>().text = "No units placed";
|
||||
endTurnButton.interactable = false;
|
||||
} else if (unplacedUnitsWithLocations.Count() > 10) {
|
||||
endTurnButton.GetComponentInChildren<TMP_Text>().text = "Too many units";
|
||||
endTurnButton.interactable = false;
|
||||
} else {
|
||||
endTurnButton.GetComponentInChildren<TMP_Text>().text = "End Turn";
|
||||
}
|
||||
|
||||
if (Model.HasAvailableCommandWhere(
|
||||
command => commandTypeUIManager.CommandGroupForType(command.Type) ==
|
||||
CommandTypeUIManager.EndTurnCommandGroup)) {
|
||||
endTurnButton.interactable = true;
|
||||
}
|
||||
|
||||
SelectAppropriateDefaultCommand();
|
||||
SetDisplayedCommandGroup(0);
|
||||
} else {
|
||||
turnStatusLabel.text = $"{Model.CurrentPlayerName}'s Turn";
|
||||
endTurnButton.GetComponentInChildren<TMP_Text>().text = "End Turn";
|
||||
}
|
||||
|
||||
locationNameText.text = Model.LocationName;
|
||||
|
||||
if (Model.History.Count > 0) {
|
||||
string monthString = new DateTime(777, Model.Month, 1)
|
||||
.ToString("MMMM", CultureInfo.InvariantCulture);
|
||||
roundInfoText.text = $"{monthString} {Model.CurrentRound}";
|
||||
Weather weather = Model.Weather;
|
||||
if (weather != null) {
|
||||
roundInfoText.text += ", " + ProtoExtensions.WeatherToString(weather);
|
||||
}
|
||||
if (Model.HasAvailableCommandWhere(
|
||||
command => commandTypeUIManager.CommandGroupForType(command.Type) ==
|
||||
CommandTypeUIManager.EndTurnCommandGroup)) {
|
||||
endTurnButton.interactable = true;
|
||||
}
|
||||
|
||||
if (Model.History.Count == 0) {
|
||||
turnHistoryButtonText.text = NoHistoryText;
|
||||
} else if (Model.History.Count > _lastRetrievedHistoryCount) {
|
||||
for (int i = _lastRetrievedHistoryCount; i < Model.History.Count; i++) {
|
||||
var historyEntry = Model.History[i];
|
||||
turnHistoryPanel.AddLine(GetActionResultDescription(historyEntry));
|
||||
SelectAppropriateDefaultCommand();
|
||||
} else {
|
||||
turnStatusLabel.text = $"{Model.CurrentPlayerName}'s Turn";
|
||||
}
|
||||
|
||||
ActionType type = historyEntry.Type;
|
||||
var thisSound = soundManager.SoundForType(type);
|
||||
if (thisSound != null) { audioClipSource.PlayOneShot(thisSound, 1.0f); }
|
||||
}
|
||||
_lastRetrievedHistoryCount = Model.History.Count;
|
||||
turnHistoryButtonText.text = GetActionResultDescription(Model.History.Last());
|
||||
SetModifiers();
|
||||
locationNameText.text = Model.LocationName;
|
||||
|
||||
if (Model.History.Count > 0) {
|
||||
string monthString = new DateTime(777, Model.Month, 1)
|
||||
.ToString("MMMM", CultureInfo.InvariantCulture);
|
||||
roundInfoText.text = $"{monthString} {Model.CurrentRound}";
|
||||
Weather weather = Model.Weather;
|
||||
if (weather != null) {
|
||||
roundInfoText.text += ", " + ProtoExtensions.WeatherToString(weather);
|
||||
}
|
||||
}
|
||||
|
||||
SetupArmiesTable();
|
||||
});
|
||||
if (Model.History.Count == 0) {
|
||||
turnHistoryButtonText.text = NoHistoryText;
|
||||
} else if (Model.History.Count > _lastRetrievedHistoryCount) {
|
||||
for (int i = _lastRetrievedHistoryCount; i < Model.History.Count; i++) {
|
||||
var historyEntry = Model.History[i];
|
||||
turnHistoryPanel.AddLine(GetActionResultDescription(historyEntry));
|
||||
|
||||
ActionType type = historyEntry.Type;
|
||||
var thisSound = soundManager.SoundForType(type);
|
||||
if (thisSound != null) { audioClipSource.PlayOneShot(thisSound, 1.0f); }
|
||||
}
|
||||
_lastRetrievedHistoryCount = Model.History.Count;
|
||||
turnHistoryButtonText.text = GetActionResultDescription(Model.History.Last());
|
||||
SetModifiers();
|
||||
}
|
||||
|
||||
SetupArmiesTable();
|
||||
}
|
||||
|
||||
private void SetupArmiesTable() {
|
||||
@@ -707,34 +708,33 @@ namespace Shardok {
|
||||
}
|
||||
|
||||
void SetModifiers() {
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
hexGrid.ClearCellModifierImages();
|
||||
// Called from ModelUpdated which runs on MainQueue - no need to re-enqueue
|
||||
hexGrid.ClearCellModifierImages();
|
||||
|
||||
for (byte row = 0; row < Model.Map.RowCount; row++) {
|
||||
for (byte column = 0; column < Model.Map.ColumnCount; column++) {
|
||||
Coords coords = new Coords();
|
||||
coords.Row = row;
|
||||
coords.Column = column;
|
||||
for (byte row = 0; row < Model.Map.RowCount; row++) {
|
||||
for (byte column = 0; column < Model.Map.ColumnCount; column++) {
|
||||
Coords coords = new Coords();
|
||||
coords.Row = row;
|
||||
coords.Column = column;
|
||||
|
||||
var terrain = Model.Map.TerrainAt(coords);
|
||||
int cellIndex = MapCoordsToGridIndex(coords);
|
||||
int numberForCell = _randomNumberForCellIndex[cellIndex];
|
||||
hexGrid.SetCellTerrainImage(
|
||||
cellIndex,
|
||||
_imageForTerrainTracker
|
||||
.GetImageForTerrain(terrain, numberForCell, Model.Month));
|
||||
if (terrain.Modifier?.Fire != null) {
|
||||
hexGrid.SetCellModifierEffect(cellIndex, fireEffectPrefab);
|
||||
} else {
|
||||
hexGrid.SetCellModifierEffect(cellIndex, null);
|
||||
}
|
||||
var terrain = Model.Map.TerrainAt(coords);
|
||||
int cellIndex = MapCoordsToGridIndex(coords);
|
||||
int numberForCell = _randomNumberForCellIndex[cellIndex];
|
||||
hexGrid.SetCellTerrainImage(
|
||||
cellIndex,
|
||||
_imageForTerrainTracker
|
||||
.GetImageForTerrain(terrain, numberForCell, Model.Month));
|
||||
if (terrain.Modifier?.Fire != null) {
|
||||
hexGrid.SetCellModifierEffect(cellIndex, fireEffectPrefab);
|
||||
} else {
|
||||
hexGrid.SetCellModifierEffect(cellIndex, null);
|
||||
}
|
||||
|
||||
if (terrain.Modifier?.Bridge != null) {
|
||||
hexGrid.SetCellModifierImage(cellIndex, bridgeImage);
|
||||
}
|
||||
if (terrain.Modifier?.Bridge != null) {
|
||||
hexGrid.SetCellModifierImage(cellIndex, bridgeImage);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void HandleButton() {
|
||||
|
||||
@@ -82,7 +82,7 @@ public class ShardokGameModel {
|
||||
public int Month { get; private set; }
|
||||
private List<PlayerTotals> PlayerTotals { get; set; }
|
||||
|
||||
public List<CommandDescriptor> AvailableCommands { get; private set; }
|
||||
public List<CommandDescriptor> AvailableCommands { get; private set; } = new();
|
||||
public Action UpdateAction { get; set; }
|
||||
public List<PlayerWithHostility> players;
|
||||
public String GetPlayerName(PlayerId playerId) {
|
||||
@@ -217,6 +217,8 @@ public class ShardokGameModel {
|
||||
? newCommands.CurrentCommand.ToList()
|
||||
: newCommands.PreviewCommand.ToList();
|
||||
}
|
||||
// Trigger UI refresh after commands are updated so unit action indicators reflect new state
|
||||
UpdateAction?.Invoke();
|
||||
}
|
||||
|
||||
public bool HasTargetedCommand(Coords start, Coords target, List<CommandType> possibleTypes) {
|
||||
|
||||
+8
-3
@@ -138,9 +138,9 @@ namespace common {
|
||||
// Use TMP's calculated preferred height - this is the actual text size
|
||||
contentHeight = tmpText.preferredHeight;
|
||||
|
||||
// Ensure Content RectTransform is tall enough to contain the text
|
||||
// This is necessary for ScrollRect to scroll properly
|
||||
if (scrollRect.content.rect.height < contentHeight) {
|
||||
// Sync Content RectTransform height with actual text size.
|
||||
// Must both grow AND shrink to prevent blank space when scrolling to bottom.
|
||||
if (Mathf.Abs(scrollRect.content.rect.height - contentHeight) > 1f) {
|
||||
scrollRect.content.SetSizeWithCurrentAnchors(
|
||||
RectTransform.Axis.Vertical,
|
||||
contentHeight);
|
||||
@@ -203,6 +203,11 @@ namespace common {
|
||||
return;
|
||||
}
|
||||
|
||||
// Hold Shift to pause scrolling (lets user read at their own pace)
|
||||
bool isPausedByUser =
|
||||
Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift);
|
||||
if (isPausedByUser) return;
|
||||
|
||||
// Handle pause at bottom
|
||||
if (_isPausedAtBottom) {
|
||||
_pauseTimer += Time.deltaTime;
|
||||
|
||||
+33
-8
@@ -54,18 +54,43 @@ namespace common.GUIUtils {
|
||||
Type = type,
|
||||
Time = DateTime.UtcNow
|
||||
});
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
errorTextField.text = AllMessageText;
|
||||
panel.gameObject.SetActive(true);
|
||||
});
|
||||
|
||||
// Try to display immediately if possible, otherwise queue for later
|
||||
try {
|
||||
if (MainQueue.Q != null) {
|
||||
MainQueue.Q.Enqueue(ShowErrorPanel);
|
||||
} else {
|
||||
_pendingShow = true;
|
||||
}
|
||||
} catch {
|
||||
// MainQueue not ready yet - will show in Update
|
||||
_pendingShow = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool _pendingShow = false;
|
||||
|
||||
private void ShowErrorPanel() {
|
||||
if (errorTextField != null && panel != null) {
|
||||
errorTextField.text = AllMessageText;
|
||||
panel.gameObject.SetActive(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Register for log messages as early as possible
|
||||
void Awake() { Application.logMessageReceivedThreaded += HandleLog; }
|
||||
|
||||
void Update() {
|
||||
// Handle errors that occurred before MainQueue was ready
|
||||
if (_pendingShow) {
|
||||
_pendingShow = false;
|
||||
ShowErrorPanel();
|
||||
}
|
||||
}
|
||||
|
||||
// Use this for initialization
|
||||
void Start() {
|
||||
panel.gameObject.SetActive(false);
|
||||
Application.logMessageReceivedThreaded += HandleLog;
|
||||
}
|
||||
void Start() { panel.gameObject.SetActive(false); }
|
||||
|
||||
public void DismissClicked() {
|
||||
lock (_messages) {
|
||||
|
||||
@@ -21,14 +21,24 @@ option java_outer_classname = "EagleInterface";
|
||||
option objc_class_prefix = "E0G";
|
||||
|
||||
service ShardokInternalInterface {
|
||||
// Server-side streaming for game updates - replaces polling via GetGameStatus
|
||||
rpc SubscribeToGame(GameSubscriptionRequest) returns (stream GameStatusResponse) {}
|
||||
|
||||
rpc PostCommand(PostCommandRequest) returns (GameStatusResponse) {}
|
||||
rpc PostPlacementCommands(PlacementCommandsRequest) returns (GameStatusResponse) {}
|
||||
|
||||
// Deprecated: Use SubscribeToGame for streaming updates instead
|
||||
rpc GetGameStatus(GameStatusRequest) returns (GameStatusResponse) {}
|
||||
|
||||
rpc GetHexMap(HexMapRequest) returns (HexMapResponse) {}
|
||||
rpc GetHexMapNames(HexMapNamesRequest) returns (HexMapNamesResponse) {}
|
||||
}
|
||||
|
||||
message GameSubscriptionRequest {
|
||||
string game_id = 1;
|
||||
GameSetupInfo game_setup_info = 2;
|
||||
}
|
||||
|
||||
message PostCommandRequest {
|
||||
string game_id = 1;
|
||||
int32 player_id = 2;
|
||||
|
||||
@@ -53,4 +53,6 @@ message ChangedFaction {
|
||||
|
||||
.google.protobuf.Int32Value new_last_acted_province_id = 22;
|
||||
bool clear_last_acted_province_id = 23;
|
||||
|
||||
.google.protobuf.StringValue new_name = 26;
|
||||
}
|
||||
|
||||
@@ -72,6 +72,7 @@ message GeneratedTextRequestDetails {
|
||||
PrisonerReleasedMessage prisoner_released_message = 32;
|
||||
PrisonerExiledMessage prisoner_exiled_message = 33;
|
||||
PrisonerReturnedMessage prisoner_returned_message = 34;
|
||||
NewFactionHeadMessage new_faction_head_message = 35;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,3 +335,13 @@ message PrisonerReturnedMessage {
|
||||
int32 to_faction_id = 4;
|
||||
int32 province_id = 5;
|
||||
}
|
||||
|
||||
message NewFactionHeadMessage {
|
||||
int32 new_head_hero_id = 1;
|
||||
int32 faction_id = 2;
|
||||
string previous_faction_name = 3;
|
||||
// If present, the faction is being renamed to this
|
||||
string new_faction_name = 4;
|
||||
// The previous faction head who was executed
|
||||
int32 previous_head_hero_id = 5;
|
||||
}
|
||||
|
||||
@@ -58,4 +58,8 @@ message Hero {
|
||||
repeated EventForHeroBackstory new_backstory_events = 29;
|
||||
|
||||
.net.eagle0.eagle.common.Gender pronoun_gender = 27;
|
||||
|
||||
// The name of the faction this hero would lead, if they led one.
|
||||
// Populated for "great person" heroes from the heroes TSV; empty for others.
|
||||
string led_faction_name = 30;
|
||||
}
|
||||
|
||||
@@ -43,4 +43,6 @@ message FactionViewDiff {
|
||||
repeated .net.eagle0.eagle.common.DiplomacyOffer removed_incoming_diplomacy_offers = 13;
|
||||
|
||||
.google.protobuf.Int32Value new_focus_province_id = 11;
|
||||
|
||||
.google.protobuf.StringValue new_name = 14;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ filegroup(
|
||||
name = "settings",
|
||||
srcs = ["settings.tsv"],
|
||||
visibility = [
|
||||
"//ci:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/service:__pkg__",
|
||||
],
|
||||
@@ -13,6 +14,7 @@ filegroup(
|
||||
name = "game_parameters",
|
||||
srcs = ["game_parameters.json"],
|
||||
visibility = [
|
||||
"//ci:__pkg__",
|
||||
"//src/main/scala/net/eagle0:__subpackages__",
|
||||
"//src/test:__subpackages__",
|
||||
],
|
||||
@@ -22,6 +24,7 @@ filegroup(
|
||||
name = "beasts",
|
||||
srcs = ["beasts.tsv"],
|
||||
visibility = [
|
||||
"//ci:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:__pkg__",
|
||||
"//src/main/scala/net/eagle0/util:__pkg__",
|
||||
"//src/test:__subpackages__",
|
||||
@@ -32,6 +35,7 @@ filegroup(
|
||||
name = "headshots",
|
||||
srcs = ["headshots.tsv"],
|
||||
visibility = [
|
||||
"//ci:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/hero_generator/image_paths:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/hero_generator/random_hero_generator:__pkg__",
|
||||
"//src/main/scala/net/eagle0/util:__pkg__",
|
||||
@@ -46,6 +50,7 @@ filegroup(
|
||||
"heroes.tsv",
|
||||
],
|
||||
visibility = [
|
||||
"//ci:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/service/new_game_creation:__pkg__",
|
||||
"//src/main/scala/net/eagle0/util:__pkg__",
|
||||
"//src/test:__subpackages__",
|
||||
@@ -56,6 +61,7 @@ filegroup(
|
||||
name = "province_map",
|
||||
srcs = ["province_map.tsv"],
|
||||
visibility = [
|
||||
"//ci:__pkg__",
|
||||
"//src/main/scala/net/eagle0:__subpackages__",
|
||||
"//src/test:__subpackages__",
|
||||
],
|
||||
|
||||
@@ -5,6 +5,7 @@ filegroup(
|
||||
name = "settings",
|
||||
srcs = ["settings.tsv"],
|
||||
visibility = [
|
||||
"//ci:__pkg__",
|
||||
"//src/main/cpp/net/eagle0/shardok:__subpackages__",
|
||||
"//src/test/cpp/net/eagle0/shardok:__subpackages__",
|
||||
],
|
||||
@@ -44,6 +45,7 @@ filegroup(
|
||||
name = "battalion_types",
|
||||
srcs = ["battalionTypes.tsv"],
|
||||
visibility = [
|
||||
"//ci:__pkg__",
|
||||
"//src/main/cpp/net/eagle0/shardok:__subpackages__",
|
||||
"//src/main/scala/net/eagle0/eagle/service:__pkg__",
|
||||
"//src/main/scala/net/eagle0/util:__pkg__",
|
||||
|
||||
@@ -114,6 +114,7 @@ class ActionResultApplierImpl(validator: Option[ScalaValidator]) extends ActionR
|
||||
// Apply all remaining entity changes using extension methods
|
||||
val finalState = stateAfterChangedBattalions
|
||||
.applyProvinceActed(result.provinceIdActed)
|
||||
.applyLastCommand(result.provinceId, result.lastCommandTypeForActingProvince)
|
||||
.applyNewBattalions(result.newBattalions, result.provinceId)
|
||||
.applyDestroyedBattalionIds(result.destroyedBattalionIds)
|
||||
.applyNewHeroes(result.newHeroes)
|
||||
|
||||
@@ -54,6 +54,7 @@ scala_library(
|
||||
deps = [
|
||||
":province_update_helpers",
|
||||
":province_update_helpers2",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library:eagle_internal_exception",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:eagle_require",
|
||||
@@ -106,7 +107,6 @@ scala_library(
|
||||
srcs = ["GameStateFactionExtensions.scala"],
|
||||
visibility = ["//visibility:private"],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/views:province_view_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library:eagle_internal_exception",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_faction_trait",
|
||||
@@ -114,6 +114,7 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/faction",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/faction/concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/province:province_view",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -154,6 +155,7 @@ scala_library(
|
||||
":game_state_hero_extensions",
|
||||
":game_state_misc_extensions",
|
||||
":game_state_province_extensions",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_faction_trait",
|
||||
@@ -169,6 +171,7 @@ scala_library(
|
||||
":game_state_hero_extensions",
|
||||
":game_state_misc_extensions",
|
||||
":game_state_province_extensions",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:extra_xp_for_stat_bump_over100",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:xp_for_stat_bump",
|
||||
|
||||
+2
-1
@@ -9,7 +9,7 @@ import net.eagle0.eagle.model.state.faction.FactionRelationship
|
||||
import net.eagle0.eagle.model.state.faction.FactionT
|
||||
import net.eagle0.eagle.model.state.faction.FactionT.OutgoingOfferRound
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.views.province_view.ProvinceView
|
||||
import net.eagle0.eagle.model.view.province.ProvinceView
|
||||
|
||||
object GameStateFactionExtensions {
|
||||
|
||||
@@ -108,6 +108,7 @@ object GameStateFactionExtensions {
|
||||
)
|
||||
|
||||
val updatedFaction = existingFaction.copy(
|
||||
name = cf.newName.getOrElse(existingFaction.name),
|
||||
factionHeadId = cf.newFactionHeadHeroId.getOrElse(existingFaction.factionHeadId),
|
||||
leaderIds = existingFaction.leaderIds.diff(cf.removedLeaderHeroIds) ++ cf.newLeaderHeroIds,
|
||||
prestigeModifiers =
|
||||
|
||||
+18
@@ -1,6 +1,7 @@
|
||||
package net.eagle0.eagle.library.actions.applier
|
||||
|
||||
import net.eagle0.eagle.*
|
||||
import net.eagle0.eagle.api.selected_command.SelectedCommand
|
||||
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
|
||||
import net.eagle0.eagle.library.EagleInternalException
|
||||
import net.eagle0.eagle.model.action_result.changed_province.concrete.ChangedProvinceC
|
||||
@@ -75,5 +76,22 @@ object GameStateProvinceExtensions {
|
||||
}
|
||||
.getOrElse(gameState)
|
||||
}
|
||||
|
||||
def applyLastCommand(
|
||||
provinceId: Option[ProvinceId],
|
||||
lastCommand: Option[SelectedCommand]
|
||||
): GameState =
|
||||
provinceId
|
||||
.filter(_ => lastCommand.exists(!_.isEmpty))
|
||||
.map { pid =>
|
||||
val province = gameState.provinces(pid) match {
|
||||
case p: ProvinceC => p
|
||||
case p => throw new EagleInternalException(s"Unknown ProvinceT type: ${p.getClass}")
|
||||
}
|
||||
gameState.copy(
|
||||
provinces = gameState.provinces.updated(pid, province.copy(lastCommand = lastCommand))
|
||||
)
|
||||
}
|
||||
.getOrElse(gameState)
|
||||
}
|
||||
}
|
||||
|
||||
+22
-17
@@ -8,24 +8,27 @@ import net.eagle0.eagle.api.available_command.{
|
||||
RecommendedBattleConfig
|
||||
}
|
||||
import net.eagle0.eagle.api.command.util.battalion_with_food_cost.BattalionWithFoodCost
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.internal.province.Province
|
||||
import net.eagle0.eagle.internal.game_state.GameState as GameStateProto
|
||||
import net.eagle0.eagle.library.settings.{MaxCombatUnitCountPerSide, MinVigorForMarch}
|
||||
import net.eagle0.eagle.library.util.{BattalionSuitability, LegacyBattalionUtils, ShardokMapInfo}
|
||||
import net.eagle0.eagle.library.util.{BattalionSuitability, BattalionUtils, ShardokMapInfo}
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.{CombatUnitSelector, MarchSuppliesHelpers}
|
||||
import net.eagle0.eagle.library.util.hero.LegacyHeroUtils
|
||||
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
|
||||
import net.eagle0.eagle.library.util.hero.HeroUtils
|
||||
import net.eagle0.eagle.library.util.province.ProvinceUtils
|
||||
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
|
||||
import net.eagle0.eagle.model.proto_converters.BattalionTypeConverter
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.province.ProvinceT
|
||||
|
||||
object AvailableMarchCommandFactory extends AvailableCommandsFactoryForType {
|
||||
|
||||
private def oneOriginProvinceCommand(
|
||||
gameState: GameState,
|
||||
originProvince: Province
|
||||
originProvince: ProvinceT
|
||||
): Option[MarchCommandFromOneProvince] = {
|
||||
val availableDestinations = originProvince.neighbors
|
||||
.map(_.provinceId)
|
||||
.flatMap(gameState.provinces.get)
|
||||
.filterNot(LegacyProvinceUtils.hasBlizzard)
|
||||
.map(gameState.provinces)
|
||||
.filterNot(ProvinceUtils.hasBlizzard)
|
||||
.map { destinationProvince =>
|
||||
val startingPositionIndex = destinationProvince.neighbors
|
||||
.find(n => n.provinceId == originProvince.id)
|
||||
@@ -49,14 +52,14 @@ object AvailableMarchCommandFactory extends AvailableCommandsFactoryForType {
|
||||
.map(heroId => gameState.heroes(heroId))
|
||||
.filter(_.vigor >= MinVigorForMarch.doubleValue)
|
||||
|
||||
if LegacyProvinceUtils.hasBlizzard(originProvince) then return None
|
||||
if ProvinceUtils.hasBlizzard(originProvince) then return None
|
||||
|
||||
if availableDestinations.isEmpty then return None
|
||||
if availableHeroes.isEmpty then return None
|
||||
|
||||
val availableBattalions = originProvince.battalionIds
|
||||
.map(battId => gameState.battalions(battId))
|
||||
.sortBy(_.`type`.index)
|
||||
.sortBy(_.typeId.value)
|
||||
|
||||
val recommendedSendCount =
|
||||
availableHeroes.size
|
||||
@@ -74,7 +77,7 @@ object AvailableMarchCommandFactory extends AvailableCommandsFactoryForType {
|
||||
RecommendedBattleConfig(
|
||||
units = units,
|
||||
food = MarchSuppliesHelpers.foodToTakeOnAttack(
|
||||
units.flatMap(_.battalionId).map(gameState.battalions),
|
||||
units.flatMap(_.battalionId).size,
|
||||
originProvince.food
|
||||
),
|
||||
gold = MarchSuppliesHelpers.goldToTakeOnAttack(
|
||||
@@ -90,9 +93,9 @@ object AvailableMarchCommandFactory extends AvailableCommandsFactoryForType {
|
||||
availableBattalions = availableBattalions.map { batt =>
|
||||
BattalionWithFoodCost(
|
||||
battalionId = batt.id,
|
||||
monthlyFood = LegacyBattalionUtils.monthlyConsumedFood(
|
||||
monthlyFood = BattalionUtils.monthlyConsumedFood(
|
||||
battalion = batt,
|
||||
bts = gameState.battalionTypes.toVector
|
||||
bts = gameState.battalionTypes
|
||||
)
|
||||
)
|
||||
}.toVector,
|
||||
@@ -112,11 +115,12 @@ object AvailableMarchCommandFactory extends AvailableCommandsFactoryForType {
|
||||
}
|
||||
|
||||
override def availableCommand(
|
||||
gameState: GameState,
|
||||
gameStateProto: GameStateProto,
|
||||
factionId: FactionId,
|
||||
provinceId: ProvinceId
|
||||
): Option[MarchAvailableCommand] =
|
||||
LegacyHeroUtils
|
||||
): Option[MarchAvailableCommand] = {
|
||||
val gameState = GameStateConverter.fromProto(gameStateProto)
|
||||
HeroUtils
|
||||
.consideredProvinces(gameState, factionId, provinceId)
|
||||
.flatMap(p => oneOriginProvinceCommand(gameState = gameState, originProvince = p)) match {
|
||||
case items if items.isEmpty => None
|
||||
@@ -125,8 +129,9 @@ object AvailableMarchCommandFactory extends AvailableCommandsFactoryForType {
|
||||
MarchAvailableCommand(
|
||||
actingProvinceId = provinceId,
|
||||
oneProvinceCommands = opcs,
|
||||
availableTypes = gameState.battalionTypes
|
||||
availableTypes = gameState.battalionTypes.map(BattalionTypeConverter.toProto)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -447,12 +447,16 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:max_combat_unit_count_per_side",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:min_vigor_for_march",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:battalion_suitability",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:legacy_battalion_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:battalion_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:shardok_map_info",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:combat_unit_selector",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:march_supplies_helpers",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/hero:legacy_hero_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/province:legacy_province_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/hero:hero_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:battalion_type_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -149,14 +149,16 @@ scala_library(
|
||||
exports = [
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:action_result_notification_details_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:action_result_type_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:faction_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library:game_history",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request/chronicle_event",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/date",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/date",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -207,9 +209,6 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types:captured_hero_resolved_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types:end_aftermath_phase_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:battle_revelation_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:notification_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/llm_request",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:battle_revelation",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
|
||||
@@ -803,7 +802,6 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:action_result_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_battalion_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_hero_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero:gender",
|
||||
@@ -890,7 +888,6 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request/chronicle_event",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types:new_round_action_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/llm_request/chronicle_event",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/chronicle_entry",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/date",
|
||||
@@ -1288,7 +1285,6 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types:end_recon_resolution_phase_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types:recon_succeeded_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province:incoming_end_turn_action",
|
||||
@@ -1837,35 +1833,6 @@ scala_library(
|
||||
],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "unaffiliated_hero_moved_action",
|
||||
srcs = ["UnaffiliatedHeroMovedAction.scala"],
|
||||
visibility = [
|
||||
"//src/main/scala/net/eagle0/eagle/library:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/common:functional_random",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:round_phase_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/scala/net/eagle0/common:functional_random",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:free_hero_move_vigor_cost",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/unaffiliated_hero",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:unaffiliated_hero_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/quest",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/unaffiliated_hero",
|
||||
],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "unaffiliated_hero_rejoined_action",
|
||||
srcs = ["UnaffiliatedHeroRejoinedAction.scala"],
|
||||
|
||||
+27
-15
@@ -8,6 +8,7 @@ import net.eagle0.eagle.library.util.ransom_validity.RansomValidity
|
||||
import net.eagle0.eagle.library.util.ReturningHeroes
|
||||
import net.eagle0.eagle.model.action_result.changed_province.concrete.ChangedProvinceC
|
||||
import net.eagle0.eagle.model.action_result.concrete.{ActionResultC, ChangedFactionC, ChangedHeroC}
|
||||
import net.eagle0.eagle.model.action_result.generated_text_request.LlmRequestT.NewFactionHeadMessage
|
||||
import net.eagle0.eagle.model.action_result.types.{
|
||||
FactionDestroyedResultType,
|
||||
FactionLeaderRemovedResultType,
|
||||
@@ -208,24 +209,35 @@ case class CheckForFactionChangesAction(
|
||||
revisedFactions match {
|
||||
case Nil => None
|
||||
case fs =>
|
||||
val changedFactionData = fs.map { revisedFaction =>
|
||||
val originalFaction = factions.find(_.id == revisedFaction.id).get
|
||||
val newHeadHero = heroes.find(_.id == revisedFaction.factionHeadId)
|
||||
val newFactionName = newHeadHero.flatMap(_.ledFactionName)
|
||||
(
|
||||
ChangedFactionC(
|
||||
factionId = revisedFaction.id,
|
||||
removedLeaderHeroIds = originalFaction.leaderIds.filter(killedHeroIds.contains),
|
||||
newFactionHeadHeroId = Some(revisedFaction.factionHeadId),
|
||||
newName = newFactionName
|
||||
),
|
||||
NewFactionHeadMessage(
|
||||
requestId = s"new_faction_head_${revisedFaction.id}_${revisedFaction.factionHeadId}",
|
||||
eagleGameId = gameId,
|
||||
recipientFactionIds = Vector.empty,
|
||||
alwaysGenerate = false,
|
||||
newHeadHeroId = revisedFaction.factionHeadId,
|
||||
factionId = revisedFaction.id,
|
||||
previousFactionName = originalFaction.name,
|
||||
newFactionName = newFactionName,
|
||||
previousHeadHeroId = originalFaction.factionHeadId
|
||||
)
|
||||
)
|
||||
}.toVector
|
||||
Some(
|
||||
ActionResultC(
|
||||
actionResultType = FactionLeaderRemovedResultType,
|
||||
changedFactions = fs
|
||||
.map(revisedFaction =>
|
||||
ChangedFactionC(
|
||||
factionId = revisedFaction.id,
|
||||
removedLeaderHeroIds = factions
|
||||
.find(
|
||||
_.id == revisedFaction.id
|
||||
)
|
||||
.get
|
||||
.leaderIds
|
||||
.filter(killedHeroIds.contains),
|
||||
newFactionHeadHeroId = Some(revisedFaction.factionHeadId)
|
||||
)
|
||||
)
|
||||
.toVector
|
||||
changedFactions = changedFactionData.map(_._1),
|
||||
newGeneratedTextRequests = changedFactionData.map(_._2)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
+310
-327
@@ -28,47 +28,14 @@ import net.eagle0.eagle.common.action_result_notification_details.{
|
||||
}
|
||||
import net.eagle0.eagle.common.action_result_notification_details.ShatteredArmyDetails.Reason.{
|
||||
SHATTERED_ARMY_REASON_BLIZZARD,
|
||||
SHATTERED_ARMY_REASON_UNKNOWN_UNSPECIFIED,
|
||||
SHATTERED_ARMY_REASON_WITHDRAW,
|
||||
Unrecognized
|
||||
SHATTERED_ARMY_REASON_WITHDRAW
|
||||
}
|
||||
import net.eagle0.eagle.common.action_result_type.ActionResultType.FACTION_DESTROYED
|
||||
import net.eagle0.eagle.internal.action_result.ActionResult
|
||||
import net.eagle0.eagle.internal.event_for_chronicle.{
|
||||
AllianceAcceptedEvent,
|
||||
BreakAllianceAmbassadorImprisonedEvent,
|
||||
BreakAllianceEvent,
|
||||
CapturedHeroExecutedEvent,
|
||||
CapturedHeroExiledEvent,
|
||||
CapturedHeroImprisonedEvent,
|
||||
EventForChronicle,
|
||||
EventForChronicleDetails,
|
||||
FactionDestroyedEvent,
|
||||
HeroDepartedEvent,
|
||||
InvitationAcceptedEvent,
|
||||
InvitationAmbassadorImprisonedEvent,
|
||||
OutlawApprehendedEvent,
|
||||
PrisonerExecutedEvent,
|
||||
PrisonersExchangedEvent,
|
||||
ProvinceConqueredEvent,
|
||||
ProvinceExpansionEvent,
|
||||
ProvinceHeldEvent,
|
||||
RansomPaidEvent,
|
||||
RansomRejectedEvent,
|
||||
ShatteredArmyEvent,
|
||||
SuppressBeastsFailedEvent,
|
||||
SwearBrotherhoodEvent,
|
||||
TruceAcceptedEvent,
|
||||
TruceAmbassadorImprisonedEvent,
|
||||
VassalExiledEvent
|
||||
}
|
||||
import net.eagle0.eagle.internal.event_for_chronicle.ShatteredArmyEvent.Reason.{
|
||||
SHATTERED_ARMY_EVENT_REASON_BLIZZARD,
|
||||
SHATTERED_ARMY_EVENT_REASON_UNKNOWN_UNSPECIFIED,
|
||||
SHATTERED_ARMY_EVENT_REASON_WITHDRAW
|
||||
}
|
||||
import net.eagle0.eagle.library.actions.impl.common.ActionWithResultingState
|
||||
import net.eagle0.eagle.library.GameHistory
|
||||
import net.eagle0.eagle.model.action_result.generated_text_request.chronicle_event.*
|
||||
import net.eagle0.eagle.model.proto_converters.date.DateConverter
|
||||
import net.eagle0.eagle.model.state.date.Date
|
||||
|
||||
object ChronicleEventGenerator {
|
||||
@@ -79,325 +46,341 @@ object ChronicleEventGenerator {
|
||||
|
||||
private def notificationEntries(
|
||||
awrs: ActionWithResultingState
|
||||
): Vector[EventForChronicle] = {
|
||||
val detailList: Vector[EventForChronicleDetails] =
|
||||
relevantNotifications(awrs.actionResult)
|
||||
.map(_.details)
|
||||
.collect {
|
||||
case AllianceAcceptedDetails(
|
||||
offeringFid,
|
||||
targetFid,
|
||||
ambassadorHid,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
AllianceAcceptedEvent(
|
||||
offeringFactionId = offeringFid,
|
||||
targetFactionId = targetFid,
|
||||
ambassadorHeroId = ambassadorHid
|
||||
)
|
||||
): Vector[ChronicleEvent] = {
|
||||
val date = DateConverter.fromProto(awrs.gameState.currentDate)
|
||||
relevantNotifications(awrs.actionResult)
|
||||
.map(_.details)
|
||||
.collect {
|
||||
case AllianceAcceptedDetails(
|
||||
offeringFid,
|
||||
targetFid,
|
||||
ambassadorHid,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
AllianceAcceptedChronicleEvent(
|
||||
date = date,
|
||||
offeringFactionId = offeringFid,
|
||||
targetFactionId = targetFid,
|
||||
ambassadorHeroId = ambassadorHid
|
||||
)
|
||||
|
||||
case CapturedHeroExiledDetails(
|
||||
capturedFromFactionId,
|
||||
provinceId,
|
||||
exiledHeroId,
|
||||
exilingFactionId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
CapturedHeroExiledEvent(
|
||||
capturedFromFactionId = capturedFromFactionId,
|
||||
provinceId = provinceId,
|
||||
exiledHeroId = exiledHeroId,
|
||||
exilingFactionId = exilingFactionId
|
||||
)
|
||||
case CapturedHeroExiledDetails(
|
||||
capturedFromFactionId,
|
||||
provinceId,
|
||||
exiledHeroId,
|
||||
exilingFactionId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
CapturedHeroExiledChronicleEvent(
|
||||
date = date,
|
||||
capturedFromFactionId = capturedFromFactionId,
|
||||
provinceId = provinceId,
|
||||
exiledHeroId = exiledHeroId,
|
||||
exilingFactionId = exilingFactionId
|
||||
)
|
||||
|
||||
case CapturedHeroExecutedDetails(
|
||||
capturedFromFactionId,
|
||||
provinceId,
|
||||
executedHeroId,
|
||||
executingFactionId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
CapturedHeroExecutedEvent(
|
||||
capturedFromFactionId = capturedFromFactionId,
|
||||
provinceId = provinceId,
|
||||
executedHeroId = executedHeroId,
|
||||
executingFactionId = executingFactionId
|
||||
)
|
||||
case CapturedHeroExecutedDetails(
|
||||
capturedFromFactionId,
|
||||
provinceId,
|
||||
executedHeroId,
|
||||
executingFactionId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
CapturedHeroExecutedChronicleEvent(
|
||||
date = date,
|
||||
capturedFromFactionId = capturedFromFactionId,
|
||||
provinceId = provinceId,
|
||||
executedHeroId = executedHeroId,
|
||||
executingFactionId = executingFactionId
|
||||
)
|
||||
|
||||
case CapturedHeroImprisonedDetails(
|
||||
capturedFromFactionId,
|
||||
provinceId,
|
||||
imprisonedHeroId,
|
||||
imprisoningFactionId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
CapturedHeroImprisonedEvent(
|
||||
capturedFromFactionId = capturedFromFactionId,
|
||||
provinceId = provinceId,
|
||||
imprisonedHeroId = imprisonedHeroId,
|
||||
imprisoningFactionId = imprisoningFactionId
|
||||
)
|
||||
case CapturedHeroImprisonedDetails(
|
||||
capturedFromFactionId,
|
||||
provinceId,
|
||||
imprisonedHeroId,
|
||||
imprisoningFactionId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
CapturedHeroImprisonedChronicleEvent(
|
||||
date = date,
|
||||
capturedFromFactionId = capturedFromFactionId,
|
||||
provinceId = provinceId,
|
||||
imprisonedHeroId = imprisonedHeroId,
|
||||
imprisoningFactionId = imprisoningFactionId
|
||||
)
|
||||
|
||||
case HeroDepartureDetails(
|
||||
departingHeroId,
|
||||
fromFactionId,
|
||||
provinceId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
HeroDepartedEvent(
|
||||
departingHeroId = departingHeroId,
|
||||
fromFactionId = fromFactionId,
|
||||
provinceId = provinceId
|
||||
)
|
||||
case HeroDepartureDetails(
|
||||
departingHeroId,
|
||||
fromFactionId,
|
||||
provinceId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
HeroDepartedChronicleEvent(
|
||||
date = date,
|
||||
departingHeroId = departingHeroId,
|
||||
fromFactionId = fromFactionId,
|
||||
provinceId = provinceId
|
||||
)
|
||||
|
||||
case SuppressBeastsFailedDetails(
|
||||
heroId,
|
||||
provinceId,
|
||||
_ /* singularName */,
|
||||
pluralName,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
SuppressBeastsFailedEvent(
|
||||
heroId = heroId,
|
||||
provinceId = provinceId,
|
||||
pluralName = pluralName
|
||||
)
|
||||
case SuppressBeastsFailedDetails(
|
||||
heroId,
|
||||
provinceId,
|
||||
singularName,
|
||||
pluralName,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
SuppressBeastsFailedChronicleEvent(
|
||||
date = date,
|
||||
heroId = heroId,
|
||||
provinceId = provinceId,
|
||||
singularName = singularName,
|
||||
pluralName = pluralName
|
||||
)
|
||||
|
||||
case PrisonerExecutedDetails(
|
||||
lastFactionId,
|
||||
provinceId,
|
||||
executedHeroId,
|
||||
executingFactionId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
PrisonerExecutedEvent(
|
||||
lastFactionId = lastFactionId,
|
||||
provinceId = provinceId,
|
||||
executedHeroId = executedHeroId,
|
||||
executingFactionId = executingFactionId
|
||||
)
|
||||
case PrisonerExecutedDetails(
|
||||
lastFactionId,
|
||||
provinceId,
|
||||
executedHeroId,
|
||||
executingFactionId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
PrisonerExecutedChronicleEvent(
|
||||
date = date,
|
||||
lastFactionId = lastFactionId,
|
||||
provinceId = provinceId,
|
||||
executedHeroId = executedHeroId,
|
||||
executingFactionId = executingFactionId
|
||||
)
|
||||
|
||||
case PrisonerExchangeDetails(
|
||||
hero1Id,
|
||||
hero1FactionId,
|
||||
hero1ProvinceId,
|
||||
hero2Id,
|
||||
hero2FactionId,
|
||||
hero2ProvinceId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
PrisonersExchangedEvent(
|
||||
hero1Id = hero1Id,
|
||||
hero1FactionId = hero1FactionId,
|
||||
hero1ProvinceId = hero1ProvinceId,
|
||||
hero2Id = hero2Id,
|
||||
hero2FactionId = hero2FactionId,
|
||||
hero2ProvinceId = hero2ProvinceId
|
||||
)
|
||||
case PrisonerExchangeDetails(
|
||||
hero1Id,
|
||||
hero1FactionId,
|
||||
hero1ProvinceId,
|
||||
hero2Id,
|
||||
hero2FactionId,
|
||||
hero2ProvinceId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
PrisonersExchangedChronicleEvent(
|
||||
date = date,
|
||||
hero1Id = hero1Id,
|
||||
hero1FactionId = hero1FactionId,
|
||||
hero1ProvinceId = hero1ProvinceId,
|
||||
hero2Id = hero2Id,
|
||||
hero2FactionId = hero2FactionId,
|
||||
hero2ProvinceId = hero2ProvinceId
|
||||
)
|
||||
|
||||
case ProvinceConqueredDetails(
|
||||
provinceId,
|
||||
conqueringFactionId,
|
||||
otherAttackingFactionIds,
|
||||
defendingFactionId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
ProvinceConqueredEvent(
|
||||
provinceId = provinceId,
|
||||
conqueringFactionId = conqueringFactionId,
|
||||
otherAttackingFactionIds = otherAttackingFactionIds,
|
||||
defendingFactionId = defendingFactionId
|
||||
)
|
||||
case ProvinceConqueredDetails(
|
||||
provinceId,
|
||||
conqueringFactionId,
|
||||
otherAttackingFactionIds,
|
||||
defendingFactionId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
ProvinceConqueredChronicleEvent(
|
||||
date = date,
|
||||
provinceId = provinceId,
|
||||
conqueringFactionId = conqueringFactionId,
|
||||
otherAttackingFactionIds = otherAttackingFactionIds.toVector,
|
||||
defendingFactionId = defendingFactionId
|
||||
)
|
||||
|
||||
case ProvinceExpansionDetails(
|
||||
provinceId,
|
||||
expandingFactionId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
ProvinceExpansionEvent(
|
||||
provinceId = provinceId,
|
||||
expandingFactionId = expandingFactionId
|
||||
)
|
||||
case ProvinceExpansionDetails(
|
||||
provinceId,
|
||||
expandingFactionId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
ProvinceExpansionChronicleEvent(
|
||||
date = date,
|
||||
provinceId = provinceId,
|
||||
expandingFactionId = expandingFactionId
|
||||
)
|
||||
|
||||
case ProvinceHeldDetails(
|
||||
provinceId,
|
||||
attackingFids,
|
||||
defendingFid,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
ProvinceHeldEvent(
|
||||
provinceId = provinceId,
|
||||
attackingFactionIds = attackingFids,
|
||||
defendingFactionId = defendingFid
|
||||
)
|
||||
case ProvinceHeldDetails(
|
||||
provinceId,
|
||||
attackingFids,
|
||||
defendingFid,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
ProvinceHeldChronicleEvent(
|
||||
date = date,
|
||||
provinceId = provinceId,
|
||||
attackingFactionIds = attackingFids.toVector,
|
||||
defendingFactionId = defendingFid
|
||||
)
|
||||
|
||||
case SwearBrotherhoodDetails(
|
||||
factionId,
|
||||
newBrotherHeroId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
SwearBrotherhoodEvent(
|
||||
factionId = factionId,
|
||||
newBrotherHeroId = newBrotherHeroId
|
||||
)
|
||||
case SwearBrotherhoodDetails(
|
||||
factionId,
|
||||
newBrotherHeroId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
SwearBrotherhoodChronicleEvent(
|
||||
date = date,
|
||||
factionId = factionId,
|
||||
newBrotherHeroId = newBrotherHeroId
|
||||
)
|
||||
|
||||
case InvitationAcceptedDetails(
|
||||
offeringFactionId,
|
||||
invitedFactionId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
InvitationAcceptedEvent(
|
||||
offeringFactionId = offeringFactionId,
|
||||
invitedFactionId = invitedFactionId
|
||||
)
|
||||
case InvitationAcceptedDetails(
|
||||
offeringFactionId,
|
||||
invitedFactionId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
InvitationAcceptedChronicleEvent(
|
||||
date = date,
|
||||
offeringFactionId = offeringFactionId,
|
||||
invitedFactionId = invitedFactionId
|
||||
)
|
||||
|
||||
case InvitationAmbassadorImprisonedDetails(
|
||||
offeringFactionId,
|
||||
invitedFactionId,
|
||||
ambassadorHeroId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
InvitationAmbassadorImprisonedEvent(
|
||||
offeringFactionId = offeringFactionId,
|
||||
invitedFactionId = invitedFactionId,
|
||||
ambassadorHeroId = ambassadorHeroId
|
||||
)
|
||||
case InvitationAmbassadorImprisonedDetails(
|
||||
offeringFactionId,
|
||||
invitedFactionId,
|
||||
ambassadorHeroId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
InvitationAmbassadorImprisonedChronicleEvent(
|
||||
date = date,
|
||||
offeringFactionId = offeringFactionId,
|
||||
invitedFactionId = invitedFactionId,
|
||||
ambassadorHeroId = ambassadorHeroId
|
||||
)
|
||||
|
||||
case TruceAcceptedDetails(
|
||||
offeringFactionId,
|
||||
targetFactionId,
|
||||
ambassadorHeroId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
TruceAcceptedEvent(
|
||||
offeringFactionId = offeringFactionId,
|
||||
targetFactionId = targetFactionId,
|
||||
ambassadorHeroId = ambassadorHeroId
|
||||
)
|
||||
case TruceAcceptedDetails(
|
||||
offeringFactionId,
|
||||
targetFactionId,
|
||||
ambassadorHeroId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
TruceAcceptedChronicleEvent(
|
||||
date = date,
|
||||
offeringFactionId = offeringFactionId,
|
||||
targetFactionId = targetFactionId,
|
||||
ambassadorHeroId = ambassadorHeroId
|
||||
)
|
||||
|
||||
case TruceAmbassadorImprisonedDetails(
|
||||
offeringFactionId,
|
||||
targetFactionId,
|
||||
ambassadorHeroId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
TruceAmbassadorImprisonedEvent(
|
||||
offeringFactionId = offeringFactionId,
|
||||
targetFactionId = targetFactionId,
|
||||
ambassadorHeroId = ambassadorHeroId
|
||||
)
|
||||
case TruceAmbassadorImprisonedDetails(
|
||||
offeringFactionId,
|
||||
targetFactionId,
|
||||
ambassadorHeroId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
TruceAmbassadorImprisonedChronicleEvent(
|
||||
date = date,
|
||||
offeringFactionId = offeringFactionId,
|
||||
targetFactionId = targetFactionId,
|
||||
ambassadorHeroId = ambassadorHeroId
|
||||
)
|
||||
|
||||
case BreakAllianceAcceptedDetails(
|
||||
offeringFactionId,
|
||||
targetFactionId,
|
||||
ambassadorHeroId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
BreakAllianceEvent(
|
||||
offeringFactionId = offeringFactionId,
|
||||
targetFactionId = targetFactionId,
|
||||
ambassadorHeroId = ambassadorHeroId
|
||||
)
|
||||
case BreakAllianceAcceptedDetails(
|
||||
offeringFactionId,
|
||||
targetFactionId,
|
||||
ambassadorHeroId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
BreakAllianceChronicleEvent(
|
||||
date = date,
|
||||
offeringFactionId = offeringFactionId,
|
||||
targetFactionId = targetFactionId,
|
||||
ambassadorHeroId = ambassadorHeroId
|
||||
)
|
||||
|
||||
case BreakAllianceAmbassadorImprisonedDetails(
|
||||
offeringFactionId,
|
||||
targetFactionId,
|
||||
ambassadorHeroId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
BreakAllianceAmbassadorImprisonedEvent(
|
||||
offeringFactionId = offeringFactionId,
|
||||
targetFactionId = targetFactionId,
|
||||
ambassadorHeroId = ambassadorHeroId
|
||||
)
|
||||
case BreakAllianceAmbassadorImprisonedDetails(
|
||||
offeringFactionId,
|
||||
targetFactionId,
|
||||
ambassadorHeroId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
BreakAllianceAmbassadorImprisonedChronicleEvent(
|
||||
date = date,
|
||||
offeringFactionId = offeringFactionId,
|
||||
targetFactionId = targetFactionId,
|
||||
ambassadorHeroId = ambassadorHeroId
|
||||
)
|
||||
|
||||
case OutlawApprehendedDetails(
|
||||
factionId,
|
||||
provinceId,
|
||||
apprehendedHeroId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
OutlawApprehendedEvent(
|
||||
factionId = factionId,
|
||||
provinceId = provinceId,
|
||||
apprehendedHeroId = apprehendedHeroId
|
||||
)
|
||||
case OutlawApprehendedDetails(
|
||||
factionId,
|
||||
provinceId,
|
||||
apprehendedHeroId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
OutlawApprehendedChronicleEvent(
|
||||
date = date,
|
||||
factionId = factionId,
|
||||
provinceId = provinceId,
|
||||
apprehendedHeroId = apprehendedHeroId
|
||||
)
|
||||
|
||||
case RansomPaidDetails(
|
||||
ransomedHeroId,
|
||||
ransomPaidByFactionId,
|
||||
ransomPaidToFactionId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
RansomPaidEvent(
|
||||
ransomedHeroId = ransomedHeroId,
|
||||
ransomPaidByFactionId = ransomPaidByFactionId,
|
||||
ransomPaidToFactionId = ransomPaidToFactionId
|
||||
)
|
||||
case RansomPaidDetails(
|
||||
ransomedHeroId,
|
||||
ransomPaidByFactionId,
|
||||
ransomPaidToFactionId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
RansomPaidChronicleEvent(
|
||||
date = date,
|
||||
ransomedHeroId = ransomedHeroId,
|
||||
ransomPaidByFactionId = ransomPaidByFactionId,
|
||||
ransomPaidToFactionId = ransomPaidToFactionId
|
||||
)
|
||||
|
||||
case RansomRejectedDetails(
|
||||
ransomedHeroId,
|
||||
ransomRefusedByFactionId,
|
||||
ransomOfferingFactionId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
RansomRejectedEvent(
|
||||
ransomedHeroId = ransomedHeroId,
|
||||
ransomRejectedByFactionId = ransomRefusedByFactionId,
|
||||
ransomOfferingFactionId = ransomOfferingFactionId
|
||||
)
|
||||
case RansomRejectedDetails(
|
||||
ransomedHeroId,
|
||||
ransomRefusedByFactionId,
|
||||
ransomOfferingFactionId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
RansomRejectedChronicleEvent(
|
||||
date = date,
|
||||
ransomedHeroId = ransomedHeroId,
|
||||
ransomRejectedByFactionId = ransomRefusedByFactionId,
|
||||
ransomOfferingFactionId = ransomOfferingFactionId
|
||||
)
|
||||
|
||||
case ShatteredArmyDetails(
|
||||
factionId,
|
||||
provinceId,
|
||||
reason,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
ShatteredArmyEvent(
|
||||
factionId = factionId,
|
||||
provinceId = provinceId,
|
||||
reason = reason match {
|
||||
case SHATTERED_ARMY_REASON_BLIZZARD =>
|
||||
SHATTERED_ARMY_EVENT_REASON_BLIZZARD
|
||||
case SHATTERED_ARMY_REASON_UNKNOWN_UNSPECIFIED =>
|
||||
SHATTERED_ARMY_EVENT_REASON_UNKNOWN_UNSPECIFIED
|
||||
case SHATTERED_ARMY_REASON_WITHDRAW =>
|
||||
SHATTERED_ARMY_EVENT_REASON_WITHDRAW
|
||||
case Unrecognized(value) =>
|
||||
ShatteredArmyEvent.Reason.Unrecognized(value)
|
||||
}
|
||||
)
|
||||
case ShatteredArmyDetails(
|
||||
factionId,
|
||||
provinceId,
|
||||
reason,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
ShatteredArmyChronicleEvent(
|
||||
date = date,
|
||||
factionId = factionId,
|
||||
provinceId = provinceId,
|
||||
reason = reason match {
|
||||
case SHATTERED_ARMY_REASON_BLIZZARD =>
|
||||
ShatteredArmyChronicleEvent.Blizzard
|
||||
case SHATTERED_ARMY_REASON_WITHDRAW =>
|
||||
ShatteredArmyChronicleEvent.Withdraw
|
||||
case _ =>
|
||||
ShatteredArmyChronicleEvent.Withdraw // Default fallback
|
||||
}
|
||||
)
|
||||
|
||||
case VassalExiledDetails(
|
||||
factionId,
|
||||
provinceId,
|
||||
exiledHeroId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
VassalExiledEvent(
|
||||
factionId = factionId,
|
||||
provinceId = provinceId,
|
||||
exiledHeroId = exiledHeroId
|
||||
)
|
||||
}
|
||||
|
||||
detailList.map { details =>
|
||||
EventForChronicle(
|
||||
awrs.gameState.currentDate,
|
||||
details = details
|
||||
)
|
||||
}
|
||||
case VassalExiledDetails(
|
||||
factionId,
|
||||
provinceId,
|
||||
exiledHeroId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
VassalExiledChronicleEvent(
|
||||
date = date,
|
||||
factionId = factionId,
|
||||
provinceId = provinceId,
|
||||
exiledHeroId = exiledHeroId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private def basicActionResultEntries(
|
||||
awrs: ActionWithResultingState
|
||||
): Vector[EventForChronicle] =
|
||||
): Vector[ChronicleEvent] =
|
||||
awrs.actionResult.`type` match {
|
||||
case FACTION_DESTROYED =>
|
||||
val date = DateConverter.fromProto(awrs.gameState.currentDate)
|
||||
awrs.actionResult.removedFactionIds.map { factionId =>
|
||||
EventForChronicle(
|
||||
date = awrs.gameState.currentDate,
|
||||
details = FactionDestroyedEvent(factionId = factionId)
|
||||
FactionDestroyedChronicleEvent(
|
||||
date = date,
|
||||
factionId = factionId
|
||||
)
|
||||
}.toVector
|
||||
|
||||
@@ -407,7 +390,7 @@ object ChronicleEventGenerator {
|
||||
def eventTextEntries(
|
||||
gameHistory: GameHistory,
|
||||
since: Date
|
||||
): Vector[EventForChronicle] =
|
||||
): Vector[ChronicleEvent] =
|
||||
gameHistory.sinceDate(since).flatMap { (awrs: ActionWithResultingState) =>
|
||||
notificationEntries(awrs) ++ basicActionResultEntries(awrs)
|
||||
}
|
||||
|
||||
+8
-19
@@ -22,7 +22,6 @@ import net.eagle0.eagle.model.action_result.{ActionResultT, NotificationDetails,
|
||||
import net.eagle0.eagle.model.action_result.changed_province.concrete.ChangedProvinceC
|
||||
import net.eagle0.eagle.model.action_result.concrete.{ActionResultC, ChangedFactionC, ChangedHeroC, NotificationC}
|
||||
import net.eagle0.eagle.model.action_result.types.{CapturedHeroResolvedResultType, EndAftermathPhaseResultType}
|
||||
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
|
||||
import net.eagle0.eagle.model.state.{BattleRevelation, RoundPhase}
|
||||
import net.eagle0.eagle.model.state.faction.FactionRelationship
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
@@ -350,15 +349,10 @@ case class EndBattleAftermathPhaseAction(
|
||||
actionResultApplier: ActionResultApplier
|
||||
) extends ProtolessRandomSequentialResultsAction {
|
||||
|
||||
// Revelation changes still need proto for ProvinceViewFilter
|
||||
private def revelationChange(
|
||||
battleRevelation: BattleRevelation,
|
||||
gs: GameState
|
||||
): RevelationChange = {
|
||||
// Lazy conversion to proto for ProvinceViewFilter calls
|
||||
lazy val gsProto = GameStateConverter.toProto(gs)
|
||||
lazy val provinceProto = gsProto.provinces(battleRevelation.provinceId)
|
||||
|
||||
): RevelationChange =
|
||||
RevelationChange(
|
||||
changedFaction = battleRevelation.revelationType match {
|
||||
case Unknown =>
|
||||
@@ -369,25 +363,21 @@ case class EndBattleAftermathPhaseAction(
|
||||
updatedReconnedProvinces = Vector(
|
||||
ProvinceViewFilter
|
||||
.withdrawnFromProvinceView(
|
||||
province = provinceProto,
|
||||
gs = gsProto,
|
||||
province = gs.provinces(battleRevelation.provinceId),
|
||||
gs = gs,
|
||||
factionId = battleRevelation.revealedToFactionId
|
||||
)
|
||||
.withAsOf(gsProto.currentDate.get)
|
||||
.copy(asOf = gs.currentDate)
|
||||
)
|
||||
)
|
||||
case DidBattle =>
|
||||
ChangedFactionC(
|
||||
factionId = battleRevelation.revealedToFactionId,
|
||||
updatedReconnedProvinces = Vector(
|
||||
ProvinceViewFilter
|
||||
.filteredProvinceView(
|
||||
provinceProto,
|
||||
gsProto
|
||||
// FIXME: setting factionId to None right now to grab the full info. This
|
||||
// probably isn't exactly what we want.
|
||||
)
|
||||
.withAsOf(gsProto.currentDate.get)
|
||||
ProvinceViewFilter.filteredProvinceView(
|
||||
gs.provinces(battleRevelation.provinceId),
|
||||
gs
|
||||
)
|
||||
)
|
||||
)
|
||||
},
|
||||
@@ -396,7 +386,6 @@ case class EndBattleAftermathPhaseAction(
|
||||
removedBattleRevelations = Vector(battleRevelation)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
def revelationChanges(gs: GameState): Vector[RevelationChange] =
|
||||
for {
|
||||
|
||||
-7
@@ -1,14 +1,11 @@
|
||||
package net.eagle0.eagle.library.actions.impl.action
|
||||
|
||||
import net.eagle0.eagle.internal.game_state.GameState as GameStateProto
|
||||
import net.eagle0.eagle.library.actions.impl.common.ProtolessSequentialResultsAction
|
||||
import net.eagle0.eagle.library.util.faction_utils.FactionUtils
|
||||
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
|
||||
object HeroBackstoryUpdateActionGenerator {
|
||||
|
||||
/** Creates action from Scala GameState (preferred) */
|
||||
def apply(gameState: GameState): ProtolessSequentialResultsAction = {
|
||||
val factions = gameState.factions.values.toVector
|
||||
HeroBackstoryUpdateAction(
|
||||
@@ -25,8 +22,4 @@ object HeroBackstoryUpdateActionGenerator {
|
||||
.flatMap(_.rulingFactionId)
|
||||
)
|
||||
}
|
||||
|
||||
/** Creates action from proto GameState (for sequencer callbacks) */
|
||||
def fromGameState(gameState: GameStateProto): ProtolessSequentialResultsAction =
|
||||
apply(GameStateConverter.fromProto(gameState))
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ import net.eagle0.eagle.model.action_result.concrete.{
|
||||
import net.eagle0.eagle.model.action_result.generated_text_request.{ChronicleUpdatePreviousEntry, LlmRequestT}
|
||||
import net.eagle0.eagle.model.action_result.types.NewRoundActionResultType
|
||||
import net.eagle0.eagle.model.action_result.ActionResultT
|
||||
import net.eagle0.eagle.model.proto_converters.generated_text_request.chronicle_event.ChronicleEventConverter
|
||||
import net.eagle0.eagle.model.state.chronicle_entry.ChronicleEntry
|
||||
import net.eagle0.eagle.model.state.date.Date
|
||||
import net.eagle0.eagle.model.state.faction.FactionRelationship
|
||||
@@ -117,14 +116,12 @@ case class NewRoundAction(
|
||||
generatedTextId = entry.generatedTextId
|
||||
)
|
||||
},
|
||||
new_entries = ChronicleEventGenerator
|
||||
.eventTextEntries(
|
||||
gameHistory = gameHistory,
|
||||
since = gs.chronicleEntries.lastOption
|
||||
.map(_.date)
|
||||
.getOrElse(Date(year = 0, month = Date.Month.January))
|
||||
)
|
||||
.map(ChronicleEventConverter.fromProto)
|
||||
new_entries = ChronicleEventGenerator.eventTextEntries(
|
||||
gameHistory = gameHistory,
|
||||
since = gs.chronicleEntries.lastOption
|
||||
.map(_.date)
|
||||
.getOrElse(Date(year = 0, month = Date.Month.January))
|
||||
)
|
||||
)
|
||||
)
|
||||
else Vector()
|
||||
|
||||
+4
-13
@@ -1,7 +1,6 @@
|
||||
package net.eagle0.eagle.library.actions.impl.action
|
||||
|
||||
import net.eagle0.common.{FunctionalRandom, RandomState}
|
||||
import net.eagle0.eagle.internal.game_state.GameState as GameStateProto
|
||||
import net.eagle0.eagle.library.actions.applier.ActionResultApplier
|
||||
import net.eagle0.eagle.library.actions.impl.common.ProtolessRandomSequentialResultsAction
|
||||
import net.eagle0.eagle.library.actions.random_state_sequencer.RandomStateSequencer
|
||||
@@ -11,7 +10,6 @@ import net.eagle0.eagle.model.action_result.changed_province.concrete.ChangedPro
|
||||
import net.eagle0.eagle.model.action_result.concrete.{ActionResultC, ChangedFactionC, ClientTextVisibilityExtensionC}
|
||||
import net.eagle0.eagle.model.action_result.types.{EndReconResolutionPhaseResultType, ReconSucceededResultType}
|
||||
import net.eagle0.eagle.model.action_result.ActionResultT
|
||||
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.province.{IncomingEndTurnAction, IncomingRecon}
|
||||
import net.eagle0.eagle.model.state.RoundPhase
|
||||
@@ -22,9 +20,6 @@ case class PerformReconResolutionAction(
|
||||
actionResultApplier: ActionResultApplier
|
||||
) extends ProtolessRandomSequentialResultsAction {
|
||||
|
||||
// Proto GameState still needed for ProvinceViewFilter.filteredProvinceView
|
||||
private lazy val startingGameStateProto: GameStateProto = GameStateConverter.toProto(gameState)
|
||||
|
||||
override protected def randomResults(
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Vector[ActionResultT]] =
|
||||
@@ -94,14 +89,10 @@ case class PerformReconResolutionAction(
|
||||
ChangedFactionC(
|
||||
factionId = fromFactionId,
|
||||
updatedReconnedProvinces = Vector(
|
||||
ProvinceViewFilter
|
||||
.filteredProvinceView(
|
||||
startingGameStateProto.provinces(provinceId),
|
||||
startingGameStateProto
|
||||
// FIXME: setting factionId to None right now to grab the full info. This
|
||||
// probably isn't exactly what we want.
|
||||
)
|
||||
.withAsOf(startingGameStateProto.currentDate.get)
|
||||
ProvinceViewFilter.filteredProvinceView(
|
||||
gameState.provinces(provinceId),
|
||||
gameState
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
-84
@@ -1,84 +0,0 @@
|
||||
package net.eagle0.eagle.library.actions.impl.action
|
||||
|
||||
import net.eagle0.common.{FunctionalRandom, RandomState}
|
||||
import net.eagle0.eagle.common.action_result_notification_details.{Notification, OutlawSpottedDetails}
|
||||
import net.eagle0.eagle.common.action_result_type.ActionResultType.HERO_MOVED
|
||||
import net.eagle0.eagle.common.unaffiliated_hero_type.UnaffiliatedHeroType.UNAFFILIATED_HERO_OUTLAW
|
||||
import net.eagle0.eagle.internal.action_result.ActionResult
|
||||
import net.eagle0.eagle.internal.changed_hero.ChangedHero
|
||||
import net.eagle0.eagle.internal.changed_hero.ChangedHero.Vigor
|
||||
import net.eagle0.eagle.internal.changed_province.ChangedProvince
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.internal.unaffiliated_hero.UnaffiliatedHero
|
||||
import net.eagle0.eagle.library.settings.FreeHeroMoveVigorCost
|
||||
import net.eagle0.eagle.library.util.unaffiliated_hero.UnaffiliatedHeroUtils
|
||||
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
|
||||
import net.eagle0.eagle.model.proto_converters.UnaffiliatedHeroConverter
|
||||
import net.eagle0.eagle.ProvinceId
|
||||
|
||||
case class UnaffiliatedHeroMovedAction(
|
||||
uh: UnaffiliatedHero,
|
||||
fromProvinceId: ProvinceId,
|
||||
toProvinceId: ProvinceId,
|
||||
gameState: GameState
|
||||
) {
|
||||
|
||||
private val affectedProvinces =
|
||||
Vector(fromProvinceId, toProvinceId).map(gameState.provinces)
|
||||
private val affectedFactions = affectedProvinces.flatMap(_.rulingFactionId)
|
||||
private val notifications = Option
|
||||
.when(uh.`type` == UNAFFILIATED_HERO_OUTLAW && affectedFactions.nonEmpty) {
|
||||
Notification(
|
||||
details = OutlawSpottedDetails(
|
||||
outlawHeroId = uh.heroId,
|
||||
provinceId = toProvinceId
|
||||
),
|
||||
targetFactions = affectedFactions
|
||||
.map(fid => Notification.TargetFaction(factionId = fid))
|
||||
)
|
||||
}
|
||||
.toVector
|
||||
|
||||
def immediateExecute(
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[ActionResult] =
|
||||
computeNewRecruitmentInfo(functionalRandom).map { newRecruitmentInfo =>
|
||||
ActionResult(
|
||||
`type` = HERO_MOVED,
|
||||
changedHeroes = Vector(
|
||||
ChangedHero(
|
||||
id = uh.heroId,
|
||||
vigor = Vigor.VigorDelta(-FreeHeroMoveVigorCost.doubleValue)
|
||||
)
|
||||
),
|
||||
changedProvinces = Vector(
|
||||
ChangedProvince(
|
||||
id = fromProvinceId,
|
||||
removedUnaffiliatedHeroIds = Vector(uh.heroId)
|
||||
),
|
||||
ChangedProvince(
|
||||
id = toProvinceId,
|
||||
newUnaffiliatedHeroes = Vector(
|
||||
uh.update(
|
||||
_.recruitmentInfo := UnaffiliatedHeroConverter.recruitmentInfoToProto(newRecruitmentInfo)
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
notificationsToDeliver = notifications
|
||||
)
|
||||
}
|
||||
|
||||
private def computeNewRecruitmentInfo(
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[net.eagle0.eagle.model.state.unaffiliated_hero.RecruitmentInfo] = {
|
||||
val scalaGameState = GameStateConverter.fromProto(gameState)
|
||||
UnaffiliatedHeroUtils.newRecruitmentInfo(
|
||||
gameState = scalaGameState,
|
||||
provinceId = toProvinceId,
|
||||
unaffiliatedHero = UnaffiliatedHeroConverter.fromProto(uh),
|
||||
hero = scalaGameState.heroes(uh.heroId),
|
||||
functionalRandom = functionalRandom
|
||||
)
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -119,8 +119,8 @@ object ResolveRansomOfferCommand {
|
||||
case Accepted =>
|
||||
NotificationDetails.RansomPaid(
|
||||
ransomedHeroId = ransomOffer.prisonerToBeRansomed.prisonerHeroId,
|
||||
ransomPaidByFactionId = actingFactionId,
|
||||
ransomPaidToFactionId = ransomOffer.originatingFactionId
|
||||
ransomPaidByFactionId = ransomOffer.originatingFactionId,
|
||||
ransomPaidToFactionId = actingFactionId
|
||||
)
|
||||
|
||||
case Rejected =>
|
||||
|
||||
@@ -347,6 +347,24 @@ scala_library(
|
||||
deps = [],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "new_faction_head_prompt_generator",
|
||||
srcs = ["NewFactionHeadPromptGenerator.scala"],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
":faction_info_utilities",
|
||||
":generator_utilities",
|
||||
":hero_description_generator",
|
||||
":hero_info_utilities",
|
||||
":llm_prompt_generator",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:client_text_store",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:text_generation_result",
|
||||
],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "please_recruit_me_prompt_generator",
|
||||
srcs = ["PleaseRecruitMePromptGenerator.scala"],
|
||||
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package net.eagle0.eagle.library.actions.llm_prompt_generators
|
||||
|
||||
import net.eagle0.eagle.client_text.{ClientTextStore, TextGenerationResult}
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.internal.generated_text_request.NewFactionHeadMessage
|
||||
|
||||
case class NewFactionHeadPromptGenerator(
|
||||
newFactionHeadMessage: NewFactionHeadMessage,
|
||||
gameState: GameState,
|
||||
clientTextStore: ClientTextStore
|
||||
) extends LLMPromptGenerator {
|
||||
private val newHeadHero = HeroInfoUtilities.getHero(
|
||||
newFactionHeadMessage.newHeadHeroId,
|
||||
gameState
|
||||
)
|
||||
private val previousHeadHero = HeroInfoUtilities.getHero(
|
||||
newFactionHeadMessage.previousHeadHeroId,
|
||||
gameState
|
||||
)
|
||||
private val faction = FactionInfoUtilities.getFaction(
|
||||
newFactionHeadMessage.factionId,
|
||||
gameState
|
||||
)
|
||||
|
||||
override def generate: TextGenerationResult = for {
|
||||
newHeadHeroDescription <- HeroDescriptionGenerator.description(
|
||||
heroId = newFactionHeadMessage.newHeadHeroId,
|
||||
gameState = gameState,
|
||||
clientTextStore = clientTextStore
|
||||
)
|
||||
previousHeadHeroDescription <- HeroDescriptionGenerator.description(
|
||||
heroId = newFactionHeadMessage.previousHeadHeroId,
|
||||
gameState = gameState,
|
||||
clientTextStore = clientTextStore
|
||||
)
|
||||
newHeadHeroName <- clientTextStore.getText(newHeadHero.nameTextId)
|
||||
previousHeadHeroName <- clientTextStore.getText(previousHeadHero.nameTextId)
|
||||
setup <- GeneratorUtilities.basicSetup(gameState, clientTextStore)
|
||||
} yield {
|
||||
val previousFactionName = newFactionHeadMessage.previousFactionName
|
||||
val newFactionName = Option(newFactionHeadMessage.newFactionName).filter(_.nonEmpty)
|
||||
|
||||
val renameClause = newFactionName match {
|
||||
case Some(newName) =>
|
||||
s" $newHeadHeroName is also renaming the faction from \"$previousFactionName\" to \"$newName\"."
|
||||
case None => ""
|
||||
}
|
||||
|
||||
val possP = HeroDescriptionGenerator.possessivePronoun(newHeadHero.pronounGender)
|
||||
|
||||
val inputText =
|
||||
s"""$setup
|
||||
|
|
||||
|NEW FACTION HEAD:
|
||||
|$newHeadHeroDescription
|
||||
|
|
||||
|PREVIOUS FACTION HEAD (EXECUTED):
|
||||
|$previousHeadHeroDescription
|
||||
|
|
||||
|$previousHeadHeroName, the previous head of the faction "$previousFactionName", has been executed.
|
||||
|$newHeadHeroName has now become the new head of the faction.$renameClause
|
||||
|
|
||||
|Write a message from $newHeadHeroName to be delivered to the world, declaring $possP new leadership
|
||||
|in the wake of $previousHeadHeroName's execution. The message should reflect $possP personality and
|
||||
|ambitions, and may reference the circumstances of $possP rise to power or their relationship with
|
||||
|the previous leader. If the faction is being renamed, the declaration should explain why the new
|
||||
|name represents $possP vision for the faction.
|
||||
|
|
||||
|${GeneratorUtilities.limitations}""".stripMargin
|
||||
|
||||
inputText
|
||||
}
|
||||
}
|
||||
@@ -45,11 +45,10 @@ scala_library(
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/api/command/util:appropriate_battalions_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:battalion_type_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:profession_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:battalion_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:hero_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -64,10 +63,12 @@ scala_library(
|
||||
],
|
||||
exports = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:battalion_type_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type_id",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:battalion_type_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type_id",
|
||||
],
|
||||
)
|
||||
@@ -97,6 +98,7 @@ scala_library(
|
||||
deps = [
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type_id",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
|
||||
],
|
||||
)
|
||||
@@ -386,8 +388,12 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/util:__pkg__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/stat_with_condition",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/views:stat_with_condition_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/stat_with_condition",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -10,25 +10,25 @@ import net.eagle0.eagle.api.command.util.appropriate_battalions.SuitableBattalio
|
||||
RESTRICTIVE,
|
||||
SUBOPTIMAL
|
||||
}
|
||||
import net.eagle0.eagle.common.battalion_type.BattalionType
|
||||
import net.eagle0.eagle.common.profession.Profession.{ENGINEER, MAGE, NECROMANCER, RANGER}
|
||||
import net.eagle0.eagle.internal.battalion.Battalion
|
||||
import net.eagle0.eagle.internal.hero.Hero
|
||||
import net.eagle0.eagle.model.state.battalion.BattalionT
|
||||
import net.eagle0.eagle.model.state.hero.HeroT
|
||||
import net.eagle0.eagle.model.state.hero.Profession.{Engineer, Mage, Necromancer, Ranger}
|
||||
import net.eagle0.eagle.model.state.BattalionType
|
||||
import net.eagle0.eagle.HeroId
|
||||
|
||||
object BattalionSuitability {
|
||||
def suitabilityLevel(h: Hero, bt: BattalionType): SuitabilityLevel =
|
||||
def suitabilityLevel(h: HeroT, bt: BattalionType): SuitabilityLevel =
|
||||
h.profession match {
|
||||
case MAGE | NECROMANCER =>
|
||||
case Mage | Necromancer =>
|
||||
if !bt.allowsCasting then RESTRICTIVE
|
||||
else if bt.alwaysArcheryCapable then SUBOPTIMAL
|
||||
else OPTIMAL
|
||||
|
||||
case ENGINEER =>
|
||||
case Engineer =>
|
||||
if bt.alwaysArcheryCapable then SUBOPTIMAL
|
||||
else OPTIMAL
|
||||
|
||||
case RANGER =>
|
||||
case Ranger =>
|
||||
if !bt.allowsStealth then RESTRICTIVE
|
||||
else OPTIMAL
|
||||
|
||||
@@ -36,8 +36,8 @@ object BattalionSuitability {
|
||||
}
|
||||
|
||||
def suitableBattalions(
|
||||
h: Hero,
|
||||
bs: Vector[Battalion],
|
||||
h: HeroT,
|
||||
bs: Vector[BattalionT],
|
||||
bts: Vector[BattalionType]
|
||||
): SuitableBattalions =
|
||||
SuitableBattalions(
|
||||
@@ -45,14 +45,14 @@ object BattalionSuitability {
|
||||
BattalionIdWithSuitability(
|
||||
battalionId = b.id,
|
||||
level = BattalionSuitability
|
||||
.suitabilityLevel(h, bts.find(_.typeId == b.`type`).get)
|
||||
.suitabilityLevel(h, bts.find(_.typeId == b.typeId).get)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
def suitableBattalionsForHeroes(
|
||||
hs: Vector[Hero],
|
||||
bs: Vector[Battalion],
|
||||
hs: Vector[HeroT],
|
||||
bs: Vector[BattalionT],
|
||||
bts: Vector[BattalionType]
|
||||
): Map[HeroId, SuitableBattalions] =
|
||||
hs.map(h => h.id -> suitableBattalions(h, bs, bts)).toMap
|
||||
|
||||
@@ -4,7 +4,7 @@ import net.eagle0.eagle.common.battalion_type.{
|
||||
BattalionType as BattalionTypeProto,
|
||||
BattalionTypeId as BattalionTypeIdProto
|
||||
}
|
||||
import net.eagle0.eagle.model.state.BattalionTypeId
|
||||
import net.eagle0.eagle.model.state.{BattalionType, BattalionTypeId}
|
||||
|
||||
object BattalionTypeFinder {
|
||||
def battalionType(
|
||||
@@ -18,4 +18,10 @@ object BattalionTypeFinder {
|
||||
battalionTypes: Vector[BattalionTypeProto]
|
||||
): BattalionTypeProto =
|
||||
battalionTypes.find(_.typeId == battalionTypeId).get
|
||||
|
||||
def findBattalionType(
|
||||
battalionTypeId: BattalionTypeId,
|
||||
battalionTypes: Vector[BattalionType]
|
||||
): BattalionType =
|
||||
battalionTypes.find(_.typeId == battalionTypeId).get
|
||||
}
|
||||
|
||||
@@ -1,9 +1,24 @@
|
||||
package net.eagle0.eagle.library.util
|
||||
|
||||
import net.eagle0.eagle.model.state.{BattalionType, BattalionTypeId}
|
||||
import net.eagle0.eagle.model.state.battalion.BattalionT
|
||||
import net.eagle0.eagle.model.state.BattalionType
|
||||
import net.eagle0.eagle.model.state.BattalionTypeId.{
|
||||
HeavyCavalry,
|
||||
HeavyInfantry,
|
||||
LightCavalry,
|
||||
LightInfantry,
|
||||
Longbowmen
|
||||
}
|
||||
|
||||
object BattalionUtils {
|
||||
val powerMultiplier: Map[BattalionTypeId, Double] = Map(
|
||||
LightInfantry -> 1.0,
|
||||
HeavyInfantry -> 2.0,
|
||||
LightCavalry -> 2.0,
|
||||
HeavyCavalry -> 4.0,
|
||||
Longbowmen -> 1.5
|
||||
)
|
||||
|
||||
def monthlyConsumedFood(
|
||||
battalions: Iterable[BattalionT],
|
||||
bts: Vector[BattalionType]
|
||||
@@ -21,4 +36,9 @@ object BattalionUtils {
|
||||
val battalionType = bts.find(_.typeId == battalion.typeId).get
|
||||
battalion.size.toDouble * battalionType.monthlyFoodCost
|
||||
}
|
||||
|
||||
def power(batt: BattalionT): Double =
|
||||
powerMultiplier(
|
||||
batt.typeId
|
||||
) * batt.size * (1.0 + batt.armament / 100.0) + (1.0 + batt.training / 100.0)
|
||||
}
|
||||
|
||||
@@ -194,7 +194,8 @@ object GameStateViewDiffer {
|
||||
changedFactionRelationships = after.factionRelationships.diff(before.factionRelationships),
|
||||
addedIncomingDiplomacyOffers = after.incomingDiplomacyOffers.diff(before.incomingDiplomacyOffers),
|
||||
removedIncomingDiplomacyOffers = before.incomingDiplomacyOffers.diff(after.incomingDiplomacyOffers),
|
||||
newFocusProvinceId = after.focusProvinceId
|
||||
newFocusProvinceId = after.focusProvinceId,
|
||||
newName = optionalDiff(before.name, after.name)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package net.eagle0.eagle.library.util
|
||||
|
||||
import net.eagle0.eagle.model.view.stat_with_condition.{Condition, StatWithCondition as StatWithConditionT}
|
||||
import net.eagle0.eagle.views.stat_with_condition.StatWithCondition
|
||||
import net.eagle0.eagle.views.stat_with_condition.StatWithCondition.Condition.{HIGH, LOW, MEDIUM}
|
||||
|
||||
@@ -11,6 +12,13 @@ object StatWithConditionUtils {
|
||||
minHigh = 55
|
||||
)
|
||||
|
||||
def supportScala(support: Double): StatWithConditionT =
|
||||
scWithRangesScala(
|
||||
support.floor.toInt,
|
||||
minMedium = 40,
|
||||
minHigh = 55
|
||||
)
|
||||
|
||||
def scWithRanges(
|
||||
stat: Int,
|
||||
minMedium: Int,
|
||||
@@ -23,4 +31,17 @@ object StatWithConditionUtils {
|
||||
else if stat >= minMedium then MEDIUM
|
||||
else LOW
|
||||
)
|
||||
|
||||
def scWithRangesScala(
|
||||
stat: Int,
|
||||
minMedium: Int,
|
||||
minHigh: Int
|
||||
): StatWithConditionT =
|
||||
StatWithConditionT(
|
||||
stat = stat,
|
||||
condition =
|
||||
if stat >= minHigh then Condition.High
|
||||
else if stat >= minMedium then Condition.Medium
|
||||
else Condition.Low
|
||||
)
|
||||
}
|
||||
|
||||
+65
-18
@@ -3,41 +3,42 @@ package net.eagle0.eagle.library.util.command_choice_helpers
|
||||
import net.eagle0.eagle.{FactionId, ProvinceId}
|
||||
import net.eagle0.eagle.api.available_command.{AlmsAvailableCommand, AvailableCommand}
|
||||
import net.eagle0.eagle.api.selected_command.AlmsSelectedCommand
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.internal.hero.Hero
|
||||
import net.eagle0.eagle.internal.province.Province
|
||||
import net.eagle0.eagle.internal.game_state.GameState as GameStateProto
|
||||
import net.eagle0.eagle.library.actions.impl.command.AlmsCommand
|
||||
import net.eagle0.eagle.library.settings.MinSupportForTaxes
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.AvailableCommandSelector.flatSelectionForType
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.FoodConsumptionUtils.foodConsumptionMonthsToHold
|
||||
import net.eagle0.eagle.library.util.hero.LegacyHeroUtils
|
||||
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
|
||||
import net.eagle0.eagle.library.util.hero.HeroUtils
|
||||
import net.eagle0.eagle.library.util.province.ProvinceUtils
|
||||
import net.eagle0.eagle.library.util.CommandSelection
|
||||
import net.eagle0.eagle.model.proto_converters.hero.HeroConverter
|
||||
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.hero.HeroT
|
||||
import net.eagle0.eagle.model.state.province.ProvinceT
|
||||
|
||||
object AlmsCommandSelector {
|
||||
|
||||
private def bestHeroWithSupport(
|
||||
availableHeroes: Vector[Hero]
|
||||
): (Hero, Double) =
|
||||
availableHeroes: Vector[HeroT]
|
||||
): (HeroT, Double) =
|
||||
availableHeroes
|
||||
.map(h => (h, AlmsCommand.supportIncreasePerFood(HeroConverter.fromProto(h))))
|
||||
.map(h => (h, AlmsCommand.supportIncreasePerFood(h)))
|
||||
.maxBy {
|
||||
case (hero, inc) =>
|
||||
(inc, -LegacyHeroUtils.fatigue(hero))
|
||||
(inc, -HeroUtils.fatigue(hero))
|
||||
}
|
||||
|
||||
private def neededSupport(province: Province): Double =
|
||||
private def neededSupport(province: ProvinceT): Double =
|
||||
MinSupportForTaxes.doubleValue - province.support
|
||||
|
||||
def surplusFood(province: Province, gameState: GameState): Int =
|
||||
def surplusFood(province: ProvinceT, gameState: GameState): Int =
|
||||
Math.max(
|
||||
0,
|
||||
province.food - foodConsumptionMonthsToHold(
|
||||
province.food.toInt - foodConsumptionMonthsToHold(
|
||||
provinceId = province.id,
|
||||
gameState = gameState
|
||||
) * LegacyProvinceUtils
|
||||
.monthlyFoodConsumption(province.id, gameState)
|
||||
) * ProvinceUtils
|
||||
.monthlyFoodConsumption(province, gameState)
|
||||
)
|
||||
|
||||
def chosenAlmsForSupportCommand(
|
||||
@@ -97,7 +98,7 @@ object AlmsCommandSelector {
|
||||
gameState: GameState,
|
||||
availableCommand: AlmsAvailableCommand,
|
||||
minimumFood: Int,
|
||||
province: Province
|
||||
province: ProvinceT
|
||||
): Option[CommandSelection] = {
|
||||
val foodAfterFeedingTroops = surplusFood(province, gameState)
|
||||
val effectiveMinimumFood = minimumFood.min(foodAfterFeedingTroops)
|
||||
@@ -109,7 +110,7 @@ object AlmsCommandSelector {
|
||||
)
|
||||
|
||||
val neededSupportPerMonth =
|
||||
neededSupport(province) / (13 - gameState.currentDate.get.month)
|
||||
neededSupport(province) / (13 - gameState.currentDate.get.month.value)
|
||||
val neededFoodForSupport =
|
||||
neededSupport(province) / supportPerFood
|
||||
val neededFoodPerMonth =
|
||||
@@ -123,7 +124,7 @@ object AlmsCommandSelector {
|
||||
|
||||
val isPartialGift =
|
||||
neededFoodForSupport > availableCommand.foodAvailable
|
||||
val isDecember = gameState.currentDate.get.month == 12
|
||||
val isDecember = gameState.currentDate.get.month.value == 12
|
||||
val haveExcessToQuellRiot =
|
||||
(foodAfterFeedingTroops - neededFoodForSupport > 1000) || province.gold > 700
|
||||
|
||||
@@ -152,4 +153,50 @@ object AlmsCommandSelector {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Proto-accepting overloads for backward compatibility
|
||||
def surplusFood(province: net.eagle0.eagle.internal.province.Province, gameState: GameStateProto): Int =
|
||||
surplusFood(
|
||||
net.eagle0.eagle.model.proto_converters.province.ProvinceConverter.fromProto(province),
|
||||
GameStateConverter.fromProto(gameState)
|
||||
)
|
||||
|
||||
def chosenAlmsForSupportCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand]
|
||||
): Option[CommandSelection] =
|
||||
chosenAlmsForSupportCommand(
|
||||
actingFactionId,
|
||||
GameStateConverter.fromProto(gameState),
|
||||
availableCommands
|
||||
)
|
||||
|
||||
def chosenAlmsForSupportWithMinimumCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand],
|
||||
minimumFood: Int
|
||||
): Option[CommandSelection] =
|
||||
chosenAlmsForSupportWithMinimumCommand(
|
||||
actingFactionId,
|
||||
GameStateConverter.fromProto(gameState),
|
||||
availableCommands,
|
||||
minimumFood
|
||||
)
|
||||
|
||||
def chosenAlmsForSupportWithMinimumCommandForProvince(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand],
|
||||
minimumFood: Int,
|
||||
provinceId: ProvinceId
|
||||
): Option[CommandSelection] =
|
||||
chosenAlmsForSupportWithMinimumCommandForProvince(
|
||||
actingFactionId,
|
||||
GameStateConverter.fromProto(gameState),
|
||||
availableCommands,
|
||||
minimumFood,
|
||||
provinceId
|
||||
)
|
||||
}
|
||||
|
||||
+51
-20
@@ -4,22 +4,23 @@ import net.eagle0.eagle.{FactionId, ProvinceId}
|
||||
import net.eagle0.eagle.api.available_command.*
|
||||
import net.eagle0.eagle.api.selected_command.*
|
||||
import net.eagle0.eagle.common.combat_unit.CombatUnit
|
||||
import net.eagle0.eagle.common.profession.Profession.{ENGINEER, MAGE}
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.internal.hero.Hero
|
||||
import net.eagle0.eagle.internal.province.Province
|
||||
import net.eagle0.eagle.internal.game_state.GameState as GameStateProto
|
||||
import net.eagle0.eagle.library.settings.{
|
||||
MinVigorForMarch,
|
||||
RequiredRatioToAttack,
|
||||
RequiredRatioToAttackWithoutTakingCastles
|
||||
}
|
||||
import net.eagle0.eagle.library.util.{CommandSelection, LegacyBattalionUtils, ShardokMapInfo}
|
||||
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
|
||||
import net.eagle0.eagle.library.util.{BattalionUtils, CommandSelection, LegacyBattalionUtils, ShardokMapInfo}
|
||||
import net.eagle0.eagle.library.util.faction_utils.FactionUtils
|
||||
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.hero.HeroT
|
||||
import net.eagle0.eagle.model.state.hero.Profession.{Engineer, Mage}
|
||||
import net.eagle0.eagle.model.state.province.ProvinceT
|
||||
import net.eagle0.eagle.views.battalion_view.BattalionView
|
||||
|
||||
object AttackCommandChooser {
|
||||
|
||||
import net.eagle0.eagle.library.util.DateProtoUtils._Date
|
||||
import AvailableCommandSelector.flatSelectionForType
|
||||
|
||||
private val winterMonths = Set(12, 1, 2)
|
||||
@@ -27,7 +28,7 @@ object AttackCommandChooser {
|
||||
private def heroesWithSufficientVigorForAttack(
|
||||
provinceId: ProvinceId,
|
||||
gs: GameState
|
||||
): Vector[Hero] =
|
||||
): Vector[HeroT] =
|
||||
gs.provinces(provinceId)
|
||||
.rulingFactionHeroIds
|
||||
.map(gs.heroes)
|
||||
@@ -41,10 +42,13 @@ object AttackCommandChooser {
|
||||
gameState: GameState
|
||||
): Boolean =
|
||||
destinationFactionId.exists(dfid =>
|
||||
dfid != actingFactionId && !LegacyFactionUtils
|
||||
.hasAlliance(actingFactionId, dfid, gameState) && !LegacyFactionUtils
|
||||
.truceExpirationDate(dfid, actingFactionId, gameState)
|
||||
.exists(_ > gameState.currentDate.get)
|
||||
dfid != actingFactionId && !FactionUtils
|
||||
.hasAlliance(actingFactionId, dfid, gameState.factions.values.toVector) && !FactionUtils
|
||||
.factionRelationship(dfid, actingFactionId, gameState.factions.values)
|
||||
.resetDate
|
||||
.exists(d =>
|
||||
d.year > gameState.currentDate.get.year || (d.year == gameState.currentDate.get.year && d.month.value > gameState.currentDate.get.month.value)
|
||||
)
|
||||
)
|
||||
|
||||
// Calculates the best attack for a particular destination, not taking into account whether it's a good idea --
|
||||
@@ -56,7 +60,7 @@ object AttackCommandChooser {
|
||||
goldAvailable: Int,
|
||||
foodAvailable: Int,
|
||||
originProvinceId: ProvinceId,
|
||||
destinationProvince: Province,
|
||||
destinationProvince: ProvinceT,
|
||||
gameState: GameState
|
||||
): MarchSelectedCommand = MarchSelectedCommand(
|
||||
marchingUnits = selectedUnits,
|
||||
@@ -65,7 +69,7 @@ object AttackCommandChooser {
|
||||
gold = MarchSuppliesHelpers
|
||||
.goldToTakeOnAttack(selectedUnits.size, goldAvailable),
|
||||
food = MarchSuppliesHelpers.foodToTakeOnAttack(
|
||||
selectedUnits.flatMap(_.battalionId).map(gameState.battalions),
|
||||
selectedUnits.flatMap(_.battalionId).size,
|
||||
foodAvailable
|
||||
)
|
||||
)
|
||||
@@ -81,7 +85,7 @@ object AttackCommandChooser {
|
||||
goldAvailable: Int,
|
||||
foodAvailable: Int,
|
||||
originProvinceId: ProvinceId,
|
||||
destinationProvince: Province,
|
||||
destinationProvince: ProvinceT,
|
||||
requiresRiverCrossing: Boolean,
|
||||
totalBattalionPower: Double
|
||||
)
|
||||
@@ -124,7 +128,7 @@ object AttackCommandChooser {
|
||||
desiredCount = unitCount,
|
||||
heroes = heroes,
|
||||
battalions = battalions.toVector,
|
||||
battalionTypes = gameState.battalionTypes.toVector
|
||||
battalionTypes = gameState.battalionTypes
|
||||
)
|
||||
|
||||
// Get river crossing requirement from the available destination
|
||||
@@ -140,7 +144,7 @@ object AttackCommandChooser {
|
||||
totalBattalionPower = selectedUnits
|
||||
.flatMap(_.battalionId)
|
||||
.map(gameState.battalions)
|
||||
.map(LegacyBattalionUtils.power)
|
||||
.map(BattalionUtils.power)
|
||||
.sum
|
||||
)
|
||||
}).toVector
|
||||
@@ -162,7 +166,7 @@ object AttackCommandChooser {
|
||||
.filter {
|
||||
_.destinationProvince.incomingArmies
|
||||
.exists(incomingArmy =>
|
||||
incomingArmy.getArmy.factionId == actingFactionId && incomingArmy.arrivalRound > gameState.currentRoundId
|
||||
incomingArmy.army.factionId == actingFactionId && incomingArmy.arrivalRound > gameState.currentRoundId
|
||||
)
|
||||
}
|
||||
.map {
|
||||
@@ -258,12 +262,12 @@ object AttackCommandChooser {
|
||||
.map(u => u.heroId)
|
||||
.map(gameState.heroes)
|
||||
.map(_.profession)
|
||||
.exists(prof => prof == ENGINEER || prof == MAGE)
|
||||
.exists(prof => prof == Engineer || prof == Mage)
|
||||
|
||||
// If it's winter or we have a river crossing profession, take everything.
|
||||
// Otherwise, only take those that don't require a river crossing.
|
||||
val isWinter =
|
||||
winterMonths.contains(gameState.currentDate.get.month)
|
||||
winterMonths.contains(gameState.currentDate.get.month.value)
|
||||
|
||||
val allMarchingCIs =
|
||||
if isWinter || hasRiverCrossingProfession then commandInfos
|
||||
@@ -316,4 +320,31 @@ object AttackCommandChooser {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Proto-accepting overloads for backward compatibility
|
||||
def possibleAttackCommands(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameStateProto,
|
||||
ac: MarchAvailableCommand
|
||||
): Vector[PossibleAttackCommandInfo] =
|
||||
possibleAttackCommands(
|
||||
actingFactionId,
|
||||
GameStateConverter.fromProto(gameState),
|
||||
ac
|
||||
)
|
||||
|
||||
def chosenAttackCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameStateProto,
|
||||
acs: Vector[AvailableCommand],
|
||||
knownEnemyHeroCount: ProvinceId => Int,
|
||||
knownEnemyBattalions: ProvinceId => Vector[BattalionView]
|
||||
): Option[CommandSelection] =
|
||||
chosenAttackCommand(
|
||||
actingFactionId,
|
||||
GameStateConverter.fromProto(gameState),
|
||||
acs,
|
||||
knownEnemyHeroCount,
|
||||
knownEnemyBattalions
|
||||
)
|
||||
}
|
||||
|
||||
+25
-3
@@ -11,11 +11,14 @@ import net.eagle0.eagle.api.command.util.attack_decision_type.{
|
||||
}
|
||||
import net.eagle0.eagle.api.selected_command.AttackDecisionSelectedCommand
|
||||
import net.eagle0.eagle.common.tribute_amount.TributeAmount
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.internal.game_state.GameState as GameStateProto
|
||||
import net.eagle0.eagle.library.settings.MinimumPercentageOfEnemyToAttack
|
||||
import net.eagle0.eagle.library.util.{BattalionPower, CommandSelection, IncomingArmyUtils}
|
||||
import net.eagle0.eagle.library.util.{BattalionPower, CommandSelection}
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.AvailableCommandSelector.flatSelectionForType
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.CommandChooserImplicits.*
|
||||
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.Army
|
||||
import net.eagle0.eagle.FactionId
|
||||
|
||||
object AttackDecisionCommandChooser {
|
||||
@@ -127,6 +130,11 @@ object AttackDecisionCommandChooser {
|
||||
}.flatten
|
||||
}
|
||||
|
||||
private def armyPower(army: Army, gs: GameState): Double = {
|
||||
val battalions = army.units.flatMap(_.battalionId).map(gs.battalions)
|
||||
battalions.map(BattalionPower.power).sum
|
||||
}
|
||||
|
||||
private def okToAttack(
|
||||
availableCommand: AttackDecisionAvailableCommand,
|
||||
gameState: GameState
|
||||
@@ -143,7 +151,7 @@ object AttackDecisionCommandChooser {
|
||||
.find(hag => hag.factionId == armyStats.factionId)
|
||||
.map(_.armies)
|
||||
.getOrElse(Vector.empty)
|
||||
.map(ia => IncomingArmyUtils.armyPower(ia.getArmy, gameState))
|
||||
.map(ia => armyPower(ia.army, gameState))
|
||||
.sum
|
||||
)
|
||||
.sum
|
||||
@@ -219,4 +227,18 @@ object AttackDecisionCommandChooser {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Proto-accepting overload for backward compatibility
|
||||
def attackDecisionSelectedCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand],
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Option[CommandSelection]] =
|
||||
attackDecisionSelectedCommand(
|
||||
actingFactionId,
|
||||
GameStateConverter.fromProto(gameState),
|
||||
availableCommands,
|
||||
functionalRandom
|
||||
)
|
||||
}
|
||||
|
||||
@@ -26,19 +26,26 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers/quest_command_selectors:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/util/command_choice_helpers:__pkg__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
deps = [
|
||||
":available_command_selector",
|
||||
":food_consumption_utils",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/command:alms_command",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:min_support_for_taxes",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:command_selection",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/hero:legacy_hero_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/province:legacy_province_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/hero",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/hero:hero_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -49,25 +56,30 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/ai:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/util/command_choice_helpers:__pkg__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
deps = [
|
||||
":available_command_selector",
|
||||
":combat_unit_selector",
|
||||
":march_supplies_helpers",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:battalion_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:hero_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:province_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/views:battalion_view_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:min_vigor_for_march",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:required_ratio_to_attack",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:required_ratio_to_attack_without_taking_castles",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:battalion_type_finder",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:battalion_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:command_selection",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:date_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:legacy_battalion_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:shardok_map_info",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils:legacy_faction_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -78,6 +90,9 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/ai:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/util/command_choice_helpers:__pkg__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
deps = [
|
||||
":available_command_selector",
|
||||
":command_chooser",
|
||||
@@ -89,7 +104,9 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:minimum_percentage_of_enemy_to_attack",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:battalion_power",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:command_selection",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:incoming_army_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:army",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -118,12 +135,13 @@ scala_library(
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/api/command/util:appropriate_battalions_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:combat_unit_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:battalion_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:hero_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:battalion_suitability",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:battalion_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:eagle_require",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:legacy_battalion_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/hero:legacy_hero_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/hero:hero_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -143,6 +161,8 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/common:functional_random",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:command_selection",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -198,13 +218,22 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:vassal_minimum_food_to_send_supplies",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:vassal_minimum_gold_to_send_supplies",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:battalion_type_finder",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:battalion_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:beast_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:command_selection",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:incoming_army_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:province_distances",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils:legacy_faction_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/hero:hero_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/hero:legacy_hero_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/province",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/province:legacy_province_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -233,6 +262,10 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/ai:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/util/command_choice_helpers:__pkg__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
],
|
||||
deps = [
|
||||
":available_command_selector",
|
||||
":food_consumption_utils",
|
||||
@@ -242,12 +275,15 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/common:functional_random",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:max_combat_unit_count_per_side",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:battalion_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:command_selection",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:eagle_require",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:legacy_battalion_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:province_gold_surplus_calculator",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils:legacy_faction_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/province:legacy_province_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -258,10 +294,18 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/ai:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/util/command_choice_helpers:__pkg__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:min_support_for_taxes",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/date",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -323,9 +367,9 @@ scala_library(
|
||||
"//src/test/scala/net/eagle0/eagle/library/util/command_choice_helpers:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:hero_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/hero:legacy_hero_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/hero:hero_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -337,16 +381,21 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/ai:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/util/command_choice_helpers:__pkg__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
deps = [
|
||||
":available_command_selector",
|
||||
":hero_selector",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:hero_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:command_selection",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/hero:legacy_hero_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/hero:hero_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -358,9 +407,7 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/util/command_choice_helpers:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:battalion_scala_proto",
|
||||
],
|
||||
deps = [],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
@@ -380,8 +427,13 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:battalion_type_finder",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:command_selection",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:province_gold_surplus_calculator",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/hero:legacy_hero_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/province:legacy_province_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:battalion_type_id_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type_id",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -396,6 +448,8 @@ scala_library(
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:gold_per_hero_held_back",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
+15
-15
@@ -4,19 +4,19 @@ import scala.annotation.tailrec
|
||||
|
||||
import net.eagle0.eagle.api.command.util.appropriate_battalions.SuitableBattalions.SuitabilityLevel
|
||||
import net.eagle0.eagle.api.command.util.appropriate_battalions.SuitableBattalions.SuitabilityLevel.RESTRICTIVE
|
||||
import net.eagle0.eagle.common.battalion_type.BattalionType
|
||||
import net.eagle0.eagle.common.combat_unit.CombatUnit
|
||||
import net.eagle0.eagle.internal.battalion.Battalion
|
||||
import net.eagle0.eagle.internal.hero.Hero
|
||||
import net.eagle0.eagle.library.util.{BattalionSuitability, LegacyBattalionUtils}
|
||||
import net.eagle0.eagle.library.util.hero.LegacyHeroUtils
|
||||
import net.eagle0.eagle.library.util.{BattalionSuitability, BattalionUtils}
|
||||
import net.eagle0.eagle.library.util.hero.HeroUtils
|
||||
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
|
||||
import net.eagle0.eagle.model.state.battalion.BattalionT
|
||||
import net.eagle0.eagle.model.state.hero.HeroT
|
||||
import net.eagle0.eagle.model.state.BattalionType
|
||||
|
||||
object CombatUnitSelector {
|
||||
def selectedCombatBattalions(
|
||||
desiredCount: Int,
|
||||
heroes: Vector[Hero],
|
||||
battalions: Vector[Battalion],
|
||||
heroes: Vector[HeroT],
|
||||
battalions: Vector[BattalionT],
|
||||
battalionTypes: Vector[BattalionType]
|
||||
): Vector[CombatUnit] = {
|
||||
val suitableBattalionsMap =
|
||||
@@ -26,18 +26,18 @@ object CombatUnitSelector {
|
||||
bts = battalionTypes
|
||||
)
|
||||
|
||||
def suitability(h: Hero, b: Battalion): SuitabilityLevel =
|
||||
def suitability(h: HeroT, b: BattalionT): SuitabilityLevel =
|
||||
suitableBattalionsMap(h.id).battalionIdsWithSuitability
|
||||
.find(_.battalionId == b.id)
|
||||
.get
|
||||
.level
|
||||
|
||||
def chooseOne(hs: Vector[Hero], bs: Vector[Battalion]): CombatUnit = {
|
||||
def chooseOne(hs: Vector[HeroT], bs: Vector[BattalionT]): CombatUnit = {
|
||||
val bestBattalionOpt = bs
|
||||
.filter(b => hs.exists(h => suitability(h, b) != RESTRICTIVE))
|
||||
.maxByOption(LegacyBattalionUtils.power)
|
||||
.maxByOption(BattalionUtils.power)
|
||||
bestBattalionOpt.map { bestBattalion =>
|
||||
val bestHero = hs.minBy(h => (suitability(h, bestBattalion).index, -LegacyHeroUtils.power(h)))
|
||||
val bestHero = hs.minBy(h => (suitability(h, bestBattalion).index, -HeroUtils.power(h)))
|
||||
CombatUnit(
|
||||
heroId = bestHero.id,
|
||||
battalionId = Some(bestBattalion.id),
|
||||
@@ -46,8 +46,8 @@ object CombatUnitSelector {
|
||||
}
|
||||
.getOrElse(
|
||||
CombatUnit(
|
||||
heroId = hs.maxBy(LegacyHeroUtils.power).id,
|
||||
battalionId = bs.maxByOption(LegacyBattalionUtils.power).map(_.id),
|
||||
heroId = hs.maxBy(HeroUtils.power).id,
|
||||
battalionId = bs.maxByOption(BattalionUtils.power).map(_.id),
|
||||
factionId = hs.head.factionId.get
|
||||
)
|
||||
)
|
||||
@@ -56,8 +56,8 @@ object CombatUnitSelector {
|
||||
@tailrec
|
||||
def go(
|
||||
c: Int,
|
||||
hs: Vector[Hero],
|
||||
bs: Vector[Battalion],
|
||||
hs: Vector[HeroT],
|
||||
bs: Vector[BattalionT],
|
||||
acc: Vector[CombatUnit]
|
||||
): Vector[CombatUnit] =
|
||||
if c == 0 then acc
|
||||
|
||||
+97
-86
@@ -24,7 +24,7 @@ import net.eagle0.eagle.common.recruitment_info.RecruitmentStatus.{
|
||||
import net.eagle0.eagle.common.tribute_amount.TributeAmount
|
||||
import net.eagle0.eagle.common.unaffiliated_hero_type.UnaffiliatedHeroType.UNAFFILIATED_HERO_PRISONER
|
||||
import net.eagle0.eagle.internal.battalion.Battalion
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.internal.game_state.GameState as GameStateProto
|
||||
import net.eagle0.eagle.internal.hero.Hero
|
||||
import net.eagle0.eagle.internal.province.Province
|
||||
import net.eagle0.eagle.internal.unaffiliated_hero.UnaffiliatedHero
|
||||
@@ -41,24 +41,32 @@ import net.eagle0.eagle.library.settings.{
|
||||
VassalMinimumFoodToSendSupplies,
|
||||
VassalMinimumGoldToSendSupplies
|
||||
}
|
||||
import net.eagle0.eagle.library.util.{BattalionTypeFinder, CommandSelection, IncomingArmyUtils, ProvinceDistances}
|
||||
import net.eagle0.eagle.library.util.{
|
||||
BattalionTypeFinder,
|
||||
BattalionUtils,
|
||||
CommandSelection,
|
||||
IncomingArmyUtils,
|
||||
ProvinceDistances
|
||||
}
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.AvailableCommandSelector.randomSelectionForType
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.CommandChooserImplicits.*
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.CommandChooserProtoImplicits.*
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.FoodConsumptionUtils.foodConsumptionMonthsToHold
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.ProvinceGoldSurplusCalculator.{
|
||||
goldToHoldBack,
|
||||
provinceGoldSurplus
|
||||
}
|
||||
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
|
||||
import net.eagle0.eagle.library.util.hero.LegacyHeroUtils
|
||||
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
|
||||
import net.eagle0.eagle.library.util.faction_utils.{FactionUtils, LegacyFactionUtils}
|
||||
import net.eagle0.eagle.library.util.hero.{HeroUtils, LegacyHeroUtils}
|
||||
import net.eagle0.eagle.library.util.province.{LegacyProvinceUtils, ProvinceUtils}
|
||||
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
|
||||
import net.eagle0.eagle.model.state.hero.{HeroT, Profession}
|
||||
|
||||
object CommandChoiceHelpers {
|
||||
import AvailableCommandSelector.{flatSelectionForType, selectionForType}
|
||||
|
||||
def chosenUnderAttackCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand]
|
||||
): Option[CommandSelection] =
|
||||
flatSelectionForType[OrganizeTroopsAvailableCommand](availableCommands) { organizeCommand =>
|
||||
@@ -80,7 +88,7 @@ object CommandChoiceHelpers {
|
||||
|
||||
def excessBeyondMaximumSupplies(
|
||||
actingProvinceId: ProvinceId,
|
||||
gameState: GameState
|
||||
gameState: GameStateProto
|
||||
): Option[Supplies] = {
|
||||
def battalionType(battalionTypeId: BattalionTypeId): BattalionType =
|
||||
BattalionTypeFinder.battalionType(
|
||||
@@ -149,7 +157,7 @@ object CommandChoiceHelpers {
|
||||
|
||||
private def chosenShipExcessSuppliesCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand]
|
||||
): Option[CommandSelection] =
|
||||
flatSelectionForType[SendSuppliesAvailableCommand](availableCommands) { ac =>
|
||||
@@ -166,7 +174,7 @@ object CommandChoiceHelpers {
|
||||
// Commands that should be done first regardless of the province's orders.
|
||||
def chosenUniversalCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand],
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Option[CommandSelection]] =
|
||||
@@ -174,7 +182,7 @@ object CommandChoiceHelpers {
|
||||
actingFactionId = actingFactionId,
|
||||
gameState = gameState,
|
||||
availableCommands = availableCommands,
|
||||
rankedChoosers = Vector[CommandChooser](
|
||||
rankedChoosers = Vector[CommandChooserProto](
|
||||
chosenCommandWhileTraveling,
|
||||
AttackDecisionCommandChooser.attackDecisionSelectedCommand,
|
||||
(actingFactionId, gameState, availableCommands, functionalRandom) =>
|
||||
@@ -231,7 +239,7 @@ object CommandChoiceHelpers {
|
||||
|
||||
def chosenDevelopCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand],
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Option[CommandSelection]] =
|
||||
@@ -239,7 +247,7 @@ object CommandChoiceHelpers {
|
||||
actingFactionId = actingFactionId,
|
||||
gameState = gameState,
|
||||
availableCommands = availableCommands,
|
||||
rankedChoosers = Vector[CommandChooser](
|
||||
rankedChoosers = Vector[CommandChooserProto](
|
||||
chosenUniversalCommand,
|
||||
(actingFactionId, gameState, availableCommands, functionalRandom) =>
|
||||
RandomState(
|
||||
@@ -275,7 +283,7 @@ object CommandChoiceHelpers {
|
||||
|
||||
def chosenMobilizeCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand],
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Option[CommandSelection]] =
|
||||
@@ -283,7 +291,7 @@ object CommandChoiceHelpers {
|
||||
actingFactionId = actingFactionId,
|
||||
gameState = gameState,
|
||||
availableCommands = availableCommands,
|
||||
rankedChoosers = Vector[CommandChooser](
|
||||
rankedChoosers = Vector[CommandChooserProto](
|
||||
chosenUniversalCommand,
|
||||
(actingFactionId, gameState, availableCommands, functionalRandom) =>
|
||||
RandomState(
|
||||
@@ -352,7 +360,7 @@ object CommandChoiceHelpers {
|
||||
|
||||
def chosenEntrustCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand],
|
||||
actingProvinceId: ProvinceId,
|
||||
functionalRandom: FunctionalRandom
|
||||
@@ -361,11 +369,11 @@ object CommandChoiceHelpers {
|
||||
actingFactionId = actingFactionId,
|
||||
gameState = gameState,
|
||||
availableCommands = availableCommands,
|
||||
rankedChoosers = Vector[CommandChooser](
|
||||
rankedChoosers = Vector[CommandChooserProto](
|
||||
chosenUniversalCommand,
|
||||
(
|
||||
fid: FactionId,
|
||||
gs: GameState,
|
||||
gs: GameStateProto,
|
||||
acs: Vector[AvailableCommand],
|
||||
fr: FunctionalRandom
|
||||
) =>
|
||||
@@ -381,14 +389,14 @@ object CommandChoiceHelpers {
|
||||
functionalRandom = functionalRandom
|
||||
)
|
||||
|
||||
private def suppressBeastsValue(hero: Hero): Double =
|
||||
(hero.strength + hero.agility) / 2.0 + (if hero.profession.isNoProfession
|
||||
private def suppressBeastsValue(hero: HeroT): Double =
|
||||
(hero.strength + hero.agility) / 2.0 + (if hero.profession == Profession.NoProfession
|
||||
then 0.0
|
||||
else 50.0)
|
||||
|
||||
def chosenSuppressBeastsCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand]
|
||||
): Option[CommandSelection] =
|
||||
flatSelectionForType[SuppressBeastsAvailableCommand](availableCommands) { suppressAc =>
|
||||
@@ -405,10 +413,11 @@ object CommandChoiceHelpers {
|
||||
if province.battalionIds
|
||||
.exists(b => gameState.battalions(b).size >= beastPower)
|
||||
then {
|
||||
val scalaGs = GameStateConverter.fromProto(gameState)
|
||||
val leaderId = HeroSelector
|
||||
.minimallyFatiguedHeroes(
|
||||
suppressAc.availableHeroIds
|
||||
.map(gameState.heroes)
|
||||
.map(scalaGs.heroes)
|
||||
.toVector
|
||||
)
|
||||
.maxBy(suppressBeastsValue(_))
|
||||
@@ -441,7 +450,7 @@ object CommandChoiceHelpers {
|
||||
|
||||
private def chosenMobilizeIfAdjacentEnemyCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand],
|
||||
actingProvinceId: ProvinceId,
|
||||
functionalRandom: FunctionalRandom
|
||||
@@ -460,7 +469,7 @@ object CommandChoiceHelpers {
|
||||
|
||||
def chosenExpandCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand],
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Option[CommandSelection]] =
|
||||
@@ -468,12 +477,12 @@ object CommandChoiceHelpers {
|
||||
actingFactionId = actingFactionId,
|
||||
gameState = gameState,
|
||||
availableCommands = availableCommands,
|
||||
rankedChoosers = Vector[CommandChooser](
|
||||
rankedChoosers = Vector[CommandChooserProto](
|
||||
chosenUniversalCommand,
|
||||
ExpandCommandSelector.maybeChosenExpandCommand,
|
||||
(
|
||||
fid: FactionId,
|
||||
gs: GameState,
|
||||
gs: GameStateProto,
|
||||
acs: Vector[AvailableCommand],
|
||||
fr: FunctionalRandom
|
||||
) =>
|
||||
@@ -512,7 +521,7 @@ object CommandChoiceHelpers {
|
||||
|
||||
private def chosenShouldRestCommand(
|
||||
actingFactionId: FactionId,
|
||||
gs: GameState,
|
||||
gs: GameStateProto,
|
||||
acs: Vector[AvailableCommand],
|
||||
reason: String
|
||||
): Option[CommandSelection] =
|
||||
@@ -540,7 +549,7 @@ object CommandChoiceHelpers {
|
||||
|
||||
def chosenRestCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand],
|
||||
reason: String
|
||||
): Option[CommandSelection] =
|
||||
@@ -556,7 +565,7 @@ object CommandChoiceHelpers {
|
||||
|
||||
def handleCapturedHeroesSelectedCommand(
|
||||
actingFactionId: FactionId,
|
||||
gs: GameState,
|
||||
gs: GameStateProto,
|
||||
acs: Vector[AvailableCommand],
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Option[CommandSelection]] =
|
||||
@@ -599,7 +608,7 @@ object CommandChoiceHelpers {
|
||||
|
||||
def resolvePleaseRecruitMeSelectedCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand]
|
||||
): Option[CommandSelection] =
|
||||
selectionForType[PleaseRecruitMeAvailableCommand](availableCommands) { pleaseRecruitMeAc =>
|
||||
@@ -618,7 +627,7 @@ object CommandChoiceHelpers {
|
||||
|
||||
private def handleRiotGiveSelectedCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand]
|
||||
): Option[CommandSelection] =
|
||||
selectionForType[HandleRiotGiveAvailableCommand](availableCommands) { availableCommand =>
|
||||
@@ -643,7 +652,7 @@ object CommandChoiceHelpers {
|
||||
|
||||
private def handleRiotCrackDownSelectedCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand]
|
||||
): Option[CommandSelection] =
|
||||
selectionForType[HandleRiotCrackDownAvailableCommand](availableCommands) { availableCommand =>
|
||||
@@ -663,7 +672,7 @@ object CommandChoiceHelpers {
|
||||
|
||||
private def handleRiotDoNothingSelectedCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand]
|
||||
): Option[CommandSelection] =
|
||||
selectionForType[HandleRiotDoNothingAvailableCommand](availableCommands) { availableCommand =>
|
||||
@@ -678,7 +687,7 @@ object CommandChoiceHelpers {
|
||||
|
||||
def handleRiotSelectedCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand],
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Option[CommandSelection]] =
|
||||
@@ -686,7 +695,7 @@ object CommandChoiceHelpers {
|
||||
actingFactionId = actingFactionId,
|
||||
gameState = gameState,
|
||||
availableCommands = availableCommands,
|
||||
rankedChoosers = Vector[CommandChooser](
|
||||
rankedChoosers = Vector[CommandChooserProto](
|
||||
(actingFactionId, gameState, availableCommands, functionalRandom) =>
|
||||
RandomState(
|
||||
handleRiotGiveSelectedCommand(
|
||||
@@ -720,7 +729,7 @@ object CommandChoiceHelpers {
|
||||
|
||||
def freeForAllDecisionSelectedCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand],
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Option[CommandSelection]] =
|
||||
@@ -728,7 +737,7 @@ object CommandChoiceHelpers {
|
||||
actingFactionId = actingFactionId,
|
||||
gameState = gameState,
|
||||
availableCommands = availableCommands,
|
||||
rankedChoosers = Vector[CommandChooser](
|
||||
rankedChoosers = Vector[CommandChooserProto](
|
||||
(actingFactionId, gameState, availableCommands, functionalRandom) =>
|
||||
RandomState(
|
||||
advanceFFASelectedCommand(
|
||||
@@ -753,7 +762,7 @@ object CommandChoiceHelpers {
|
||||
|
||||
private def withdrawFFASelectedCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand]
|
||||
): Option[CommandSelection] =
|
||||
flatSelectionForType[FreeForAllDecisionAvailableCommand](
|
||||
@@ -777,7 +786,7 @@ object CommandChoiceHelpers {
|
||||
|
||||
private def advanceFFASelectedCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand]
|
||||
): Option[CommandSelection] =
|
||||
flatSelectionForType[FreeForAllDecisionAvailableCommand](
|
||||
@@ -801,7 +810,7 @@ object CommandChoiceHelpers {
|
||||
|
||||
def resolveTributeSelectedCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand],
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Option[CommandSelection]] =
|
||||
@@ -836,7 +845,7 @@ object CommandChoiceHelpers {
|
||||
|
||||
def defendSelectedCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand],
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Option[CommandSelection]] =
|
||||
@@ -873,23 +882,25 @@ object CommandChoiceHelpers {
|
||||
}
|
||||
|
||||
def defendingUnits(
|
||||
gs: GameState,
|
||||
gs: GameStateProto,
|
||||
availableCommand: DefendAvailableCommand
|
||||
): Vector[CombatUnit] =
|
||||
): Vector[CombatUnit] = {
|
||||
val scalaGs = GameStateConverter.fromProto(gs)
|
||||
CombatUnitSelector.selectedCombatBattalions(
|
||||
desiredCount = availableCommand.availableHeroIds.size
|
||||
.min(MaxCombatUnitCountPerSide.intValue),
|
||||
heroes = availableCommand.availableHeroIds.map(gs.heroes).toVector,
|
||||
heroes = availableCommand.availableHeroIds.map(scalaGs.heroes).toVector,
|
||||
battalions = availableCommand.availableBattalions
|
||||
.map(_.battalionId)
|
||||
.map(gs.battalions)
|
||||
.map(scalaGs.battalions)
|
||||
.toVector,
|
||||
battalionTypes = gs.battalionTypes.toVector
|
||||
battalionTypes = scalaGs.battalionTypes
|
||||
)
|
||||
}
|
||||
|
||||
def foodAmountToBuy(
|
||||
province: Province,
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
goldAvailable: Int,
|
||||
foodBuyPrice: Double
|
||||
): Option[Int] = {
|
||||
@@ -910,7 +921,7 @@ object CommandChoiceHelpers {
|
||||
|
||||
def maybeChosenTravelToBuyFoodCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand]
|
||||
): Option[CommandSelection] =
|
||||
flatSelectionForType[TravelAvailableCommand](availableCommands) { availableCommand =>
|
||||
@@ -929,7 +940,7 @@ object CommandChoiceHelpers {
|
||||
|
||||
def chosenBuyFoodCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand]
|
||||
): Option[CommandSelection] =
|
||||
flatSelectionForType[TradeAvailableCommand](availableCommands) { availableCommand =>
|
||||
@@ -957,7 +968,7 @@ object CommandChoiceHelpers {
|
||||
|
||||
def chosenImproveInfrastructureIfBelowTrainingCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand]
|
||||
): Option[CommandSelection] =
|
||||
flatSelectionForType[ImproveAvailableCommand](availableCommands) { availableCommand =>
|
||||
@@ -988,7 +999,7 @@ object CommandChoiceHelpers {
|
||||
}
|
||||
}
|
||||
|
||||
def suppliesToSend(provinceId: ProvinceId, gameState: GameState): Supplies =
|
||||
def suppliesToSend(provinceId: ProvinceId, gameState: GameStateProto): Supplies =
|
||||
Supplies(
|
||||
food = provinceFoodSurplus(provinceId, gameState),
|
||||
gold = provinceGoldSurplus(provinceId, gameState)
|
||||
@@ -996,7 +1007,7 @@ object CommandChoiceHelpers {
|
||||
|
||||
private def closestLeader(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
actingProvinceId: ProvinceId
|
||||
): Option[(HeroId, ProvinceId)] =
|
||||
gameState
|
||||
@@ -1027,7 +1038,7 @@ object CommandChoiceHelpers {
|
||||
|
||||
def preferredSuppliesDestination(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
ac: SendSuppliesAvailableCommand
|
||||
): Option[ProvinceId] =
|
||||
gameState
|
||||
@@ -1052,7 +1063,7 @@ object CommandChoiceHelpers {
|
||||
|
||||
def chosenSendSuppliesCommandWithSuppliesAndDestination(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
ac: SendSuppliesAvailableCommand,
|
||||
supplies: Supplies,
|
||||
destination: ProvinceId
|
||||
@@ -1073,7 +1084,7 @@ object CommandChoiceHelpers {
|
||||
|
||||
def chosenSendSuppliesCommandWithSupplies(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
ac: SendSuppliesAvailableCommand,
|
||||
supplies: Supplies
|
||||
): Option[CommandSelection] =
|
||||
@@ -1104,7 +1115,7 @@ object CommandChoiceHelpers {
|
||||
private def lowLoyaltyHeroes(
|
||||
provinceId: ProvinceId,
|
||||
heroIds: Vector[HeroId],
|
||||
gameState: GameState
|
||||
gameState: GameStateProto
|
||||
): Vector[Hero] =
|
||||
heroIds
|
||||
.filter(hid =>
|
||||
@@ -1118,7 +1129,7 @@ object CommandChoiceHelpers {
|
||||
|
||||
def chosenFeastCommandIfAvailable(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand],
|
||||
reason: String
|
||||
): Option[CommandSelection] =
|
||||
@@ -1145,7 +1156,7 @@ object CommandChoiceHelpers {
|
||||
|
||||
private def maybeChosenFeastCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand],
|
||||
reason: String
|
||||
): Option[CommandSelection] =
|
||||
@@ -1163,7 +1174,7 @@ object CommandChoiceHelpers {
|
||||
|
||||
private def maybeChosenGiftCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand],
|
||||
reason: String
|
||||
): Option[CommandSelection] =
|
||||
@@ -1188,7 +1199,7 @@ object CommandChoiceHelpers {
|
||||
private def maybeSpreadIntoOwnedProvince(
|
||||
opmc: MarchCommandFromOneProvince,
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
ac: MarchAvailableCommand
|
||||
): Option[CommandSelection] = {
|
||||
val originProvince = gameState.provinces(opmc.originProvinceId)
|
||||
@@ -1242,7 +1253,7 @@ object CommandChoiceHelpers {
|
||||
private def maybeSpreadIntoEmptyProvince(
|
||||
opmc: MarchCommandFromOneProvince,
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
ac: MarchAvailableCommand,
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Option[CommandSelection]] = {
|
||||
@@ -1308,7 +1319,7 @@ object CommandChoiceHelpers {
|
||||
|
||||
def maybeChosenGetUnderHeroCapCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand],
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Option[CommandSelection]] =
|
||||
@@ -1347,13 +1358,13 @@ object CommandChoiceHelpers {
|
||||
|
||||
private def maybeChosenAttackToSaveLeaderCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand]
|
||||
): Option[CommandSelection] = None
|
||||
|
||||
def maybeRansomLeaderCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand],
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Option[CommandSelection]] = {
|
||||
@@ -1419,7 +1430,7 @@ object CommandChoiceHelpers {
|
||||
|
||||
private def chosenRescueLeaderIfAllPrisonersCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand],
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Option[CommandSelection]] =
|
||||
@@ -1445,7 +1456,7 @@ object CommandChoiceHelpers {
|
||||
|
||||
def chosenRescueLeaderCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand],
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Option[CommandSelection]] =
|
||||
@@ -1454,7 +1465,7 @@ object CommandChoiceHelpers {
|
||||
actingFactionId = actingFactionId,
|
||||
gameState = gameState,
|
||||
availableCommands = availableCommands,
|
||||
rankedChoosers = Vector[CommandChooser](
|
||||
rankedChoosers = Vector[CommandChooserProto](
|
||||
(actingFactionId, gameState, availableCommands, functionalRandom) =>
|
||||
RandomState(
|
||||
maybeChosenAttackToSaveLeaderCommand(
|
||||
@@ -1472,7 +1483,7 @@ object CommandChoiceHelpers {
|
||||
|
||||
private def chosenLoyaltyManagementCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand],
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Option[CommandSelection]] =
|
||||
@@ -1481,7 +1492,7 @@ object CommandChoiceHelpers {
|
||||
actingFactionId = actingFactionId,
|
||||
gameState = gameState,
|
||||
availableCommands = availableCommands,
|
||||
rankedChoosers = Vector[CommandChooser](
|
||||
rankedChoosers = Vector[CommandChooserProto](
|
||||
maybeChosenGetUnderHeroCapCommand,
|
||||
(actingFactionId, gameState, availableCommands, functionalRandom) =>
|
||||
RandomState(
|
||||
@@ -1510,7 +1521,7 @@ object CommandChoiceHelpers {
|
||||
|
||||
def maybeChosenSendSuppliesCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand]
|
||||
): Option[CommandSelection] =
|
||||
flatSelectionForType[SendSuppliesAvailableCommand](availableCommands) { ac =>
|
||||
@@ -1527,7 +1538,7 @@ object CommandChoiceHelpers {
|
||||
|
||||
def chosenCommandWhileTraveling(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand],
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Option[CommandSelection]] =
|
||||
@@ -1536,7 +1547,7 @@ object CommandChoiceHelpers {
|
||||
actingFactionId = actingFactionId,
|
||||
gameState = gameState,
|
||||
availableCommands = availableCommands,
|
||||
rankedChoosers = Vector[CommandChooser](
|
||||
rankedChoosers = Vector[CommandChooserProto](
|
||||
(actingFactionId, gameState, availableCommands, functionalRandom) =>
|
||||
RandomState(
|
||||
maybeChosenRecruitCommand(
|
||||
@@ -1590,7 +1601,7 @@ object CommandChoiceHelpers {
|
||||
|
||||
private def maybeChosenRecruitCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand]
|
||||
): Option[CommandSelection] =
|
||||
flatSelectionForType[RecruitHeroesAvailableCommand](availableCommands) { availableCommand =>
|
||||
@@ -1610,7 +1621,7 @@ object CommandChoiceHelpers {
|
||||
|
||||
private def maybeChosenDivineCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand]
|
||||
): Option[CommandSelection] =
|
||||
flatSelectionForType[DivineAvailableCommand](availableCommands) { availableCommand =>
|
||||
@@ -1637,7 +1648,7 @@ object CommandChoiceHelpers {
|
||||
|
||||
private def chosenReturnCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand]
|
||||
): Option[CommandSelection] =
|
||||
selectionForType[ReturnAvailableCommand](availableCommands) { availableCommand =>
|
||||
@@ -1652,7 +1663,7 @@ object CommandChoiceHelpers {
|
||||
|
||||
def maybeChosenTravelToArmTroopsCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand]
|
||||
): Option[CommandSelection] =
|
||||
flatSelectionForType[TravelAvailableCommand](availableCommands) { availableCommand =>
|
||||
@@ -1681,7 +1692,7 @@ object CommandChoiceHelpers {
|
||||
|
||||
def maybeMoveToRecruitCommand(
|
||||
actingFactionId: FactionId,
|
||||
gs: GameState,
|
||||
gs: GameStateProto,
|
||||
acs: Vector[AvailableCommand],
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Option[CommandSelection]] =
|
||||
@@ -1768,14 +1779,14 @@ object CommandChoiceHelpers {
|
||||
private def isInterestingUH(uh: UnaffiliatedHero): Boolean =
|
||||
uh.recruitmentInfo.get.status == RECRUITMENT_STATUS_WOULD_JOIN || uh.recruitmentInfo.get.status == RECRUITMENT_STATUS_NOT_DIVINED
|
||||
|
||||
private def hasInterestingUH(provinceId: ProvinceId, gs: GameState): Boolean =
|
||||
private def hasInterestingUH(provinceId: ProvinceId, gs: GameStateProto): Boolean =
|
||||
gs.provinces(provinceId)
|
||||
.unaffiliatedHeroes
|
||||
.exists(isInterestingUH)
|
||||
|
||||
def maybeChosenTravelToRecruitCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand]
|
||||
): Option[CommandSelection] =
|
||||
flatSelectionForType[TravelAvailableCommand](availableCommands) { availableCommand =>
|
||||
@@ -1810,7 +1821,7 @@ object CommandChoiceHelpers {
|
||||
|
||||
def chosenTrainCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand]
|
||||
): Option[CommandSelection] =
|
||||
selectionForType[TrainAvailableCommand](availableCommands) { availableCommand =>
|
||||
@@ -1827,7 +1838,7 @@ object CommandChoiceHelpers {
|
||||
|
||||
private def organizeForFightingBeastsCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand],
|
||||
beastPower: Int
|
||||
): Option[CommandSelection] =
|
||||
@@ -1863,7 +1874,7 @@ object CommandChoiceHelpers {
|
||||
|
||||
private def maybeChosenArmTroopsCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand],
|
||||
reason: String
|
||||
): Option[CommandSelection] =
|
||||
@@ -1913,7 +1924,7 @@ object CommandChoiceHelpers {
|
||||
end if
|
||||
}
|
||||
|
||||
private def provinceFoodSurplus(provinceId: ProvinceId, gs: GameState): Int =
|
||||
private def provinceFoodSurplus(provinceId: ProvinceId, gs: GameStateProto): Int =
|
||||
(LegacyProvinceUtils.absoluteFoodSurplus(
|
||||
provinceId,
|
||||
gs
|
||||
@@ -1922,7 +1933,7 @@ object CommandChoiceHelpers {
|
||||
|
||||
private def destinationCloserTo(
|
||||
actingFactionId: FactionId,
|
||||
gs: GameState,
|
||||
gs: GameStateProto,
|
||||
actingProvinceId: ProvinceId,
|
||||
availableProvinceIds: Vector[ProvinceId],
|
||||
pid: ProvinceId
|
||||
@@ -1943,7 +1954,7 @@ object CommandChoiceHelpers {
|
||||
|
||||
private def topUpBattalionToFightBeastsCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
organizeCommand: OrganizeTroopsAvailableCommand,
|
||||
province: Province,
|
||||
costs: Map[BattalionTypeId, Double],
|
||||
@@ -2001,7 +2012,7 @@ object CommandChoiceHelpers {
|
||||
|
||||
private def newBattalionToFightBeastsCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
organizeCommand: AvailableCommand,
|
||||
province: Province,
|
||||
costs: Map[BattalionTypeId, Double],
|
||||
|
||||
+69
-1
@@ -4,8 +4,10 @@ import scala.annotation.tailrec
|
||||
|
||||
import net.eagle0.common.{FunctionalRandom, RandomState}
|
||||
import net.eagle0.eagle.api.available_command.AvailableCommand
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.internal.game_state.GameState as GameStateProto
|
||||
import net.eagle0.eagle.library.util.CommandSelection
|
||||
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.FactionId
|
||||
|
||||
@FunctionalInterface
|
||||
@@ -35,6 +37,34 @@ object CommandChooser {
|
||||
}
|
||||
case _ => RandomState(None, functionalRandom)
|
||||
}
|
||||
|
||||
// Proto-accepting overload for backward compatibility with files still using proto GameState
|
||||
@tailrec
|
||||
def choose(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand],
|
||||
rankedChoosers: Vector[CommandChooserProto],
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Option[CommandSelection]] = rankedChoosers match {
|
||||
case h +: t =>
|
||||
h(actingFactionId, gameState, availableCommands, functionalRandom) match {
|
||||
case rss @ RandomState(Some(_), _) => rss
|
||||
case RandomState(None, nextFr) =>
|
||||
choose(actingFactionId, gameState, availableCommands, t, nextFr)
|
||||
}
|
||||
case _ => RandomState(None, functionalRandom)
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
trait CommandChooserProto {
|
||||
def apply(
|
||||
factionId: FactionId,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand],
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Option[CommandSelection]]
|
||||
}
|
||||
|
||||
object CommandChooserImplicits {
|
||||
@@ -74,3 +104,41 @@ object CommandChooserImplicits {
|
||||
randomChooser(factionId, gameState, availableCommands, functionalRandom)
|
||||
}
|
||||
}
|
||||
|
||||
object CommandChooserProtoImplicits {
|
||||
implicit class DeterministicCommandChooserProto(
|
||||
deterministicChooser: (
|
||||
FactionId,
|
||||
GameStateProto,
|
||||
Vector[AvailableCommand]
|
||||
) => Option[CommandSelection]
|
||||
) extends CommandChooserProto {
|
||||
override def apply(
|
||||
factionId: FactionId,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand],
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Option[CommandSelection]] =
|
||||
RandomState(
|
||||
deterministicChooser(factionId, gameState, availableCommands),
|
||||
functionalRandom
|
||||
)
|
||||
}
|
||||
|
||||
implicit class RandomCommandChooserProto(
|
||||
randomChooser: (
|
||||
FactionId,
|
||||
GameStateProto,
|
||||
Vector[AvailableCommand],
|
||||
FunctionalRandom
|
||||
) => RandomState[Option[CommandSelection]]
|
||||
) extends CommandChooserProto {
|
||||
override def apply(
|
||||
factionId: FactionId,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand],
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Option[CommandSelection]] =
|
||||
randomChooser(factionId, gameState, availableCommands, functionalRandom)
|
||||
}
|
||||
}
|
||||
|
||||
+56
-34
@@ -7,15 +7,17 @@ import net.eagle0.eagle.{FactionId, HeroId, ProvinceId}
|
||||
import net.eagle0.eagle.api.available_command.{AvailableCommand, MarchAvailableCommand, MarchCommandFromOneProvince}
|
||||
import net.eagle0.eagle.api.selected_command.MarchSelectedCommand
|
||||
import net.eagle0.eagle.common.combat_unit.CombatUnit
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.internal.province.Province
|
||||
import net.eagle0.eagle.internal.game_state.GameState as GameStateProto
|
||||
import net.eagle0.eagle.library.settings.MaxCombatUnitCountPerSide
|
||||
import net.eagle0.eagle.library.util.{CommandSelection, LegacyBattalionUtils}
|
||||
import net.eagle0.eagle.library.util.{BattalionUtils, CommandSelection}
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.AvailableCommandSelector.randomSelectionForType
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.ProvinceGoldSurplusCalculator.provinceGoldSurplus
|
||||
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
|
||||
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
|
||||
import net.eagle0.eagle.library.util.faction_utils.FactionUtils
|
||||
import net.eagle0.eagle.library.util.province.ProvinceUtils
|
||||
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
|
||||
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.province.ProvinceT
|
||||
|
||||
object ExpandCommandSelector {
|
||||
private case class MarchUnitsWithFood(units: Vector[CombatUnit], food: Int)
|
||||
@@ -37,7 +39,7 @@ object ExpandCommandSelector {
|
||||
|
||||
if (rulingFactionHeroIds.size < 2) // Don't abandon this province
|
||||
|| (rulingFactionHeroIds.size - rulingFactionHeroIds.count(
|
||||
LegacyFactionUtils.isFactionLeader(_, gameState)
|
||||
FactionUtils.isFactionLeader(_, gameState.factions.values)
|
||||
) < 2) // Don't leave a faction leader alone
|
||||
then randomNone
|
||||
else
|
||||
@@ -52,7 +54,7 @@ object ExpandCommandSelector {
|
||||
.map(_.provinceId)
|
||||
.toVector
|
||||
).partition(p =>
|
||||
LegacyProvinceUtils
|
||||
ProvinceUtils
|
||||
.isAdjacentEnemy(p.id, actingFactionId, gameState)
|
||||
)
|
||||
|
||||
@@ -90,20 +92,16 @@ object ExpandCommandSelector {
|
||||
gameState: GameState,
|
||||
fromProvince: ProvinceId,
|
||||
eligibleDestinations: Vector[ProvinceId]
|
||||
): Vector[Province] =
|
||||
): Vector[ProvinceT] =
|
||||
eligibleDestinations
|
||||
.map(gameState.provinces)
|
||||
.filterNot(_.rulingFactionId.isDefined)
|
||||
.filterNot(
|
||||
_.incomingArmies.exists(ma =>
|
||||
ma.army
|
||||
.map(_.factionId)
|
||||
.contains(
|
||||
gameState
|
||||
.provinces(fromProvince)
|
||||
.rulingFactionId
|
||||
.get
|
||||
)
|
||||
gameState
|
||||
.provinces(fromProvince)
|
||||
.rulingFactionId
|
||||
.contains(ma.army.factionId)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -113,7 +111,7 @@ object ExpandCommandSelector {
|
||||
gameState: GameState,
|
||||
availableCommand: MarchAvailableCommand,
|
||||
oneProvinceCommand: MarchCommandFromOneProvince,
|
||||
safeExpansionCandidateProvinces: Vector[Province],
|
||||
safeExpansionCandidateProvinces: Vector[ProvinceT],
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Option[CommandSelection]] = {
|
||||
val province = gameState.provinces(availableCommand.actingProvinceId)
|
||||
@@ -157,7 +155,7 @@ object ExpandCommandSelector {
|
||||
gameState: GameState,
|
||||
availableCommand: MarchAvailableCommand,
|
||||
oneProvinceCommand: MarchCommandFromOneProvince,
|
||||
adjacentEnemyCandidateProvinces: Vector[Province],
|
||||
adjacentEnemyCandidateProvinces: Vector[ProvinceT],
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Option[CommandSelection]] = {
|
||||
val province = gameState.provinces(availableCommand.actingProvinceId)
|
||||
@@ -175,7 +173,7 @@ object ExpandCommandSelector {
|
||||
else {
|
||||
// If the originating province is safe, only leave two heroes behind
|
||||
val heroCountToSend = (
|
||||
if LegacyProvinceUtils.isAdjacentEnemy(
|
||||
if ProvinceUtils.isAdjacentEnemy(
|
||||
oneProvinceCommand.originProvinceId,
|
||||
actingFactionId,
|
||||
gameState
|
||||
@@ -251,8 +249,8 @@ object ExpandCommandSelector {
|
||||
actingFactionId: FactionId,
|
||||
heroCountToSend: Int,
|
||||
goldToSend: Int,
|
||||
originatingProvince: Province,
|
||||
candidateDestinationProvinces: Vector[Province],
|
||||
originatingProvince: ProvinceT,
|
||||
candidateDestinationProvinces: Vector[ProvinceT],
|
||||
oneProvinceCommand: MarchCommandFromOneProvince,
|
||||
availableCommand: MarchAvailableCommand,
|
||||
gameState: GameState,
|
||||
@@ -268,8 +266,8 @@ object ExpandCommandSelector {
|
||||
rulingFactionHeroIds.diff(oneProvinceCommand.availableHeroIds)
|
||||
|
||||
// Don't leave a faction leader alone in the origin province
|
||||
if heroCountLeftBehind == 1 && notAvailableHeroes.size == 1 && LegacyFactionUtils
|
||||
.isFactionLeader(notAvailableHeroes.head, gameState)
|
||||
if heroCountLeftBehind == 1 && notAvailableHeroes.size == 1 && FactionUtils
|
||||
.isFactionLeader(notAvailableHeroes.head, gameState.factions.values)
|
||||
then {
|
||||
return randomNone
|
||||
}
|
||||
@@ -297,9 +295,8 @@ object ExpandCommandSelector {
|
||||
fid = actingFactionId,
|
||||
factionLeaders = gameState
|
||||
.factions(actingFactionId)
|
||||
.leaders
|
||||
.intersect(originatingProvince.rulingFactionHeroIds)
|
||||
.toVector,
|
||||
.leaderIds
|
||||
.intersect(originatingProvince.rulingFactionHeroIds),
|
||||
otherCountLeftBehind =
|
||||
originatingProvince.rulingFactionHeroIds.length - oneProvinceCommand.availableHeroIds.length,
|
||||
functionalRandom = innerFr
|
||||
@@ -310,19 +307,19 @@ object ExpandCommandSelector {
|
||||
rulingFactionHeroIds.filterNot(takenHeroes.contains)
|
||||
|
||||
internalRequire(
|
||||
takenHeroes.length > 1 || !LegacyFactionUtils
|
||||
takenHeroes.length > 1 || !FactionUtils
|
||||
.isFactionLeader(
|
||||
heroId = takenHeroes.head,
|
||||
gameState = gameState
|
||||
factions = gameState.factions.values
|
||||
),
|
||||
"trying to take only a faction leader!"
|
||||
)
|
||||
|
||||
internalRequire(
|
||||
leftHeroes.length > 1 || !LegacyFactionUtils
|
||||
leftHeroes.length > 1 || !FactionUtils
|
||||
.isFactionLeader(
|
||||
heroId = leftHeroes.head,
|
||||
gameState = gameState
|
||||
factions = gameState.factions.values
|
||||
),
|
||||
"trying to leave a faction leader alone!"
|
||||
)
|
||||
@@ -335,9 +332,9 @@ object ExpandCommandSelector {
|
||||
)
|
||||
.map(gameState.battalions)
|
||||
val monthlyFoodConsumptionInOrigin =
|
||||
LegacyBattalionUtils.monthlyConsumedFood(
|
||||
BattalionUtils.monthlyConsumedFood(
|
||||
battalionsHeldBack,
|
||||
gameState.battalionTypes.toVector
|
||||
gameState.battalionTypes
|
||||
)
|
||||
val foodToHoldBack = monthlyFoodConsumptionInOrigin *
|
||||
FoodConsumptionUtils.foodConsumptionMonthsToHold(
|
||||
@@ -424,7 +421,7 @@ object ExpandCommandSelector {
|
||||
heroCount = heroCountToSend,
|
||||
battalionCount = battalionCountToSend,
|
||||
fid = actingFactionId,
|
||||
factionLeaders = gameState.factions(actingFactionId).leaders.toVector,
|
||||
factionLeaders = gameState.factions(actingFactionId).leaderIds,
|
||||
otherCountLeftBehind = originProvince.rulingFactionHeroIds.length - opmc.availableHeroIds.length,
|
||||
functionalRandom = functionalRandom
|
||||
).map { marchingUnitsWithFood =>
|
||||
@@ -451,7 +448,7 @@ object ExpandCommandSelector {
|
||||
gameState: GameState,
|
||||
fromProvince: ProvinceId,
|
||||
eligibleDestinations: Vector[ProvinceId]
|
||||
): Vector[Province] = {
|
||||
): Vector[ProvinceT] = {
|
||||
val p = gameState.provinces(fromProvince)
|
||||
eligibleDestinations
|
||||
.map(gameState.provinces)
|
||||
@@ -533,4 +530,29 @@ object ExpandCommandSelector {
|
||||
end if
|
||||
}
|
||||
}
|
||||
|
||||
// Proto-accepting overloads for backward compatibility
|
||||
def expansionCandidateProvinces(
|
||||
gameState: GameStateProto,
|
||||
fromProvince: ProvinceId,
|
||||
eligibleDestinations: Vector[ProvinceId]
|
||||
): Vector[ProvinceT] =
|
||||
expansionCandidateProvinces(
|
||||
GameStateConverter.fromProto(gameState),
|
||||
fromProvince,
|
||||
eligibleDestinations
|
||||
)
|
||||
|
||||
def maybeChosenExpandCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand],
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Option[CommandSelection]] =
|
||||
maybeChosenExpandCommand(
|
||||
actingFactionId,
|
||||
GameStateConverter.fromProto(gameState),
|
||||
availableCommands,
|
||||
functionalRandom
|
||||
)
|
||||
}
|
||||
|
||||
+13
-6
@@ -1,10 +1,11 @@
|
||||
package net.eagle0.eagle.library.util.command_choice_helpers
|
||||
|
||||
import net.eagle0.eagle.common.date.Date
|
||||
import net.eagle0.eagle.common.round_phase.RoundPhase
|
||||
import net.eagle0.eagle.common.round_phase.RoundPhase.FOOD_CONSUMPTION
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.internal.game_state.GameState as GameStateProto
|
||||
import net.eagle0.eagle.library.settings.MinSupportForTaxes
|
||||
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
|
||||
import net.eagle0.eagle.model.state.date.Date
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.RoundPhase
|
||||
import net.eagle0.eagle.ProvinceId
|
||||
|
||||
object FoodConsumptionUtils {
|
||||
@@ -12,8 +13,8 @@ object FoodConsumptionUtils {
|
||||
date: Date,
|
||||
roundPhase: RoundPhase
|
||||
): Int =
|
||||
if roundPhase.value < FOOD_CONSUMPTION.value then 13 - date.month
|
||||
else 12 - date.month
|
||||
if roundPhase.value < RoundPhase.FoodConsumption.value then 13 - date.month.value
|
||||
else 12 - date.month.value
|
||||
|
||||
private def foodConsumptionMonthsRemainingInYear(gameState: GameState): Int =
|
||||
foodConsumptionMonthsRemainingInYear(
|
||||
@@ -28,4 +29,10 @@ object FoodConsumptionUtils {
|
||||
if gameState.provinces(provinceId).support >= MinSupportForTaxes.doubleValue
|
||||
then foodConsumptionMonthsRemainingInYear(gameState)
|
||||
else foodConsumptionMonthsRemainingInYear(gameState).max(6)
|
||||
|
||||
def foodConsumptionMonthsToHold(
|
||||
provinceId: ProvinceId,
|
||||
gameState: GameStateProto
|
||||
): Int =
|
||||
foodConsumptionMonthsToHold(provinceId, GameStateConverter.fromProto(gameState))
|
||||
}
|
||||
|
||||
+5
-5
@@ -1,12 +1,12 @@
|
||||
package net.eagle0.eagle.library.util.command_choice_helpers
|
||||
|
||||
import net.eagle0.eagle.internal.hero.Hero
|
||||
import net.eagle0.eagle.library.util.hero.LegacyHeroUtils
|
||||
import net.eagle0.eagle.library.util.hero.HeroUtils
|
||||
import net.eagle0.eagle.model.state.hero.HeroT
|
||||
|
||||
object HeroSelector {
|
||||
def minimallyFatiguedHeroes(heroes: Vector[Hero]): Vector[Hero] = {
|
||||
val minimumFatigue = heroes.map(LegacyHeroUtils.fatigue).min
|
||||
def minimallyFatiguedHeroes(heroes: Vector[HeroT]): Vector[HeroT] = {
|
||||
val minimumFatigue = heroes.map(HeroUtils.fatigue).min
|
||||
heroes
|
||||
.filter(LegacyHeroUtils.fatigue(_) == minimumFatigue)
|
||||
.filter(HeroUtils.fatigue(_) == minimumFatigue)
|
||||
}
|
||||
}
|
||||
|
||||
+34
-7
@@ -5,21 +5,23 @@ import net.eagle0.eagle.api.available_command.{AvailableCommand, ImproveAvailabl
|
||||
import net.eagle0.eagle.api.selected_command.ImproveSelectedCommand
|
||||
import net.eagle0.eagle.common.improvement_type.ImprovementType
|
||||
import net.eagle0.eagle.common.improvement_type.ImprovementType.DEVASTATION
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.internal.province.Province
|
||||
import net.eagle0.eagle.internal.game_state.GameState as GameStateProto
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.AvailableCommandSelector.{
|
||||
flatSelectionForType,
|
||||
selectionForType
|
||||
}
|
||||
import net.eagle0.eagle.library.util.hero.LegacyHeroUtils
|
||||
import net.eagle0.eagle.library.util.hero.HeroUtils
|
||||
import net.eagle0.eagle.library.util.CommandSelection
|
||||
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.province.ProvinceT
|
||||
|
||||
object ImproveCommandSelector {
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.HeroSelector.minimallyFatiguedHeroes
|
||||
|
||||
private def provinceValueForType(
|
||||
improvementType: ImprovementType,
|
||||
province: Province
|
||||
province: ProvinceT
|
||||
): Double = improvementType match {
|
||||
case ImprovementType.ECONOMY => province.economy
|
||||
case ImprovementType.AGRICULTURE => province.agriculture
|
||||
@@ -31,7 +33,7 @@ object ImproveCommandSelector {
|
||||
|
||||
private def chosenType(
|
||||
availableTypes: Vector[ImprovementType],
|
||||
province: Province
|
||||
province: ProvinceT
|
||||
): ImprovementType =
|
||||
if availableTypes.contains(DEVASTATION) then DEVASTATION
|
||||
else availableTypes.minBy(provinceValueForType(_, province))
|
||||
@@ -80,7 +82,7 @@ object ImproveCommandSelector {
|
||||
improveCommand.availableHeroIds.toVector.map(
|
||||
gameState.heroes(_)
|
||||
)
|
||||
).maxBy(LegacyHeroUtils.power(_)).id
|
||||
).maxBy(HeroUtils.power(_)).id
|
||||
),
|
||||
reason = "chosenSpecificImprovementCommand"
|
||||
)
|
||||
@@ -105,7 +107,7 @@ object ImproveCommandSelector {
|
||||
improveAvailableCommand.availableHeroIds.toVector.map(
|
||||
gameState.heroes(_)
|
||||
)
|
||||
).maxBy(LegacyHeroUtils.power(_)).id
|
||||
).maxBy(HeroUtils.power(_)).id
|
||||
),
|
||||
reason = "ImproveCommandSelector.commandSelection"
|
||||
)
|
||||
@@ -118,4 +120,29 @@ object ImproveCommandSelector {
|
||||
selectionForType[ImproveAvailableCommand](availableCommands) { improveCommand =>
|
||||
commandSelection(gameState, actingFactionId, improveCommand)
|
||||
}
|
||||
|
||||
// Proto-accepting overloads for backward compatibility
|
||||
def chosenImproveCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand]
|
||||
): Option[CommandSelection] =
|
||||
chosenImproveCommand(
|
||||
actingFactionId,
|
||||
GameStateConverter.fromProto(gameState),
|
||||
availableCommands
|
||||
)
|
||||
|
||||
def chosenSpecificImprovementCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand],
|
||||
improvementType: ImprovementType
|
||||
): Option[CommandSelection] =
|
||||
chosenSpecificImprovementCommand(
|
||||
actingFactionId,
|
||||
GameStateConverter.fromProto(gameState),
|
||||
availableCommands,
|
||||
improvementType
|
||||
)
|
||||
}
|
||||
|
||||
+2
-4
@@ -1,14 +1,12 @@
|
||||
package net.eagle0.eagle.library.util.command_choice_helpers
|
||||
|
||||
import net.eagle0.eagle.internal.battalion.Battalion
|
||||
|
||||
object MarchSuppliesHelpers {
|
||||
def goldToTakeOnAttack(unitCount: Int, goldAvailable: Int): Int =
|
||||
goldAvailable.min(200 * unitCount)
|
||||
|
||||
def foodToTakeOnAttack(
|
||||
battalions: Vector[Battalion],
|
||||
battalionCount: Int,
|
||||
foodAvailable: Int
|
||||
): Int =
|
||||
foodAvailable.min(300 * battalions.size)
|
||||
foodAvailable.min(300 * battalionCount)
|
||||
}
|
||||
|
||||
+64
-30
@@ -5,14 +5,17 @@ import scala.annotation.tailrec
|
||||
import net.eagle0.eagle.api.available_command.{AvailableCommand, OrganizeTroopsAvailableCommand}
|
||||
import net.eagle0.eagle.api.selected_command.OrganizeTroopsSelectedCommand
|
||||
import net.eagle0.eagle.api.selected_command.OrganizeTroopsSelectedCommand.{ChangedBattalion, NewBattalion}
|
||||
import net.eagle0.eagle.common.battalion_type.{BattalionType, BattalionTypeId}
|
||||
import net.eagle0.eagle.internal.battalion.Battalion
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.internal.game_state.GameState as GameStateProto
|
||||
import net.eagle0.eagle.library.util.{BattalionTypeFinder, CommandSelection}
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.AvailableCommandSelector.flatSelectionForType
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.ProvinceGoldSurplusCalculator.provinceGoldSurplus
|
||||
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
|
||||
import net.eagle0.eagle.library.util.province.ProvinceUtils
|
||||
import net.eagle0.eagle.library.util.BattalionTypeFinder.battalionType
|
||||
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
|
||||
import net.eagle0.eagle.model.proto_converters.BattalionTypeIdConverter
|
||||
import net.eagle0.eagle.model.state.{BattalionType, BattalionTypeId}
|
||||
import net.eagle0.eagle.model.state.battalion.BattalionT
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.FactionId
|
||||
|
||||
object OrganizeCommandSelector {
|
||||
@@ -32,7 +35,7 @@ object OrganizeCommandSelector {
|
||||
gold: Int,
|
||||
foodSurplus: Int,
|
||||
costs: Map[BattalionTypeId, Double],
|
||||
existingBattalions: Vector[Battalion],
|
||||
existingBattalions: Vector[BattalionT],
|
||||
battalionTypes: Vector[BattalionType],
|
||||
eligibleBattalionTypeIds: Vector[BattalionTypeId]
|
||||
): ChangedBattalionsResult = {
|
||||
@@ -40,17 +43,19 @@ object OrganizeCommandSelector {
|
||||
def go(
|
||||
gold: Int,
|
||||
foodSurplus: Int,
|
||||
battalionsInNeed: Vector[Battalion],
|
||||
battalionsInNeed: Vector[BattalionT],
|
||||
acc: Vector[ChangedBattalion]
|
||||
): ChangedBattalionsResult = battalionsInNeed match {
|
||||
case b +: bs =>
|
||||
val missing = battalionType(b.`type`, battalionTypes).capacity - b.size
|
||||
val goldCost = (missing * costs(b.`type`)).ceil.toInt
|
||||
val missing = BattalionTypeFinder.findBattalionType(b.typeId, battalionTypes).capacity - b.size
|
||||
val goldCost = (missing * costs(b.typeId)).ceil.toInt
|
||||
val foodCost =
|
||||
(missing * battalionType(
|
||||
b.`type`,
|
||||
battalionTypes
|
||||
).monthlyFoodCost).ceil.toInt
|
||||
(missing * BattalionTypeFinder
|
||||
.findBattalionType(
|
||||
b.typeId,
|
||||
battalionTypes
|
||||
)
|
||||
.monthlyFoodCost).ceil.toInt
|
||||
if goldCost <= gold && foodCost <= foodSurplus then
|
||||
go(
|
||||
gold - goldCost,
|
||||
@@ -77,9 +82,9 @@ object OrganizeCommandSelector {
|
||||
gold = gold,
|
||||
foodSurplus = foodSurplus,
|
||||
battalionsInNeed = existingBattalions
|
||||
.filter(b => eligibleBattalionTypeIds.contains(b.`type`))
|
||||
.filter(b => costs.contains(b.`type`))
|
||||
.filter(b => b.size < battalionType(b.`type`, battalionTypes).capacity),
|
||||
.filter(b => eligibleBattalionTypeIds.contains(b.typeId))
|
||||
.filter(b => costs.contains(b.typeId))
|
||||
.filter(b => b.size < BattalionTypeFinder.findBattalionType(b.typeId, battalionTypes).capacity),
|
||||
acc = Vector()
|
||||
)
|
||||
}
|
||||
@@ -90,7 +95,7 @@ object OrganizeCommandSelector {
|
||||
costs: Map[BattalionTypeId, Double],
|
||||
allBattalionTypes: Vector[BattalionType],
|
||||
eligibleBattalionTypeIds: Vector[BattalionTypeId],
|
||||
existingBattalions: Vector[Battalion],
|
||||
existingBattalions: Vector[BattalionT],
|
||||
maxCount: Int = 100
|
||||
): NewBattalionsResult = {
|
||||
val eligibleCosts = costs.filter {
|
||||
@@ -120,13 +125,13 @@ object OrganizeCommandSelector {
|
||||
|
||||
eligibleCosts.toVector.map {
|
||||
case (tid, costPerTroop) =>
|
||||
val battalionType =
|
||||
BattalionTypeFinder.battalionType(tid, allBattalionTypes)
|
||||
val cap = battalionType.capacity
|
||||
val bt =
|
||||
BattalionTypeFinder.findBattalionType(tid, allBattalionTypes)
|
||||
val cap = bt.capacity
|
||||
FoodAndGoldCosts(
|
||||
battalionTypeId = tid,
|
||||
goldCost = (costPerTroop * cap).ceil.toInt,
|
||||
monthlyFoodCost = (cap * battalionType.monthlyFoodCost).ceil.toInt
|
||||
monthlyFoodCost = (cap * bt.monthlyFoodCost).ceil.toInt
|
||||
)
|
||||
}
|
||||
.filter(_.goldCost <= gold)
|
||||
@@ -142,8 +147,8 @@ object OrganizeCommandSelector {
|
||||
.headOption match {
|
||||
case Some(x) =>
|
||||
val newBattalion = NewBattalion(
|
||||
`type` = x.battalionTypeId,
|
||||
newTroops = battalionType(x.battalionTypeId, allBattalionTypes).capacity
|
||||
`type` = BattalionTypeIdConverter.toProto(x.battalionTypeId),
|
||||
newTroops = BattalionTypeFinder.findBattalionType(x.battalionTypeId, allBattalionTypes).capacity
|
||||
)
|
||||
go(
|
||||
gold = gold - x.goldCost,
|
||||
@@ -166,7 +171,7 @@ object OrganizeCommandSelector {
|
||||
gold = gold,
|
||||
foodSurplus = foodSurplus,
|
||||
acc = Vector(),
|
||||
existingBattalionTypeCounts = existingBattalions.groupBy(_.`type`).map { case (k, v) => k -> v.size }
|
||||
existingBattalionTypeCounts = existingBattalions.groupBy(_.typeId).map { case (k, v) => k -> v.size }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -178,7 +183,7 @@ object OrganizeCommandSelector {
|
||||
reason: String
|
||||
): Option[CommandSelection] = {
|
||||
val foodSurplus =
|
||||
LegacyProvinceUtils.monthlyFoodSurplus(
|
||||
ProvinceUtils.monthlyFoodSurplus(
|
||||
gameState = gameState,
|
||||
provinceId = availableCommand.actingProvinceId
|
||||
) + extraFoodSurplus
|
||||
@@ -187,7 +192,7 @@ object OrganizeCommandSelector {
|
||||
if foodSurplus <= 0 || goldSurplus <= 0 then None
|
||||
else {
|
||||
val costs = availableCommand.troopCosts
|
||||
.map(tc => tc.`type` -> tc.costPerTroop)
|
||||
.map(tc => BattalionTypeId.fromInt(tc.`type`.value) -> tc.costPerTroop)
|
||||
.toMap
|
||||
|
||||
val toppedBattalionResults = changedBattalions(
|
||||
@@ -199,11 +204,11 @@ object OrganizeCommandSelector {
|
||||
.battalionIds
|
||||
.toVector
|
||||
.map(gameState.battalions(_)),
|
||||
battalionTypes = gameState.battalionTypes.toVector,
|
||||
battalionTypes = gameState.battalionTypes,
|
||||
eligibleBattalionTypeIds = availableCommand.availableBattalionTypes
|
||||
.filter(_.meetsRequirements)
|
||||
.toVector
|
||||
.map(_.typeId)
|
||||
.map(tid => BattalionTypeId.fromInt(tid.typeId.value))
|
||||
)
|
||||
|
||||
val newBattalionResults = newBattalions(
|
||||
@@ -217,14 +222,14 @@ object OrganizeCommandSelector {
|
||||
.provinces(availableCommand.actingProvinceId)
|
||||
.battalionIds
|
||||
.size,
|
||||
allBattalionTypes = gameState.battalionTypes.toVector.filter(bt =>
|
||||
allBattalionTypes = gameState.battalionTypes.filter(bt =>
|
||||
availableCommand.availableBattalionTypes
|
||||
.exists(tp => tp.typeId == bt.typeId && tp.meetsRequirements)
|
||||
.exists(tp => tp.typeId.value == bt.typeId.value && tp.meetsRequirements)
|
||||
),
|
||||
eligibleBattalionTypeIds = availableCommand.availableBattalionTypes
|
||||
.filter(_.meetsRequirements)
|
||||
.toVector
|
||||
.map(_.typeId),
|
||||
.map(tid => BattalionTypeId.fromInt(tid.typeId.value)),
|
||||
existingBattalions = gameState
|
||||
.provinces(availableCommand.actingProvinceId)
|
||||
.battalionIds
|
||||
@@ -279,4 +284,33 @@ object OrganizeCommandSelector {
|
||||
extraFoodSurplus = 0,
|
||||
reason = reason
|
||||
)
|
||||
|
||||
// Proto-accepting overloads for backward compatibility
|
||||
def selectedOrganizeCommand(
|
||||
actingFactionId: FactionId,
|
||||
availableCommand: OrganizeTroopsAvailableCommand,
|
||||
gameState: GameStateProto,
|
||||
extraFoodSurplus: Int,
|
||||
reason: String
|
||||
): Option[CommandSelection] =
|
||||
selectedOrganizeCommand(
|
||||
actingFactionId,
|
||||
availableCommand,
|
||||
GameStateConverter.fromProto(gameState),
|
||||
extraFoodSurplus,
|
||||
reason
|
||||
)
|
||||
|
||||
def chosenOrganizeCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand],
|
||||
reason: String
|
||||
): Option[CommandSelection] =
|
||||
chosenOrganizeCommand(
|
||||
actingFactionId,
|
||||
GameStateConverter.fromProto(gameState),
|
||||
availableCommands,
|
||||
reason
|
||||
)
|
||||
}
|
||||
|
||||
+15
-1
@@ -1,7 +1,9 @@
|
||||
package net.eagle0.eagle.library.util.command_choice_helpers
|
||||
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.internal.game_state.GameState as GameStateProto
|
||||
import net.eagle0.eagle.library.settings.GoldPerHeroHeldBack
|
||||
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.ProvinceId
|
||||
|
||||
object ProvinceGoldSurplusCalculator {
|
||||
@@ -15,6 +17,12 @@ object ProvinceGoldSurplusCalculator {
|
||||
.rulingFactionHeroIds
|
||||
.length).ceil.toInt
|
||||
|
||||
def goldToHoldBack(
|
||||
provinceId: ProvinceId,
|
||||
gameState: GameStateProto
|
||||
): Int =
|
||||
goldToHoldBack(provinceId, GameStateConverter.fromProto(gameState))
|
||||
|
||||
def provinceGoldSurplus(
|
||||
provinceId: ProvinceId,
|
||||
gameState: GameState
|
||||
@@ -23,4 +31,10 @@ object ProvinceGoldSurplusCalculator {
|
||||
provinceId,
|
||||
gameState
|
||||
)).max(0)
|
||||
|
||||
def provinceGoldSurplus(
|
||||
provinceId: ProvinceId,
|
||||
gameState: GameStateProto
|
||||
): Int =
|
||||
provinceGoldSurplus(provinceId, GameStateConverter.fromProto(gameState))
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
package net.eagle0.eagle.library.util.faction_utils
|
||||
|
||||
import net.eagle0.eagle.{FactionId, HeroId}
|
||||
import net.eagle0.eagle.{FactionId, HeroId, ProvinceId}
|
||||
import net.eagle0.eagle.library.settings.{MinSupportForTaxes, PrestigePerSupportedProvince}
|
||||
import net.eagle0.eagle.model.state.faction.{FactionRelationship, FactionT}
|
||||
import net.eagle0.eagle.model.state.faction.FactionRelationship.RelationshipLevel.Ally
|
||||
import net.eagle0.eagle.model.state.province.ProvinceT
|
||||
|
||||
object FactionUtils {
|
||||
def isFactionLeader(heroId: HeroId, factions: Vector[FactionT]): Boolean =
|
||||
factions.flatMap(_.leaderIds).contains(heroId)
|
||||
def isFactionLeader(heroId: HeroId, factions: Iterable[FactionT]): Boolean =
|
||||
factions.flatMap(_.leaderIds).exists(_ == heroId)
|
||||
|
||||
def factionRelationship(
|
||||
by: FactionId,
|
||||
@@ -56,6 +56,13 @@ object FactionUtils {
|
||||
): Boolean =
|
||||
hasTruce(fid1, fid2, factions) || hasAlliance(fid1, fid2, factions)
|
||||
|
||||
def factionsAreHostile(
|
||||
fid1: FactionId,
|
||||
fid2: FactionId,
|
||||
factions: Vector[FactionT]
|
||||
): Boolean =
|
||||
fid1 != fid2 && !hasTruceOrAlliance(fid1, fid2, factions)
|
||||
|
||||
private def incomingHids(province: ProvinceT): Vector[HeroId] =
|
||||
province.incomingArmies
|
||||
.map(_.army)
|
||||
@@ -136,4 +143,38 @@ object FactionUtils {
|
||||
allProvinces
|
||||
.filter(_.rulingFactionId.contains(factionId))
|
||||
.count(_.support >= MinSupportForTaxes.doubleValue)
|
||||
|
||||
def isFactionHead(heroId: HeroId, factions: Iterable[FactionT]): Boolean =
|
||||
factions.exists(_.factionHeadId == heroId)
|
||||
|
||||
def leadersBesidesHead(factionId: FactionId, factions: Iterable[FactionT]): Vector[HeroId] =
|
||||
factions
|
||||
.find(_.id == factionId)
|
||||
.map(f => f.leaderIds.filterNot(_ == f.factionHeadId))
|
||||
.getOrElse(Vector.empty)
|
||||
|
||||
def factionHeadId(factionId: FactionId, factions: Iterable[FactionT]): Option[HeroId] =
|
||||
factions.find(_.id == factionId).map(_.factionHeadId)
|
||||
|
||||
def ownedNeighbors(
|
||||
provinces: ProvinceId => ProvinceT,
|
||||
fid: FactionId
|
||||
): ProvinceId => Vector[ProvinceId] =
|
||||
pid =>
|
||||
provinces(pid).neighbors
|
||||
.map(_.provinceId)
|
||||
.filter(n => provinces(n).rulingFactionId.contains(fid))
|
||||
|
||||
def hostileNeighbors(
|
||||
province: ProvinceT,
|
||||
fid: FactionId,
|
||||
factions: Vector[FactionT],
|
||||
provinces: ProvinceId => ProvinceT
|
||||
): Vector[ProvinceT] =
|
||||
province.neighbors
|
||||
.map(_.provinceId)
|
||||
.map(provinces)
|
||||
.filter(_.rulingFactionId.isDefined)
|
||||
.filterNot(_.rulingFactionId.contains(fid))
|
||||
.filterNot(neighbor => hasTruceOrAlliance(fid, neighbor.rulingFactionId.get, factions))
|
||||
}
|
||||
|
||||
@@ -14,8 +14,10 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/faction",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero:gender",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -2,11 +2,13 @@ package net.eagle0.eagle.library.util.hero
|
||||
|
||||
import scala.annotation.tailrec
|
||||
|
||||
import net.eagle0.eagle.{FactionId, HeroId, ProvinceId}
|
||||
import net.eagle0.eagle.library.util.faction_utils.FactionUtils
|
||||
import net.eagle0.eagle.model.state.faction.FactionT
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.hero.HeroT
|
||||
import net.eagle0.eagle.model.state.hero.Profession.NoProfession
|
||||
import net.eagle0.eagle.HeroId
|
||||
import net.eagle0.eagle.model.state.province.ProvinceT
|
||||
|
||||
object HeroUtils {
|
||||
|
||||
@@ -81,4 +83,39 @@ object HeroUtils {
|
||||
|
||||
def sortOrdering(factionLeaderIds: Vector[HeroId]): Ordering[HeroT] =
|
||||
Ordering.fromLessThan(sortOrderer(factionLeaderIds))
|
||||
|
||||
def fatigue(hero: HeroT): Double = hero.constitution - hero.vigor
|
||||
|
||||
def seniorityOrder(faction: FactionT, heroId: HeroId): Int =
|
||||
faction.leaderIds.indexOf(heroId) match {
|
||||
case -1 => Int.MaxValue
|
||||
case idx => idx
|
||||
}
|
||||
|
||||
// For commands where the faction leader can act on other provinces; returns the list of provinces to consider
|
||||
// for this hero. For a vassal, considers just the one province, but for faction leader returns all owned provinces
|
||||
// that did not contain other faction leaders. Always returns the provinceId as the first element.
|
||||
def consideredProvinces(
|
||||
gameState: GameState,
|
||||
factionId: FactionId,
|
||||
provinceId: ProvinceId
|
||||
): Vector[ProvinceT] =
|
||||
if gameState
|
||||
.provinces(provinceId)
|
||||
.rulingHeroId
|
||||
.exists(hid => FactionUtils.isFactionHead(hid, gameState.factions.values))
|
||||
then
|
||||
gameState.provinces.values
|
||||
.filter(_.rulingFactionId.contains(factionId))
|
||||
.filterNot(p =>
|
||||
p.rulingFactionHeroIds.exists(
|
||||
FactionUtils.leadersBesidesHead(factionId, gameState.factions.values).contains
|
||||
) && !p.rulingFactionHeroIds
|
||||
.exists(
|
||||
FactionUtils.factionHeadId(factionId, gameState.factions.values).contains
|
||||
)
|
||||
)
|
||||
.toVector
|
||||
.sortBy(p => if p.id == provinceId then 0 else p.id)
|
||||
else Vector(gameState.provinces(provinceId))
|
||||
}
|
||||
|
||||
@@ -42,11 +42,13 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:gold_increase_per_economy",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:min_support_for_taxes",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:per_development_resource_limit",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:battalion_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province/concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:army",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:beast_info",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/faction",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero:gender",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package net.eagle0.eagle.library.util.province
|
||||
|
||||
import net.eagle0.eagle.{FactionId, HeroId, RoundId}
|
||||
import net.eagle0.eagle.{FactionId, HeroId, ProvinceId, RoundId}
|
||||
import net.eagle0.eagle.common.battalion_type.BattalionType as BattalionTypeProto
|
||||
import net.eagle0.eagle.library.settings.{
|
||||
BaseResourceLimit,
|
||||
@@ -9,11 +9,13 @@ import net.eagle0.eagle.library.settings.{
|
||||
MinSupportForTaxes,
|
||||
PerDevelopmentResourceLimit
|
||||
}
|
||||
import net.eagle0.eagle.library.util.BattalionUtils
|
||||
import net.eagle0.eagle.library.EagleInternalException
|
||||
import net.eagle0.eagle.model.action_result.changed_province.concrete.ChangedProvinceC
|
||||
import net.eagle0.eagle.model.action_result.changed_province.ChangedProvinceT
|
||||
import net.eagle0.eagle.model.state.{BattalionTypeId, BeastInfo, MovingArmy}
|
||||
import net.eagle0.eagle.model.state.faction.FactionT
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.hero.HeroT
|
||||
import net.eagle0.eagle.model.state.province.{
|
||||
BeastsEvent,
|
||||
@@ -74,6 +76,15 @@ object ProvinceUtils {
|
||||
) * FoodIncreasePerAgriculture.doubleValue).toInt
|
||||
)
|
||||
|
||||
def monthlyFoodConsumption(
|
||||
province: ProvinceT,
|
||||
gs: GameState
|
||||
): Int =
|
||||
BattalionUtils.monthlyConsumedFood(
|
||||
province.battalionIds.map(gs.battalions),
|
||||
gs.battalionTypes
|
||||
)
|
||||
|
||||
def provinceLeader(
|
||||
startingProvince: ProvinceT,
|
||||
rulingFactionOpt: Option[FactionT],
|
||||
@@ -247,6 +258,72 @@ object ProvinceUtils {
|
||||
province.rulingFactionId.nonEmpty && province.rulingFactionHeroIds.isEmpty
|
||||
)(abandonedProvinceCP(province))
|
||||
|
||||
def isAdjacentEnemy(
|
||||
provinceId: ProvinceId,
|
||||
factionId: FactionId,
|
||||
gameState: GameState
|
||||
): Boolean =
|
||||
gameState
|
||||
.provinces(provinceId)
|
||||
.neighbors
|
||||
.map(_.provinceId)
|
||||
.map(gameState.provinces)
|
||||
.exists(
|
||||
_.rulingFactionId.exists(_ != factionId)
|
||||
)
|
||||
|
||||
def adjacentHostiles(
|
||||
provinceId: ProvinceId,
|
||||
gameState: GameState
|
||||
): Vector[ProvinceT] = {
|
||||
val baseProvince = gameState.provinces(provinceId)
|
||||
baseProvince.neighbors
|
||||
.map(_.provinceId)
|
||||
.map(gameState.provinces)
|
||||
.filter(_.rulingFactionId.isDefined)
|
||||
.filter(_.rulingFactionId != baseProvince.rulingFactionId)
|
||||
}
|
||||
|
||||
def locationOf(
|
||||
heroId: HeroId,
|
||||
provinces: Iterable[ProvinceT]
|
||||
): Option[ProvinceId] =
|
||||
provinces
|
||||
.find(p =>
|
||||
(p.rulingFactionHeroIds ++ p.unaffiliatedHeroes.map(_.heroId))
|
||||
.contains(heroId)
|
||||
)
|
||||
.map(_.id)
|
||||
|
||||
def absoluteFoodSurplus(provinceId: ProvinceId, gameState: GameState): Int = {
|
||||
val province = gameState.provinces(provinceId)
|
||||
val monthlyConsumption = monthlyFoodConsumption(province, gameState)
|
||||
val annualProduction = annualFoodProduction(province).getOrElse(0)
|
||||
if annualProduction / 12 < monthlyConsumption then 0
|
||||
else {
|
||||
val currentMonth = gameState.currentDate.map(_.month.value).getOrElse(1)
|
||||
(province.food - monthlyConsumption * (12 - currentMonth)).max(0).toInt
|
||||
}
|
||||
}
|
||||
|
||||
def containsFactionLeader(pid: ProvinceId, gs: GameState): Boolean =
|
||||
gs.provinces(pid).rulingFactionId.exists { fid =>
|
||||
gs.provinces(pid)
|
||||
.rulingFactionHeroIds
|
||||
.contains(gs.factions(fid).factionHeadId)
|
||||
}
|
||||
|
||||
def monthlyFoodSurplus(gameState: GameState, provinceId: ProvinceId): Int = {
|
||||
val province = gameState.provinces(provinceId)
|
||||
val consumption = monthlyFoodConsumption(province, gameState)
|
||||
val monthsRemaining = 12 - gameState.currentDate.get.month.value
|
||||
val monthlyAvailable =
|
||||
if monthsRemaining == 0 then Int.MaxValue
|
||||
else (province.food / monthsRemaining).toInt
|
||||
val monthlyProduction = annualFoodProduction(province).getOrElse(0) / 12
|
||||
monthlyProduction.min(monthlyAvailable) - consumption
|
||||
}
|
||||
|
||||
def afterHeroDeparture(
|
||||
cp: ChangedProvinceT,
|
||||
province: ProvinceT
|
||||
|
||||
@@ -1,14 +1,34 @@
|
||||
package net.eagle0.eagle.library.util.view_filters
|
||||
|
||||
import net.eagle0.eagle.{BattalionId, FactionId}
|
||||
import net.eagle0.eagle.internal.army.Army
|
||||
import net.eagle0.eagle.internal.battalion.Battalion
|
||||
import net.eagle0.eagle.internal.army.Army as ProtoArmy
|
||||
import net.eagle0.eagle.internal.battalion.Battalion as ProtoBattalion
|
||||
import net.eagle0.eagle.model.proto_converters.CombatUnitConverter
|
||||
import net.eagle0.eagle.model.state.battalion.BattalionT
|
||||
import net.eagle0.eagle.model.state.Army as ScalaArmy
|
||||
import net.eagle0.eagle.model.view.army.ArmyView as ArmyViewT
|
||||
import net.eagle0.eagle.views.army_view.ArmyView
|
||||
|
||||
object ArmyFilter {
|
||||
// Pure Scala version
|
||||
def filterArmy(
|
||||
army: Army,
|
||||
battalions: Map[BattalionId, Battalion],
|
||||
army: ScalaArmy,
|
||||
battalions: Map[BattalionId, BattalionT],
|
||||
factionId: Option[FactionId]
|
||||
): ArmyViewT =
|
||||
ArmyViewT(
|
||||
factionId = army.factionId,
|
||||
units =
|
||||
if factionId.forall(_ == army.factionId) then army.units
|
||||
else Vector.empty,
|
||||
unitCount = army.units.length,
|
||||
troopCount = army.units.flatMap(_.battalionId).map(bid => battalions(bid)).map(_.size).sum
|
||||
)
|
||||
|
||||
// Proto version (legacy)
|
||||
def filterArmy(
|
||||
army: ProtoArmy,
|
||||
battalions: Map[BattalionId, ProtoBattalion],
|
||||
factionId: Option[FactionId]
|
||||
): ArmyView =
|
||||
ArmyView(
|
||||
@@ -19,4 +39,18 @@ object ArmyFilter {
|
||||
unitCount = army.units.length,
|
||||
troopCount = army.units.flatMap(_.battalionId).map(battalions).map(_.size).sum
|
||||
)
|
||||
|
||||
def filterArmyScala(
|
||||
army: ProtoArmy,
|
||||
battalions: Map[BattalionId, ProtoBattalion],
|
||||
factionId: Option[FactionId]
|
||||
): ArmyViewT =
|
||||
ArmyViewT(
|
||||
factionId = army.factionId,
|
||||
units =
|
||||
if factionId.forall(_ == army.factionId) then army.units.map(CombatUnitConverter.fromProto).toVector
|
||||
else Vector.empty,
|
||||
unitCount = army.units.length,
|
||||
troopCount = army.units.flatMap(_.battalionId).map(battalions).map(_.size).sum
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,11 +8,18 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/library:__subpackages__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/army:army_view",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:army_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:battalion_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/views:army_view_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:combat_unit_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:army",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/army:army_view",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -116,6 +123,7 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/view_filters:faction_view_filter",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/view_filters:hero_view_filter",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/view_filters:province_view_filter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/view/province:province_view",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -146,10 +154,15 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/library:__subpackages__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/battalion",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:battalion_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/views:battalion_view_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:battalion_type_id_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/battalion",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -161,20 +174,41 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/library:__subpackages__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/province:full_province_info",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/province:province_view",
|
||||
],
|
||||
deps = [
|
||||
":army_filter",
|
||||
":battalion_view_filter",
|
||||
":legacy_battalion_view_filter",
|
||||
":visibility",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:beast_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:date_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:incoming_army_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:province_event_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:stat_with_condition_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils:legacy_faction_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/hero:hero_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/hero:legacy_hero_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/province",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/province:legacy_province_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:unaffiliated_hero_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/date",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/hero:profession",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/province:event",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/province:order_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/view/province:full_province_info",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/view/province:province_view",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/incoming_army:incoming_army_view",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/province:full_province_info",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/province:province_view",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/province:unaffiliated_hero_basics",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -13,4 +13,15 @@ object BattalionViewFilter {
|
||||
armament = Some(battT.armament),
|
||||
name = battT.name
|
||||
)
|
||||
|
||||
/** Limited view that hides training and armament (for withdrawn faction views) */
|
||||
def limitedBattalionView(battT: BattalionT): BattalionView =
|
||||
BattalionView(
|
||||
id = battT.id,
|
||||
typeId = battT.typeId,
|
||||
size = battT.size,
|
||||
training = None,
|
||||
armament = None,
|
||||
name = battT.name
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package net.eagle0.eagle.library.util.view_filters
|
||||
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.model.proto_converters.view.province.ProvinceViewConverter
|
||||
import net.eagle0.eagle.views.faction_view.FactionView
|
||||
import net.eagle0.eagle.views.game_state_view.GameStateView
|
||||
import net.eagle0.eagle.FactionId
|
||||
@@ -16,9 +17,11 @@ object GameStateViewFilter {
|
||||
currentDate = gs.currentDate,
|
||||
provinces = gs.provinces.map {
|
||||
case (pid, prov) =>
|
||||
pid -> factionId
|
||||
.map(fid => ProvinceViewFilter.filteredProvinceView(prov, gs, fid))
|
||||
.getOrElse(ProvinceViewFilter.filteredProvinceView(prov, gs))
|
||||
pid -> ProvinceViewConverter.toProto(
|
||||
factionId
|
||||
.map(fid => ProvinceViewFilter.filteredProvinceView(prov, gs, fid))
|
||||
.getOrElse(ProvinceViewFilter.filteredProvinceView(prov, gs))
|
||||
)
|
||||
},
|
||||
heroes = (gs.heroes ++ gs.killedHeroes).map {
|
||||
case (hid, hero) =>
|
||||
|
||||
+22
@@ -1,6 +1,8 @@
|
||||
package net.eagle0.eagle.library.util.view_filters
|
||||
|
||||
import net.eagle0.eagle.internal.battalion.Battalion
|
||||
import net.eagle0.eagle.model.proto_converters.BattalionTypeIdConverter
|
||||
import net.eagle0.eagle.model.view.battalion.BattalionView as BattalionViewT
|
||||
import net.eagle0.eagle.views.battalion_view.BattalionView
|
||||
|
||||
object LegacyBattalionViewFilter {
|
||||
@@ -14,6 +16,16 @@ object LegacyBattalionViewFilter {
|
||||
name = batt.name
|
||||
)
|
||||
|
||||
def filteredBattalionViewBelongingToPlayerScala(batt: Battalion): BattalionViewT =
|
||||
BattalionViewT(
|
||||
id = batt.id,
|
||||
typeId = BattalionTypeIdConverter.fromProto(batt.`type`),
|
||||
size = batt.size,
|
||||
training = Some(batt.training),
|
||||
armament = Some(batt.armament),
|
||||
name = batt.name
|
||||
)
|
||||
|
||||
def limitedBattalionView(batt: Battalion): BattalionView =
|
||||
BattalionView(
|
||||
id = batt.id,
|
||||
@@ -23,4 +35,14 @@ object LegacyBattalionViewFilter {
|
||||
armament = None,
|
||||
name = batt.name
|
||||
)
|
||||
|
||||
def limitedBattalionViewScala(batt: Battalion): BattalionViewT =
|
||||
BattalionViewT(
|
||||
id = batt.id,
|
||||
typeId = BattalionTypeIdConverter.fromProto(batt.`type`),
|
||||
size = batt.size,
|
||||
training = None,
|
||||
armament = None,
|
||||
name = batt.name
|
||||
)
|
||||
}
|
||||
|
||||
+218
-33
@@ -8,14 +8,191 @@ import net.eagle0.eagle.internal.province.Province
|
||||
import net.eagle0.eagle.internal.unaffiliated_hero.UnaffiliatedHero
|
||||
import net.eagle0.eagle.library.util.{IncomingArmyUtils, ProvinceEventUtils}
|
||||
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
|
||||
import net.eagle0.eagle.library.util.hero.HeroUtils
|
||||
import net.eagle0.eagle.library.util.hero.LegacyHeroUtils
|
||||
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
|
||||
import net.eagle0.eagle.library.util.StatWithConditionUtils.supportSc
|
||||
import net.eagle0.eagle.views.incoming_army_view.IncomingArmyView
|
||||
import net.eagle0.eagle.views.province_view.{FullProvinceInfo, ProvinceView, UnaffiliatedHeroBasics}
|
||||
import net.eagle0.eagle.library.util.province.ProvinceUtils
|
||||
import net.eagle0.eagle.library.util.StatWithConditionUtils.supportScala
|
||||
import net.eagle0.eagle.model.proto_converters.date.DateConverter
|
||||
import net.eagle0.eagle.model.proto_converters.hero.ProfessionConverter
|
||||
import net.eagle0.eagle.model.proto_converters.province.{ProvinceEventConverter, ProvinceOrderTypeConverter}
|
||||
import net.eagle0.eagle.model.proto_converters.view.province.{FullProvinceInfoConverter, ProvinceViewConverter}
|
||||
import net.eagle0.eagle.model.proto_converters.UnaffiliatedHeroConverter
|
||||
import net.eagle0.eagle.model.state.game_state.GameState as ScalaGameState
|
||||
import net.eagle0.eagle.model.state.province.ProvinceT
|
||||
import net.eagle0.eagle.model.state.unaffiliated_hero.RecruitmentInfo
|
||||
import net.eagle0.eagle.model.view.incoming_army.{IncomingArmyView, UnitDetails}
|
||||
import net.eagle0.eagle.model.view.province.{FullProvinceInfo, ProvinceView, UnaffiliatedHeroBasics}
|
||||
import net.eagle0.eagle.FactionId
|
||||
|
||||
object ProvinceViewFilter {
|
||||
// =============== Scala GameState overloads ===============
|
||||
|
||||
/** Filtered view for server-side use (no faction visibility restrictions) */
|
||||
def filteredProvinceView(
|
||||
province: ProvinceT,
|
||||
gs: ScalaGameState
|
||||
): ProvinceView = {
|
||||
val factions = gs.factions.values.toVector
|
||||
val factionLeaderIds = factions.flatMap(_.leaderIds)
|
||||
|
||||
val info: FullProvinceInfo = FullProvinceInfo(
|
||||
rulingHeroId = province.rulingHeroId,
|
||||
rulingFactionHeroIds = province.rulingFactionHeroIds
|
||||
.map(hid => gs.heroes(hid))
|
||||
.sortWith(HeroUtils.sortOrderer(factionLeaderIds))
|
||||
.map(_.id),
|
||||
battalions = province.battalionIds
|
||||
.map(bid => gs.battalions(bid))
|
||||
.map(BattalionViewFilter.filteredBattalionViewBelongingToPlayer)
|
||||
.sortBy(_.typeId.value),
|
||||
defendingArmy = province.defendingArmy
|
||||
.map(army => ArmyFilter.filterArmy(army, gs.battalions, None)),
|
||||
economy = province.economy.toFloat,
|
||||
agriculture = province.agriculture.toFloat,
|
||||
infrastructure = province.infrastructure.toFloat,
|
||||
economyDevastation = province.economyDevastation.toFloat,
|
||||
agricultureDevastation = province.agricultureDevastation.toFloat,
|
||||
infrastructureDevastation = province.infrastructureDevastation.toFloat,
|
||||
gold = province.gold,
|
||||
food = province.food,
|
||||
priceIndex = province.priceIndex,
|
||||
goldCap = ProvinceUtils.goldCap(province),
|
||||
foodCap = ProvinceUtils.foodCap(province),
|
||||
support = Some(supportScala(province.support)),
|
||||
foodConsumption = ProvinceUtils.monthlyFoodConsumption(province, gs),
|
||||
provinceOrders = ProvinceOrderTypeConverter.toProto(province.provinceOrders),
|
||||
hexMapName = province.hexMapName,
|
||||
castleCount = province.castleCount,
|
||||
heroCap = province.heroCap,
|
||||
unaffiliatedHeroes = Vector.empty,
|
||||
rulerIsTraveling = province.rulerIsTraveling
|
||||
)
|
||||
|
||||
ProvinceView(
|
||||
id = province.id,
|
||||
name = province.name,
|
||||
rulingFactionId = province.rulingFactionId,
|
||||
fullInfo = Some(info),
|
||||
knownEvents = Vector.empty,
|
||||
asOf = gs.currentDate
|
||||
)
|
||||
}
|
||||
|
||||
/** View shown to a player who withdraws from a province they do not own. (Scala overload) */
|
||||
def withdrawnFromProvinceView(
|
||||
province: ProvinceT,
|
||||
gs: ScalaGameState,
|
||||
factionId: FactionId
|
||||
): ProvinceView = {
|
||||
val reconnedView = for {
|
||||
faction <- gs.factions.get(factionId)
|
||||
pv <- faction.reconnedProvinces.find(_.id == province.id)
|
||||
} yield pv // Already Scala ProvinceView, no conversion needed
|
||||
|
||||
// Convert Scala events to proto for filtering (ProvinceView uses proto events)
|
||||
val activeEventsProto = province.activeEvents.map(ProvinceEventConverter.toProto)
|
||||
val knownEvents = activeEventsProto.filter { pe =>
|
||||
if reconnedView.exists(_.knownEvents.contains(pe)) then true
|
||||
else eventIsUniversallyVisible(pe)
|
||||
}
|
||||
|
||||
val incomingAttackers = myIncomingArmiesScala(province, gs, factionId)
|
||||
|
||||
val factions = gs.factions.values.toVector
|
||||
val factionLeaderIds = factions.flatMap(_.leaderIds)
|
||||
|
||||
val fullInfo = reconnedView
|
||||
.flatMap(_.fullInfo)
|
||||
.getOrElse(
|
||||
FullProvinceInfo(
|
||||
rulingHeroId = province.rulingHeroId,
|
||||
rulingFactionHeroIds = province.rulingFactionHeroIds
|
||||
.map(hid => gs.heroes(hid))
|
||||
.sortWith(HeroUtils.sortOrderer(factionLeaderIds))
|
||||
.map(_.id),
|
||||
battalions = Vector.empty, // Will be replaced below
|
||||
defendingArmy = province.defendingArmy
|
||||
.map(army => ArmyFilter.filterArmy(army, gs.battalions, Some(factionId))),
|
||||
economy = province.economy.toFloat,
|
||||
agriculture = province.agriculture.toFloat,
|
||||
infrastructure = province.infrastructure.toFloat,
|
||||
economyDevastation = province.economyDevastation.toFloat,
|
||||
agricultureDevastation = province.agricultureDevastation.toFloat,
|
||||
infrastructureDevastation = province.infrastructureDevastation.toFloat,
|
||||
gold = province.gold,
|
||||
food = province.food,
|
||||
priceIndex = province.priceIndex,
|
||||
goldCap = ProvinceUtils.goldCap(province),
|
||||
foodCap = ProvinceUtils.foodCap(province),
|
||||
support = Some(supportScala(province.support)),
|
||||
foodConsumption = ProvinceUtils.monthlyFoodConsumption(province, gs),
|
||||
provinceOrders = ProvinceOrderTypeConverter.toProto(province.provinceOrders),
|
||||
hexMapName = province.hexMapName,
|
||||
castleCount = province.castleCount,
|
||||
heroCap = province.heroCap,
|
||||
unaffiliatedHeroes = Vector.empty,
|
||||
rulerIsTraveling = province.rulerIsTraveling
|
||||
)
|
||||
)
|
||||
.copy(
|
||||
battalions = province.battalionIds
|
||||
.map(bid => gs.battalions(bid))
|
||||
.map(BattalionViewFilter.limitedBattalionView)
|
||||
.toVector
|
||||
)
|
||||
|
||||
ProvinceView(
|
||||
id = province.id,
|
||||
name = province.name,
|
||||
rulingFactionId = province.rulingFactionId,
|
||||
fullInfo = Some(fullInfo),
|
||||
knownEvents = knownEvents.toVector,
|
||||
asOf = reconnedView.flatMap(_.asOf),
|
||||
knownIncomingArmies = incomingAttackers
|
||||
)
|
||||
}
|
||||
|
||||
private def myIncomingArmiesScala(
|
||||
province: ProvinceT,
|
||||
gs: ScalaGameState,
|
||||
factionId: FactionId
|
||||
): Vector[IncomingArmyView] =
|
||||
province.incomingArmies
|
||||
.filter(_.army.factionId == factionId)
|
||||
.map(army => incomingArmyInfoScala(ma = army, gs = gs, showDetails = true))
|
||||
|
||||
private def incomingArmyInfoScala(
|
||||
ma: net.eagle0.eagle.model.state.MovingArmy,
|
||||
gs: ScalaGameState,
|
||||
showDetails: Boolean
|
||||
): IncomingArmyView =
|
||||
IncomingArmyView(
|
||||
factionId = ma.army.factionId,
|
||||
heroCount = ma.army.units.size,
|
||||
troopCount = ma.army.units
|
||||
.flatMap(_.battalionId)
|
||||
.map(gs.battalions)
|
||||
.map(_.size)
|
||||
.sum,
|
||||
originProvinceId = ma.originProvinceId,
|
||||
destinationProvinceId = ma.destinationProvinceId,
|
||||
arrivalRoundId = ma.arrivalRound,
|
||||
food = ma.supplies.food,
|
||||
gold = ma.supplies.gold,
|
||||
unitDetails =
|
||||
if showDetails then
|
||||
ma.army.units.map { cu =>
|
||||
UnitDetails(
|
||||
heroId = cu.heroId,
|
||||
battalion = cu.battalionId.map(gs.battalions).map { b =>
|
||||
BattalionViewFilter.filteredBattalionViewBelongingToPlayer(b)
|
||||
}
|
||||
)
|
||||
}
|
||||
else Vector()
|
||||
)
|
||||
|
||||
private val universallyVisibleCheckers: List[ProvinceEvent => Boolean] = List(
|
||||
ProvinceEventUtils.isBlizzardEvent,
|
||||
ProvinceEventUtils.isFestivalEvent,
|
||||
@@ -36,21 +213,19 @@ object ProvinceViewFilter {
|
||||
name = province.name,
|
||||
rulingFactionId = province.rulingFactionId,
|
||||
fullInfo = Some(fullProvinceInfo(province, gs, None)),
|
||||
knownEvents = province.activeEvents,
|
||||
asOf = Some(gs.currentDate.get)
|
||||
knownEvents = province.activeEvents.toVector,
|
||||
asOf = Some(DateConverter.fromProto(gs.currentDate))
|
||||
)
|
||||
|
||||
private def deadHeroesFilteredOut(
|
||||
fullProvinceInfo: FullProvinceInfo,
|
||||
gameState: GameState
|
||||
): FullProvinceInfo =
|
||||
fullProvinceInfo.update(
|
||||
_.rulingFactionHeroIds.modify(
|
||||
_.filterNot(hid => gameState.killedHeroes.contains(hid))
|
||||
),
|
||||
_.unaffiliatedHeroes.modify(
|
||||
_.filterNot(uh => gameState.killedHeroes.contains(uh.heroId))
|
||||
)
|
||||
fullProvinceInfo.copy(
|
||||
rulingFactionHeroIds =
|
||||
fullProvinceInfo.rulingFactionHeroIds.filterNot(hid => gameState.killedHeroes.contains(hid)),
|
||||
unaffiliatedHeroes =
|
||||
fullProvinceInfo.unaffiliatedHeroes.filterNot(uh => gameState.killedHeroes.contains(uh.heroId))
|
||||
)
|
||||
|
||||
def filteredProvinceView(
|
||||
@@ -65,11 +240,13 @@ object ProvinceViewFilter {
|
||||
) ++ gs.factions.get(factionId)
|
||||
|
||||
// Take the most recent reconned view from this faction or any allies
|
||||
// (stored as proto, convert to Scala)
|
||||
val reconnedView =
|
||||
relevantFactions
|
||||
.flatMap(_.reconnedProvinces)
|
||||
.filter(_.id == province.id)
|
||||
.maxByOption(_.getAsOf)
|
||||
.map(ProvinceViewConverter.fromProto)
|
||||
|
||||
val showAll = province.rulingFactionId.exists { rfid =>
|
||||
Visibility.hasFullVisibility(factionId, rfid, gs)
|
||||
@@ -103,7 +280,7 @@ object ProvinceViewFilter {
|
||||
food = shipment.supplies.map(_.food).getOrElse(0),
|
||||
gold = shipment.supplies.map(_.gold).getOrElse(0)
|
||||
)
|
||||
}
|
||||
}.toVector
|
||||
else Vector()
|
||||
|
||||
val fullInfo = Option
|
||||
@@ -117,9 +294,9 @@ object ProvinceViewFilter {
|
||||
name = province.name,
|
||||
rulingFactionId = province.rulingFactionId,
|
||||
fullInfo = fullInfo,
|
||||
knownEvents = knownEvents,
|
||||
knownEvents = knownEvents.toVector,
|
||||
asOf =
|
||||
if showAll then Some(gs.currentDate.get)
|
||||
if showAll then Some(DateConverter.fromProto(gs.currentDate))
|
||||
else reconnedView.flatMap(_.asOf),
|
||||
knownIncomingArmies = incomingAttackers ++ incomingSupplies
|
||||
)
|
||||
@@ -131,10 +308,11 @@ object ProvinceViewFilter {
|
||||
gs: GameState,
|
||||
factionId: FactionId
|
||||
): ProvinceView = {
|
||||
// Stored as proto, convert to Scala
|
||||
val reconnedView = for {
|
||||
faction <- gs.factions.get(factionId)
|
||||
pv <- faction.reconnedProvinces.find(_.id == province.id)
|
||||
} yield pv
|
||||
} yield ProvinceViewConverter.fromProto(pv)
|
||||
|
||||
val knownEvents = province.activeEvents.filter { pe =>
|
||||
if reconnedView.exists(_.knownEvents.contains(pe)) then true
|
||||
@@ -148,10 +326,13 @@ object ProvinceViewFilter {
|
||||
val fullInfo = reconnedView
|
||||
.flatMap(_.fullInfo)
|
||||
.getOrElse(fullProvinceInfo(province, gs, Some(factionId)))
|
||||
.update(
|
||||
_.battalions := province.battalionIds.map(gs.battalions).map {
|
||||
LegacyBattalionViewFilter.limitedBattalionView
|
||||
}
|
||||
.copy(
|
||||
battalions = province.battalionIds
|
||||
.map(gs.battalions)
|
||||
.map {
|
||||
LegacyBattalionViewFilter.limitedBattalionViewScala
|
||||
}
|
||||
.toVector
|
||||
)
|
||||
|
||||
ProvinceView(
|
||||
@@ -159,7 +340,7 @@ object ProvinceViewFilter {
|
||||
name = province.name,
|
||||
rulingFactionId = province.rulingFactionId,
|
||||
fullInfo = Some(fullInfo),
|
||||
knownEvents = knownEvents,
|
||||
knownEvents = knownEvents.toVector,
|
||||
asOf = reconnedView.flatMap(_.asOf),
|
||||
knownIncomingArmies = incomingAttackers ++ incomingSupplies
|
||||
)
|
||||
@@ -172,13 +353,15 @@ object ProvinceViewFilter {
|
||||
): FullProvinceInfo = FullProvinceInfo(
|
||||
rulingHeroId = province.rulingHeroId,
|
||||
rulingFactionHeroIds = province.rulingFactionHeroIds
|
||||
.sortWith(LegacyHeroUtils.sortOrderer(gs)),
|
||||
.sortWith(LegacyHeroUtils.sortOrderer(gs))
|
||||
.toVector,
|
||||
battalions = province.battalionIds
|
||||
.map(gs.battalions)
|
||||
.map(LegacyBattalionViewFilter.filteredBattalionViewBelongingToPlayer)
|
||||
.sortBy(_.`type`.index),
|
||||
.map(LegacyBattalionViewFilter.filteredBattalionViewBelongingToPlayerScala)
|
||||
.sortBy(_.typeId.value)
|
||||
.toVector,
|
||||
defendingArmy = province.defendingArmy
|
||||
.map(ArmyFilter.filterArmy(_, gs.battalions, factionId)),
|
||||
.map(ArmyFilter.filterArmyScala(_, gs.battalions, factionId)),
|
||||
economy = province.economy.toFloat,
|
||||
agriculture = province.agriculture.toFloat,
|
||||
infrastructure = province.infrastructure.toFloat,
|
||||
@@ -190,13 +373,13 @@ object ProvinceViewFilter {
|
||||
priceIndex = province.priceIndex,
|
||||
goldCap = LegacyProvinceUtils.goldCap(province),
|
||||
foodCap = LegacyProvinceUtils.foodCap(province),
|
||||
support = Some(supportSc(province.support)),
|
||||
support = Some(supportScala(province.support)),
|
||||
foodConsumption = LegacyProvinceUtils.monthlyFoodConsumption(province.id, gs),
|
||||
provinceOrders = province.provinceOrders,
|
||||
hexMapName = province.hexMapName,
|
||||
castleCount = province.castleCount,
|
||||
heroCap = province.heroCap,
|
||||
unaffiliatedHeroes = province.unaffiliatedHeroes.map(unaffiliatedHeroInfo(_, gs)),
|
||||
unaffiliatedHeroes = province.unaffiliatedHeroes.map(unaffiliatedHeroInfo(_, gs)).toVector,
|
||||
rulerIsTraveling = province.rulerIsTraveling
|
||||
)
|
||||
|
||||
@@ -224,6 +407,7 @@ object ProvinceViewFilter {
|
||||
)
|
||||
)
|
||||
.map(army => incomingArmyInfo(ma = army, gs = gs, showDetails = false))
|
||||
.toVector
|
||||
|
||||
private def incomingArmyInfo(
|
||||
ma: MovingArmy,
|
||||
@@ -246,14 +430,14 @@ object ProvinceViewFilter {
|
||||
unitDetails =
|
||||
if showDetails then
|
||||
ma.getArmy.units.map { cu =>
|
||||
IncomingArmyView.UnitDetails(
|
||||
UnitDetails(
|
||||
heroId = cu.heroId,
|
||||
battalion = cu.battalionId.map(gs.battalions).map { b =>
|
||||
LegacyBattalionViewFilter
|
||||
.filteredBattalionViewBelongingToPlayer(b)
|
||||
.filteredBattalionViewBelongingToPlayerScala(b)
|
||||
}
|
||||
)
|
||||
}
|
||||
}.toVector
|
||||
else Vector()
|
||||
)
|
||||
|
||||
@@ -265,10 +449,11 @@ object ProvinceViewFilter {
|
||||
UnaffiliatedHeroBasics(
|
||||
heroId = hero.id,
|
||||
nameTextId = hero.nameTextId,
|
||||
profession = hero.profession,
|
||||
status = uh.`type`,
|
||||
profession = ProfessionConverter.fromProto(hero.profession),
|
||||
status = UnaffiliatedHeroConverter.unaffiliatedHeroTypeFromProto(uh.`type`),
|
||||
isFactionLeader = LegacyFactionUtils.isFactionLeader(hero.id, gs),
|
||||
recruitmentInfo = uh.recruitmentInfo
|
||||
recruitmentInfo =
|
||||
uh.recruitmentInfo.fold(RecruitmentInfo.Unknown)(UnaffiliatedHeroConverter.recruitmentInfoFromProto)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package net.eagle0.eagle.model.action_result
|
||||
|
||||
import net.eagle0.eagle.{BattalionId, FactionId, HeroId, ProvinceId, RoundId}
|
||||
import net.eagle0.eagle.api.selected_command.SelectedCommand
|
||||
import net.eagle0.eagle.model.action_result.changed_province.ChangedProvinceT
|
||||
import net.eagle0.eagle.model.action_result.generated_text_request.GeneratedTextRequestT
|
||||
import net.eagle0.eagle.model.action_result.types.base.ActionResultType
|
||||
@@ -27,7 +28,7 @@ trait ActionResultT {
|
||||
def newRoundId: Option[RoundId]
|
||||
def newDate: Option[Date]
|
||||
|
||||
// def lastCommandTypeForActingProvince: Option[CommandType]
|
||||
def lastCommandTypeForActingProvince: Option[SelectedCommand]
|
||||
|
||||
def newBattle: Option[ShardokBattle]
|
||||
// def resolvedBattle: Option[String]
|
||||
@@ -73,7 +74,7 @@ trait ActionResultT {
|
||||
newRoundPhase: Option[RoundPhase] = newRoundPhase,
|
||||
newRoundId: Option[RoundId] = newRoundId,
|
||||
newDate: Option[Date] = newDate,
|
||||
// lastCommandTypeForActingProvince: Option[CommandType] = None,
|
||||
lastCommandTypeForActingProvince: Option[SelectedCommand] = lastCommandTypeForActingProvince,
|
||||
newBattle: Option[ShardokBattle] = newBattle,
|
||||
// resolvedBattle: Option[String] = None,
|
||||
// resolvedImminentBattle: Option[Int] = None,
|
||||
|
||||
@@ -51,6 +51,7 @@ scala_library(
|
||||
":changed_hero_trait",
|
||||
":client_text_visibility_extension_trait",
|
||||
":notification_trait",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package net.eagle0.eagle.model.action_result.concrete
|
||||
|
||||
import net.eagle0.eagle.{BattalionId, FactionId, HeroId, ProvinceId, RoundId}
|
||||
import net.eagle0.eagle.api.selected_command.SelectedCommand
|
||||
import net.eagle0.eagle.model.action_result.{
|
||||
ActionResultT,
|
||||
ChangedBattalionT,
|
||||
@@ -34,6 +35,7 @@ case class ActionResultC(
|
||||
newRoundPhase: Option[RoundPhase] = None,
|
||||
newRoundId: Option[RoundId] = None,
|
||||
newDate: Option[Date] = None,
|
||||
lastCommandTypeForActingProvince: Option[SelectedCommand] = None,
|
||||
newBattle: Option[ShardokBattle] = None,
|
||||
newChronicleEntry: Option[ChronicleEntry] = None,
|
||||
changedBattalions: Vector[ChangedBattalionT] = Vector.empty,
|
||||
@@ -68,6 +70,7 @@ case class ActionResultC(
|
||||
newRoundPhase: Option[RoundPhase] = newRoundPhase,
|
||||
newRoundId: Option[RoundId] = newRoundId,
|
||||
newDate: Option[Date] = newDate,
|
||||
lastCommandTypeForActingProvince: Option[SelectedCommand] = lastCommandTypeForActingProvince,
|
||||
newBattle: Option[ShardokBattle] = newBattle,
|
||||
newChronicleEntry: Option[ChronicleEntry] = newChronicleEntry,
|
||||
changedBattalions: Vector[ChangedBattalionT] = changedBattalions,
|
||||
@@ -99,6 +102,7 @@ case class ActionResultC(
|
||||
newRoundPhase = newRoundPhase,
|
||||
newRoundId = newRoundId,
|
||||
newDate = newDate,
|
||||
lastCommandTypeForActingProvince = lastCommandTypeForActingProvince,
|
||||
newBattle = newBattle,
|
||||
newChronicleEntry = newChronicleEntry,
|
||||
changedBattalions = changedBattalions,
|
||||
|
||||
@@ -16,6 +16,7 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types/base:action_result_type",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
|
||||
@@ -71,12 +72,12 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_faction_trait",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/views:province_view_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_faction_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/diplomacy_offer",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/faction:faction_relationship",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/faction:prestige_modifier",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/province:province_view",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import net.eagle0.eagle.model.action_result.concrete.ChangedFactionC.{
|
||||
import net.eagle0.eagle.model.action_result.ChangedFactionT
|
||||
import net.eagle0.eagle.model.state.diplomacy_offer.DiplomacyOffer
|
||||
import net.eagle0.eagle.model.state.faction.{FactionRelationship, PrestigeModifier}
|
||||
import net.eagle0.eagle.views.province_view.ProvinceView
|
||||
import net.eagle0.eagle.model.view.province.ProvinceView
|
||||
|
||||
case class TrustLevelUpdate(
|
||||
targetFactionId: FactionId,
|
||||
@@ -46,5 +46,6 @@ case class ChangedFactionC(
|
||||
removedReconnedProvinceIds: Vector[ProvinceId] = Vector.empty,
|
||||
trustLevelUpdates: Vector[TrustLevelUpdate] = Vector.empty,
|
||||
newLastActedProvinceId: Option[ProvinceId] = None,
|
||||
clearLastActedProvinceId: Boolean = false
|
||||
clearLastActedProvinceId: Boolean = false,
|
||||
newName: Option[String] = None
|
||||
) extends ChangedFactionT
|
||||
|
||||
+12
@@ -406,4 +406,16 @@ enum LlmRequestT extends GeneratedTextRequestT:
|
||||
toFactionId: FactionId,
|
||||
provinceId: ProvinceId
|
||||
)
|
||||
|
||||
case NewFactionHeadMessage(
|
||||
requestId: String,
|
||||
eagleGameId: GameId,
|
||||
recipientFactionIds: Vector[FactionId] = Vector.empty,
|
||||
alwaysGenerate: Boolean = false,
|
||||
newHeadHeroId: HeroId,
|
||||
factionId: FactionId,
|
||||
previousFactionName: String,
|
||||
newFactionName: Option[String],
|
||||
previousHeadHeroId: HeroId
|
||||
)
|
||||
end LlmRequestT
|
||||
|
||||
+3
@@ -1,6 +1,7 @@
|
||||
package net.eagle0.eagle.model.proto_converters
|
||||
|
||||
import net.eagle0.eagle.{BattalionId, FactionId, HeroId, ProvinceId, RoundId}
|
||||
import net.eagle0.eagle.api.selected_command.SelectedCommand
|
||||
import net.eagle0.eagle.common.action_result_type.ActionResultType as ActionResultTypeProto
|
||||
import net.eagle0.eagle.common.round_phase.NewRoundPhase as NewRoundPhaseProto
|
||||
import net.eagle0.eagle.internal.action_result.ActionResult as ActionResultProto
|
||||
@@ -53,6 +54,7 @@ object ActionResultProtoConverter {
|
||||
newRoundPhase: Option[RoundPhase],
|
||||
newRoundId: Option[RoundId],
|
||||
newDate: Option[Date],
|
||||
lastCommandTypeForActingProvince: Option[SelectedCommand],
|
||||
newBattle: Option[ShardokBattle],
|
||||
newChronicleEntry: Option[ChronicleEntry],
|
||||
changedBattalions: Vector[ChangedBattalionT],
|
||||
@@ -86,6 +88,7 @@ object ActionResultProtoConverter {
|
||||
newRoundPhase = newRoundPhase.map(rp => NewRoundPhaseProto(RoundPhaseConverter.toProto(rp))),
|
||||
newRoundId = newRoundId,
|
||||
newDate = newDate.map(DateConverter.toProto),
|
||||
lastCommandTypeForActingProvince = lastCommandTypeForActingProvince.getOrElse(SelectedCommand.Empty),
|
||||
newBattle = newBattle.map(ShardokBattleConverter.toProto),
|
||||
newChronicleEntry = newChronicleEntry.map(ChronicleEntryConverter.toProto),
|
||||
changedBattalions = changedBattalions.map {
|
||||
|
||||
@@ -24,6 +24,7 @@ scala_library(
|
||||
":changed_province_converter",
|
||||
":notification_converter",
|
||||
":round_phase_converter",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:action_result_type_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
@@ -105,11 +106,15 @@ scala_library(
|
||||
visibility = [
|
||||
"//src/main/scala/net/eagle0/eagle/library:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/view_filters:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/province:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/view/battalion:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type_id",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:battalion_type_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
@@ -123,6 +128,7 @@ scala_library(
|
||||
srcs = ["BattalionTypeConverter.scala"],
|
||||
visibility = [
|
||||
"//src/main/scala/net/eagle0/eagle/library:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
|
||||
@@ -211,10 +217,13 @@ scala_library(
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:changed_faction_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/views:province_view_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_faction_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/diplomacy_offer",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/faction",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/view/province:province_view",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/province:province_view",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -257,10 +266,14 @@ scala_library(
|
||||
srcs = ["CombatUnitConverter.scala"],
|
||||
visibility = [
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/view_filters:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/view:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/actions/impl/command:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:combat_unit",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:combat_unit_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
@@ -426,11 +439,15 @@ scala_library(
|
||||
visibility = [
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/view_filters:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/province:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/view:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/unaffiliated_hero",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:unaffiliated_hero_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
|
||||
+13
-7
@@ -18,9 +18,11 @@ import net.eagle0.eagle.model.action_result.concrete.ChangedFactionC.{
|
||||
import net.eagle0.eagle.model.action_result.ChangedFactionT
|
||||
import net.eagle0.eagle.model.proto_converters.diplomacy_offer.DiplomacyOfferConverter
|
||||
import net.eagle0.eagle.model.proto_converters.faction.FactionConverter
|
||||
import net.eagle0.eagle.model.proto_converters.view.province.ProvinceViewConverter
|
||||
import net.eagle0.eagle.model.state.diplomacy_offer.DiplomacyOffer
|
||||
import net.eagle0.eagle.model.state.faction.{FactionRelationship, PrestigeModifier}
|
||||
import net.eagle0.eagle.views.province_view.ProvinceView
|
||||
import net.eagle0.eagle.model.view.province.ProvinceView
|
||||
import net.eagle0.eagle.views.province_view.ProvinceView as ProvinceViewProto
|
||||
|
||||
object ChangedFactionConverter {
|
||||
def toProto(changedFaction: ChangedFactionT): ChangedFactionProto =
|
||||
@@ -54,7 +56,8 @@ object ChangedFactionConverter {
|
||||
removedReconnedProvinceIds: Vector[ProvinceId],
|
||||
trustLevelUpdates: Vector[TrustLevelUpdate],
|
||||
newLastActedProvinceId: Option[ProvinceId],
|
||||
clearLastActedProvinceId: Boolean
|
||||
clearLastActedProvinceId: Boolean,
|
||||
newName: Option[String]
|
||||
) =>
|
||||
ChangedFactionProto(
|
||||
id = factionId,
|
||||
@@ -73,7 +76,7 @@ object ChangedFactionConverter {
|
||||
addedOutgoingRansomOfferPartners = newOutgoingRansomOfferFactionIds.map(_.fid),
|
||||
newFocusProvinceId = newFocusProvinceId,
|
||||
clearFocusProvinceId = clearFocusProvinceId,
|
||||
updatedReconnedProvinces = updatedReconnedProvinces,
|
||||
updatedReconnedProvinces = updatedReconnedProvinces.map(ProvinceViewConverter.toProto),
|
||||
removedReconnedProvinceIds = removedReconnedProvinceIds,
|
||||
trustLevelUpdates = trustLevelUpdates.map { tlu =>
|
||||
TrustLevelUpdateProto(
|
||||
@@ -82,7 +85,8 @@ object ChangedFactionConverter {
|
||||
)
|
||||
},
|
||||
newLastActedProvinceId = newLastActedProvinceId,
|
||||
clearLastActedProvinceId = clearLastActedProvinceId
|
||||
clearLastActedProvinceId = clearLastActedProvinceId,
|
||||
newName = newName
|
||||
)
|
||||
}
|
||||
|
||||
@@ -105,11 +109,12 @@ object ChangedFactionConverter {
|
||||
addedOutgoingRansomOfferPartners: Seq[FactionId],
|
||||
newFocusProvinceId: Option[ProvinceId],
|
||||
clearFocusProvinceId: Boolean,
|
||||
updatedReconnedProvinces: Seq[ProvinceView],
|
||||
updatedReconnedProvinces: Seq[ProvinceViewProto],
|
||||
removedReconnedProvinceIds: Seq[ProvinceId],
|
||||
trustLevelUpdates: Seq[TrustLevelUpdateProto],
|
||||
newLastActedProvinceId: Option[ProvinceId],
|
||||
clearLastActedProvinceId: Boolean,
|
||||
newName: Option[String],
|
||||
_ /* unknownFields */
|
||||
) =>
|
||||
ChangedFactionC(
|
||||
@@ -141,7 +146,7 @@ object ChangedFactionConverter {
|
||||
.toVector,
|
||||
newFocusProvinceId = newFocusProvinceId,
|
||||
clearFocusProvinceId = clearFocusProvinceId,
|
||||
updatedReconnedProvinces = updatedReconnedProvinces.toVector,
|
||||
updatedReconnedProvinces = updatedReconnedProvinces.map(ProvinceViewConverter.fromProto).toVector,
|
||||
removedReconnedProvinceIds = removedReconnedProvinceIds.toVector,
|
||||
trustLevelUpdates = trustLevelUpdates.map { tlu =>
|
||||
TrustLevelUpdate(
|
||||
@@ -150,7 +155,8 @@ object ChangedFactionConverter {
|
||||
)
|
||||
}.toVector,
|
||||
newLastActedProvinceId = newLastActedProvinceId,
|
||||
clearLastActedProvinceId = clearLastActedProvinceId
|
||||
clearLastActedProvinceId = clearLastActedProvinceId,
|
||||
newName = newName
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,11 @@ object UnaffiliatedHeroConverter {
|
||||
UNAFFILIATED_HERO_RETURNING_PRISONER -> UnaffiliatedHeroType.ReturningPrisoner
|
||||
)
|
||||
|
||||
def unaffiliatedHeroTypeFromProto(
|
||||
protoType: UnaffiliatedHeroTypeProto
|
||||
): UnaffiliatedHeroType =
|
||||
unaffiliatedHeroTypeMap.getOrElse(protoType, UnaffiliatedHeroType.Unknown)
|
||||
|
||||
def recruitmentInfoFromProto(
|
||||
recruitmentInfoProto: RecruitmentInfoProto
|
||||
): RecruitmentInfo = recruitmentInfoProto.status match {
|
||||
|
||||
@@ -7,6 +7,7 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/quest_fulfillment:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/view_filters:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/model:__subpackages__",
|
||||
"//src/main/scala/net/eagle0/eagle/service:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/service/new_game_creation:__pkg__",
|
||||
|
||||
@@ -15,14 +15,17 @@ scala_library(
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:faction_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/views:province_view_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:proto_conversion_exception",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/date",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/diplomacy_offer",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/view/province:province_view",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/date",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/diplomacy_offer",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/faction",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/faction:faction_relationship",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/faction/concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/province:province_view",
|
||||
],
|
||||
)
|
||||
|
||||
+6
-4
@@ -7,13 +7,15 @@ import net.eagle0.eagle.internal.faction.Faction.OutgoingOfferRound as OutgoingO
|
||||
import net.eagle0.eagle.internal.faction_relationship.FactionRelationship as FactionRelationshipProto
|
||||
import net.eagle0.eagle.model.proto_converters.date.DateConverter
|
||||
import net.eagle0.eagle.model.proto_converters.diplomacy_offer.DiplomacyOfferConverter
|
||||
import net.eagle0.eagle.model.proto_converters.view.province.ProvinceViewConverter
|
||||
import net.eagle0.eagle.model.proto_converters.ProtoConversionException
|
||||
import net.eagle0.eagle.model.state.date.Date
|
||||
import net.eagle0.eagle.model.state.diplomacy_offer.DiplomacyOffer
|
||||
import net.eagle0.eagle.model.state.faction.{FactionRelationship, FactionT, PrestigeModifier}
|
||||
import net.eagle0.eagle.model.state.faction.concrete.FactionC
|
||||
import net.eagle0.eagle.model.state.faction.FactionT.OutgoingOfferRound
|
||||
import net.eagle0.eagle.views.province_view.ProvinceView
|
||||
import net.eagle0.eagle.model.view.province.ProvinceView
|
||||
import net.eagle0.eagle.views.province_view.ProvinceView as ProvinceViewProto
|
||||
|
||||
object FactionConverter {
|
||||
def toProto(faction: FactionT): FactionProto = faction match {
|
||||
@@ -43,7 +45,7 @@ object FactionConverter {
|
||||
factionRelationships = factionRelationships.map(toProto),
|
||||
incomingDiplomacyOffers = incomingDiplomacyOffers.map(DiplomacyOfferConverter.toProto),
|
||||
focusProvinceId = focusProvinceId,
|
||||
reconnedProvinces = reconnedProvinces,
|
||||
reconnedProvinces = reconnedProvinces.map(ProvinceViewConverter.toProto),
|
||||
lastActedProvinceIdThisRound = lastActedProvinceIdThisRound,
|
||||
lastOutgoingTruceOfferRounds = lastOutgoingTruceOfferRounds.map(toProto),
|
||||
lastOutgoingAllianceOfferRounds = lastOutgoingAllianceOfferRounds.map(toProto),
|
||||
@@ -117,7 +119,7 @@ object FactionConverter {
|
||||
factionRelationships: Seq[FactionRelationshipProto],
|
||||
incomingDiplomacyOffers: Seq[DiplomacyOfferProto],
|
||||
focusProvinceId: Option[ProvinceId],
|
||||
reconnedProvinces: Seq[ProvinceView],
|
||||
reconnedProvinces: Seq[ProvinceViewProto],
|
||||
lastActedProvinceIdThisRound: ProvinceId,
|
||||
lastOutgoingTruceOfferRounds: Seq[OutgoingOfferRoundProto],
|
||||
lastOutgoingAllianceOfferRounds: Seq[OutgoingOfferRoundProto],
|
||||
@@ -137,7 +139,7 @@ object FactionConverter {
|
||||
.map(DiplomacyOfferConverter.fromProto)
|
||||
.toVector,
|
||||
focusProvinceId = focusProvinceId,
|
||||
reconnedProvinces = reconnedProvinces.toVector,
|
||||
reconnedProvinces = reconnedProvinces.map(ProvinceViewConverter.fromProto).toVector,
|
||||
lastActedProvinceIdThisRound = lastActedProvinceIdThisRound,
|
||||
lastOutgoingTruceOfferRounds = lastOutgoingTruceOfferRounds.map(fromProto).toVector,
|
||||
lastOutgoingAllianceOfferRounds = lastOutgoingAllianceOfferRounds.map(fromProto).toVector,
|
||||
|
||||
@@ -9,6 +9,7 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/applier:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:__subpackages__",
|
||||
"//src/main/scala/net/eagle0/eagle/service:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/library:__pkg__",
|
||||
|
||||
@@ -85,6 +85,7 @@ scala_library(
|
||||
visibility = [
|
||||
"//src/main/scala/net/eagle0/eagle/library:__subpackages__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/view_filters:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:__subpackages__",
|
||||
"//src/main/scala/net/eagle0/eagle/service:__pkg__",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user