Compare commits

..
Author SHA1 Message Date
adminandClaude Opus 4.5 5331bdedb1 Add hold-shift-to-pause for autoscroll
Hold either Shift key to pause auto-scrolling, letting the user
read at their own pace. Releasing Shift resumes from current position.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-20 17:00:32 -08:00
adminandClaude Opus 4.5 364b0978d8 Re-enable settings panel in Gameplay scene
Accidentally disabled in previous layout adjustments.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-20 15:29:04 -08:00
113 changed files with 1184 additions and 5395 deletions
-3
View File
@@ -6,7 +6,4 @@
*.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
-66
View File
@@ -1,66 +0,0 @@
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 ")"
-82
View File
@@ -1,82 +0,0 @@
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
# Also set for current directory in case Bazel uses different home
mkdir -p .docker
cp ~/.docker/config.json .docker/
- name: Push Eagle image to DO registry
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
env:
DOCKER_CONFIG: ${{ github.workspace }}/.docker
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
# Also set for current directory in case Bazel uses different home
mkdir -p .docker
cp ~/.docker/config.json .docker/
- name: Push Shardok image to DO registry
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
env:
DOCKER_CONFIG: ${{ github.workspace }}/.docker
run: bazel run --platforms=//:linux_x86_64 //ci:shardok_server_push
-9
View File
@@ -3,15 +3,6 @@ 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
+17 -60
View File
@@ -26,68 +26,50 @@ 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.6.0")
bazel_dep(name = "toolchains_llvm", version = "1.4.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",
)
# 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"],
)
use_repo(llvm, "llvm_toolchain")
#
# Language Support - Go
#
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")
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")
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",
@@ -102,15 +84,15 @@ use_repo(
# Platform Support - Apple/iOS
#
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")
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")
#
# Protocol Buffers & RPC
#
bazel_dep(name = "protobuf", version = "29.2", repo_name = "com_google_protobuf")
bazel_dep(name = "protobuf", repo_name = "com_google_protobuf", version = "29.2")
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")
@@ -121,32 +103,6 @@ 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
#
@@ -154,6 +110,7 @@ use_repo(oci, "eclipse_temurin_17", "eclipse_temurin_17_linux_amd64", "ubuntu_24
bazel_dep(name = "rules_jvm_external", version = "6.3")
maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")
maven.install(
artifacts = [
# Netty
@@ -216,6 +173,7 @@ maven.install(
"https://repo1.maven.org/maven2",
],
)
use_repo(maven, "maven", "unpinned_maven")
#
@@ -263,6 +221,5 @@ 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,
)
+88 -275
View File
@@ -26,11 +26,7 @@
"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.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.11.0/source.json": "92494d5aa43b96665397dd13ee16023097470fa85e276b93674d62a244de47ee",
"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",
@@ -55,11 +51,8 @@
"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.28.0/MODULE.bazel": "4b4200e6cbf8fa335b2c3f43e1d6ef3e240319c33d43d60cc0fbd4b87ece299d",
"https://bcr.bazel.build/modules/bazel_features/1.27.0/source.json": "ed8cf0ef05c858dce3661689d0a2b110ff398e63994e178e4f1f7555a8067fed",
"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",
@@ -76,8 +69,7 @@
"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.2/MODULE.bazel": "69ad6927098316848b34a9142bcc975e018ba27f08c4ff403f50c1b6e646ca67",
"https://bcr.bazel.build/modules/bazel_skylib/1.8.2/source.json": "34a3c8bcf233b835eb74be9d628899bb32999d3e0eadef1947a0a562a2b16ffb",
"https://bcr.bazel.build/modules/bazel_skylib/1.8.1/source.json": "7ebaefba0b03efe59cac88ed5bbc67bcf59a3eff33af937345ede2a38b2d368a",
"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",
@@ -106,8 +98,6 @@
"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",
@@ -147,10 +137,6 @@
"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",
@@ -175,7 +161,6 @@
"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",
@@ -240,14 +225,12 @@
"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",
@@ -306,8 +289,6 @@
"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",
@@ -340,8 +321,7 @@
"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.4.1/MODULE.bazel": "00e501db01bbf4e3e1dd1595959092c2fadf2087b2852d3f553b5370f5633592",
"https://bcr.bazel.build/modules/rules_shell/0.4.1/source.json": "4757bd277fe1567763991c4425b483477bb82e35e777a56fd846eb5cceda324a",
"https://bcr.bazel.build/modules/rules_shell/0.3.0/source.json": "c55ed591aa5009401ddf80ded9762ac32c358d2517ee7820be981e2de9756cf3",
"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",
@@ -359,19 +339,14 @@
"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/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/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/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",
@@ -413,7 +388,7 @@
},
"@@aspect_rules_esbuild~//esbuild:extensions.bzl%esbuild": {
"general": {
"bzlTransitiveDigest": "8jv3p0xDR/oitFeH8y0+Y5xlyrUbfsTRlc9TSwYkwl8=",
"bzlTransitiveDigest": "8iOqbPY5ve3DvjzaI1mJZ8XTiJypN2PeWvcKOvmZLy8=",
"usagesDigest": "iDVoyPxUeADmfK8ssoyG3Ehq1bj6p7A43LpEiE266os=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
@@ -1290,247 +1265,6 @@
"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=",
@@ -1559,7 +1293,7 @@
},
"@@rules_scala~//scala/extensions:deps.bzl%scala_deps": {
"general": {
"bzlTransitiveDigest": "5SDZrXQHW6tI/VEw+La2OPOK4ZWm0LGTxnChXOXBCag=",
"bzlTransitiveDigest": "F2PMm61fmZ/IE+VSw1rigJ71hBDD7k3vqyYR1/GgXeA=",
"usagesDigest": "kwo8oolISmSSITnit4b4S0vBiUtHlHK0WLDUwScxmOg=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
@@ -5170,6 +4904,85 @@
]
]
}
},
"@@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
View File
@@ -1,128 +0,0 @@
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",
)
-47
View File
@@ -1,47 +0,0 @@
# 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
+31 -56
View File
@@ -197,54 +197,34 @@ Change APIs to vend Scala `GameState` and `ActionResultT` instead of proto versi
### Remaining Proto Usage in Actions
**Progress: 46 of 52 action files (88%) are fully protoless.**
The following actions still have proto usage, blocked by utility dependencies:
The following 6 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 |
| `NewRoundAction` | 1 | `ChronicleEventGenerator` returns proto | Medium |
**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`
| 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 |
### Estimated Effort (Remaining)
| Component | Lines | Complexity | Blocks |
|-----------|-------|------------|--------|
| `CommandChoiceHelpers` to Scala | ~2000 | High | 4 vassal actions |
| `ResolveBattleAction` refactor | ~500 | High | 1 action (complex) |
| `ChronicleEventGenerator` to Scala | ~400 | Medium | 1 action |
| History API updates | ~100 | Low | - |
| **Total Remaining** | **~3000** | | |
### Progress Summary
| Metric | Value |
|--------|-------|
| Action files fully protoless | 46 / 52 (88%) |
| Proto usages in remaining actions | 33 total |
| Biggest blocker | `ResolveBattleAction` (24 usages) |
| Second biggest blocker | `CommandChoiceHelpers` (blocks 4 actions) |
| 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** | |
### Validation
- [x] `ActionResultApplier` created and tested
- [x] `RandomStateSequencer` threads Scala GameState throughout
- [x] `RoundPhaseAdvancer` uses T-types internally
- [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
- [ ] `ProvinceViewFilter` uses Scala types
- [ ] `CommandChoiceHelpers` uses Scala types
- [ ] History APIs vend Scala types
- [ ] No `ActionResultProtoConverter.toProto()` calls except at persistence/gRPC boundaries
@@ -268,27 +248,22 @@ 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 (Partially Complete)
### View Filters (Blocking Full Deproto)
The view filter utilities now have Scala overloads for server-side use:
The `ProvinceViewFilter` utility currently works entirely with proto types, blocking full deproto of actions that generate province views:
| 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 |
| 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` |
**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
**Blocked Actions**:
- `EndBattleAftermathPhaseAction` - uses `ProvinceViewFilter` for `revelationChange`, requires lazy proto conversion
- `PerformReconResolutionAction` - uses `ProvinceViewFilter` for reconned provinces
- `GameStateFactionExtensions` - uses `ProvinceViewFilter` for `updatedReconnedProvinces`
**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
**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.
---
-940
View File
@@ -1,940 +0,0 @@
# 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)
-473
View File
@@ -1,473 +0,0 @@
#!/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,6 +65,11 @@ 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,
@@ -81,12 +86,6 @@ 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,67 +111,17 @@ void ShardokGameController::DoAIThread() {
if (aiClients.empty()) { printf("No AI players, exiting AI thread.\n"); }
while (aiThreadKeepGoing) {
// 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;
while (incomingRegistrations > 0) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
// 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);
unique_lock lk(masterLock);
if (LockedCheckOneAICommand()) {
LockedNotifyClients();
aiThreadKeepGoing = !engine->GameIsOver();
aiThreadKeepGoing = aiThreadKeepGoing && !engine->GameIsOver();
} else {
aiCondition.wait(lk);
aiThreadKeepGoing = aiThreadKeepGoing && !engine->GameIsOver();
}
}
printf("Exiting AI thread.\n");
@@ -186,6 +136,24 @@ 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,
@@ -261,7 +229,6 @@ void ShardokGameController::PostPlacementCommands(
}
auto ShardokGameController::GetCurrentGameStateBytes() -> byte_vector {
scoped_lock<mutex> guard(masterLock);
return engine->GetCurrentGameStateBytes();
}
@@ -325,9 +292,6 @@ auto ShardokGameController::GetUpdates(const int64_t startingActionId) -> AllUpd
-1,
engine->FilterNewResults(-1, startingActionId),
nullptr);
auto gameStateBytes = engine->GetCurrentGameStateBytes();
updates.currentGameState.swap(gameStateBytes);
}
return updates;
@@ -357,74 +321,4 @@ 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,7 +10,6 @@
#define ShardokGameController_hpp
#include <functional>
#include <memory>
#include <mutex>
#include <utility>
#include <vector>
@@ -30,50 +29,6 @@ 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.
@@ -84,17 +39,10 @@ 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;
@@ -112,9 +60,11 @@ 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();
@@ -125,7 +75,6 @@ public:
string serializedRequest = "")
: serializedRequest(std::move(serializedRequest)),
engine(std::move(e)),
cachedGameId(engine->GetGameId()),
mapName(std::move(mapName)),
logFilePath(MakeLogFilePath()),
aiClients(MakeAIClients(engine)),
@@ -149,13 +98,26 @@ public:
PlayerId shardokPlayerId,
int eagleFactionId,
int64_t token,
const vector<UnitPlacementInfo>& infos);
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) {}
};
struct AllUpdates {
vector<ActionResult> mainResults;
vector<OnePlayerUpdates> filteredResults;
int32_t newUnfilteredCount;
byte_vector currentGameState;
};
auto GetUpdates(int64_t startingActionId) -> AllUpdates;
auto GetCurrentGameStateBytes() -> byte_vector;
@@ -165,23 +127,13 @@ public:
auto ResolvedPlayerInfos() -> vector<net::eagle0::shardok::common::PlayerInfo>;
auto EndGameUnits() -> vector<net::eagle0::shardok::storage::ResolvedUnit>;
[[nodiscard]] auto GetGameId() const -> GameId { return cachedGameId; }
[[nodiscard]] auto GetGameId() const -> GameId { return engine->GetGameId(); }
[[nodiscard]] auto GetHexMap() const -> const HexMap * {
return engine->GetCurrentGameState()->hex_map();
}
[[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,130 +529,6 @@ 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 &currentGameState) 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,7 +33,6 @@ 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;
@@ -79,11 +78,6 @@ 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
@@ -120,19 +120,6 @@ 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>
@@ -151,7 +138,7 @@ namespace eagle {
var timeUntilTest = OpenTimeoutSeconds -
(DateTime.UtcNow - _openedAt.Value).TotalSeconds;
if (timeUntilTest > 0) {
return $"Server unavailable. Retrying in {Math.Min(10, (int)timeUntilTest)}s";
return $"Server unavailable. Retrying in {(int)timeUntilTest}s";
}
}
return "Server unavailable. Testing...";
@@ -2,7 +2,6 @@ using System;
using Net.Eagle0.Eagle.Api;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace eagle {
/// <summary>
@@ -27,9 +26,6 @@ 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;
@@ -43,10 +39,9 @@ namespace eagle {
return;
}
if (retryButton != null) {
retryButton.onClick.AddListener(OnRetryClicked);
retryButton.gameObject.SetActive(false);
}
// 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
}
/// <summary>
@@ -78,12 +73,11 @@ 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 = Math.Min(10, (int)Math.Ceiling(timeUntilTest.TotalSeconds));
int seconds = (int)Math.Ceiling(timeUntilTest.TotalSeconds);
_textComponent.text =
$"<color=red>●</color> Server down. Retry in {seconds}s";
return;
@@ -92,7 +86,6 @@ 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;
}
@@ -101,11 +94,6 @@ 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...",
@@ -154,16 +142,8 @@ namespace eagle {
return "<color=yellow>●</color> Reconnecting...";
}
int secondsRemaining = Math.Min(10, (int)Math.Ceiling(timeUntilRetry.TotalSeconds));
int secondsRemaining = (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,11 +229,12 @@ namespace eagle {
void OnApplicationPause(bool pause) {
if (ModelUpdater == null) { return; }
// 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 (pause) {
ModelUpdater.StopListeningForUpdates();
} else {
// Fire-and-forget - subscription is awaited internally and failures are logged
_ = ModelUpdater.StartListeningForUpdates();
}
}
#if UNITY_EDITOR
@@ -43,7 +43,7 @@ namespace eagle {
TokenId? CommandToken { get; }
TokenId LastPostedToken { get; }
ConcurrentDictionary<String, ShardokGameModel> ShardokGameModels { get; }
Dictionary<String, ShardokGameModel> ShardokGameModels { get; }
private bool ShardokGameModelIsRunning(ShardokGameModel sgm) =>
sgm.GameStatus.State == GameStatus.Types.State.GameRunning
@@ -105,9 +105,10 @@ namespace eagle {
_currentModel.ShardokGameModels
.Select(sgm => {
var needsResync = _shardokNeedsResync.GetValueOrDefault(sgm.Key, false);
// 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);
// Use thread-safe count from gRPC thread updates, fall back to model
var count = _shardokResultCounts.GetValueOrDefault(
sgm.Key,
sgm.Value.History.Count());
return new IClientConnectionSubscriber.ShardokViewStatus {
shardokGameId = sgm.Key,
filteredResultCount = needsResync ? 0 : count,
@@ -198,8 +199,7 @@ namespace eagle {
return new List<AvailableCommand>();
}
// Thread-safe: accessed from heartbeat timer thread via ShardokViewStatuses
public ConcurrentDictionary<String, ShardokGameModel> ShardokGameModels { get; set; }
public Dictionary<String, ShardokGameModel> ShardokGameModels { get; set; }
public FactionView MaybeDestroyedFaction(FactionId factionId) {
if (ActiveFactions.TryGetValue(factionId, out var factionView)) {
@@ -241,8 +241,7 @@ namespace eagle {
new Dictionary<ProvinceId, OneProvinceAvailableCommands>();
_currentModel.GsView = new GameStateView();
_currentModel.ShardokGameModels =
new ConcurrentDictionary<ShardokGameId, ShardokGameModel>();
_currentModel.ShardokGameModels = new Dictionary<ShardokGameId, ShardokGameModel>();
_currentModel.BattalionTypes = new Dictionary<BattalionTypeId, BattalionType>();
@@ -313,10 +312,8 @@ namespace eagle {
ServerStatus = updateItem.ActionResultResponse.ServerGameStatus;
}
// 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.
_lastUnfilteredResultCount =
updateItem.ActionResultResponse.UnfilteredResultCountAfter;
if (updateItem.ActionResultResponse.ActionResultViews.Any() ||
updateItem.ActionResultResponse.AvailableCommands == null ||
updateItem.ActionResultResponse.AvailableCommands.Token !=
@@ -342,9 +339,7 @@ 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.TryRemove(
oneResponse.ShardokGameId,
out _);
_currentModel.ShardokGameModels.Remove(oneResponse.ShardokGameId);
_shardokResultCounts.TryRemove(oneResponse.ShardokGameId, out _);
continue;
}
@@ -373,9 +368,7 @@ namespace eagle {
shardokGameModel;
} else {
// Game ended - remove from active models so UI knows battle is over
_currentModel.ShardokGameModels.TryRemove(
oneResponse.ShardokGameId,
out _);
_currentModel.ShardokGameModels.Remove(oneResponse.ShardokGameId);
_shardokResultCounts.TryRemove(oneResponse.ShardokGameId, out _);
}
}
@@ -485,31 +478,6 @@ 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) {
@@ -812,7 +780,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.TryRemove(rb, out _);
_currentModel.ShardokGameModels.Remove(rb);
}
_shardokResultCounts.TryRemove(rb, out _);
@@ -132,10 +132,7 @@ 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();
@@ -1,106 +1,30 @@
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 Random Random = new();
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 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 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 string GetProfessionName(Profession profession) {
return ProfessionNames.TryGetValue(profession, out var name) ? name : "Unknown";
@@ -116,19 +40,8 @@ namespace eagle.Notifications.ARNNotifications {
}
private static string GetProfessionTitle(Profession profession) {
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;
return ProfessionTitles.TryGetValue(profession, out var title) ? title
: "Profession Gained";
}
private static IEnumerable<Notification> GenerateNotifications(
@@ -136,38 +49,14 @@ 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);
string textTemplate;
List<ProvinceId> affectedProvinces;
var affectedProvinces = currentModel.ProvincesForFaction(details.FactionId);
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);
}
string textTemplate =
$"{{Hero}} of {factionName} became {article} {professionName}.\n\n";
var heroPlaceholders = new Dictionary<string, (string nameTextId, string fallback)> {
{ "Hero", (hero.NameTextId, "A hero") }
@@ -31,13 +31,7 @@ namespace eagle.Notifications.ARNNotifications {
};
if (playerId.HasValue && playerId.Value == paidToFactionId) {
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 });
// no notification
} else if (playerId.HasValue && playerId.Value == paidByFactionId) {
yield return DynamicTextNotification.StreamingDynamicNotification(
title: "Ransom Accepted",
@@ -9,7 +9,6 @@ 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,
@@ -73,9 +72,6 @@ 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,
@@ -99,15 +95,6 @@ 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);
}
@@ -69,17 +69,6 @@ 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);
}
@@ -1071,69 +1060,51 @@ 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
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) {
if (response.GameSyncResults.Count > 0) {
foreach (var syncResult in response.GameSyncResults) {
if (!syncResult.EagleInSync) {
_remoteEagleClientLogger.LogLine(
$"[SYNC_MISMATCH] Game {syncResult.GameId}, Shardok {shardokResult.ShardokGameId}: " +
$"out of sync, server has {shardokResult.ServerFilteredResultCount} results");
$"[SYNC_MISMATCH] Game {syncResult.GameId}: Eagle out of sync, " +
$"server has {syncResult.ServerUnfilteredResultCount} results");
LogConnectionEvent(
"sync_mismatch_shardok",
$"game={syncResult.GameId}, shardok={shardokResult.ShardokGameId}, " +
$"server_count={shardokResult.ServerFilteredResultCount}");
hasMismatch = true;
"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}");
}
}
}
}
if (!hasMismatch) 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");
// 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;
// Schedule reconnect to resync
lock (this) {
_streamingCall?.Dispose();
_streamingCall = null;
_threadCancellationTokenSource?.Cancel();
}
MarkAllShardokGamesForResync();
CancelAllPendingSubscriptionAcks();
ScheduleReconnect("SyncMismatch");
}
// 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");
}
}
}
@@ -8239,10 +8239,10 @@ RectTransform:
- {fileID: 1396438488}
m_Father: {fileID: 2130578398}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 333.5, y: -11.255}
m_SizeDelta: {x: 200, y: 20}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &75920627
MonoBehaviour:
@@ -10460,10 +10460,10 @@ RectTransform:
m_Children: []
m_Father: {fileID: 2130578398}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 218.5, y: -11.255}
m_SizeDelta: {x: 20, y: 20}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &103511974
MonoBehaviour:
@@ -11831,10 +11831,10 @@ RectTransform:
m_Children: []
m_Father: {fileID: 683322975}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 306.5, y: -40}
m_SizeDelta: {x: 51, y: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &114306270
MonoBehaviour:
@@ -19609,10 +19609,10 @@ RectTransform:
m_Children: []
m_Father: {fileID: 1566150938}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 256, y: -32.504997}
m_SizeDelta: {x: 492, y: 45.01}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &171635196
MonoBehaviour:
@@ -28025,7 +28025,7 @@ RectTransform:
m_Father: {fileID: 208702977}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 10, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
@@ -30448,7 +30448,7 @@ RectTransform:
m_Father: {fileID: 1396438488}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 20, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
@@ -33904,7 +33904,6 @@ 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
@@ -35394,10 +35393,10 @@ RectTransform:
- {fileID: 1666727525}
m_Father: {fileID: 2039520752}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 321, y: -11.255}
m_SizeDelta: {x: 200, y: 20}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &329919723
MonoBehaviour:
@@ -45318,8 +45317,8 @@ RectTransform:
m_Children: []
m_Father: {fileID: 1666727525}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.1, y: 0}
m_AnchorMax: {x: 0.1, y: 1}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 20, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
@@ -50852,148 +50851,6 @@ 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
@@ -56414,9 +56271,9 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_IgnoreLayout: 0
m_MinWidth: 250
m_MinWidth: 176
m_MinHeight: -1
m_PreferredWidth: 250
m_PreferredWidth: 176
m_PreferredHeight: -1
m_FlexibleWidth: -1
m_FlexibleHeight: 1
@@ -68740,10 +68597,10 @@ RectTransform:
m_Children: []
m_Father: {fileID: 1566150938}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 10, y: -135.03}
m_SizeDelta: {x: 0, y: 10}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &590819392
MonoBehaviour:
@@ -80878,10 +80735,10 @@ RectTransform:
- {fileID: 1119022577}
m_Father: {fileID: 1566150938}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 256, y: -354}
m_SizeDelta: {x: 492, y: 40}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &683322976
MonoBehaviour:
@@ -90621,10 +90478,10 @@ RectTransform:
- {fileID: 392813076}
m_Father: {fileID: 1566150938}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 256, y: -245.03}
m_SizeDelta: {x: 492, y: 30}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &749366807
MonoBehaviour:
@@ -92658,10 +92515,10 @@ RectTransform:
- {fileID: 699949417}
m_Father: {fileID: 1566150938}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 256, y: -205.03}
m_SizeDelta: {x: 492, y: 30}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &770361456
MonoBehaviour:
@@ -93468,10 +93325,10 @@ RectTransform:
m_Children: []
m_Father: {fileID: 2039520752}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 208.5, y: -11.255}
m_SizeDelta: {x: 15, y: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &779503534
MonoBehaviour:
@@ -96471,10 +96328,10 @@ RectTransform:
m_Children: []
m_Father: {fileID: 2039520752}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 446, y: -11.255}
m_SizeDelta: {x: 40, y: 15.01}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &806086202
MonoBehaviour:
@@ -96875,10 +96732,10 @@ RectTransform:
m_Children: []
m_Father: {fileID: 1566150938}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 10, y: -313.27002}
m_SizeDelta: {x: 0, y: 21.459991}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &808870206
MonoBehaviour:
@@ -101960,10 +101817,10 @@ RectTransform:
- {fileID: 113544276}
m_Father: {fileID: 1590704177}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 333.5, y: -11.255}
m_SizeDelta: {x: 200, y: 20}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &858025936
MonoBehaviour:
@@ -117461,10 +117318,10 @@ RectTransform:
m_Children: []
m_Father: {fileID: 1590704177}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 196, y: -11.255}
m_SizeDelta: {x: 15, y: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &972401127
MonoBehaviour:
@@ -118582,7 +118439,7 @@ RectTransform:
m_Father: {fileID: 113544276}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 20, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
@@ -119596,7 +119453,7 @@ RectTransform:
m_Father: {fileID: 1425392451}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0.1, y: 1}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 10, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
@@ -121702,7 +121559,6 @@ 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}
@@ -135956,10 +135812,10 @@ RectTransform:
- {fileID: 717768291}
m_Father: {fileID: 683322975}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 417, y: -20}
m_SizeDelta: {x: 150, y: 40}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1119022578
MonoBehaviour:
@@ -142407,10 +142263,10 @@ RectTransform:
m_Children: []
m_Father: {fileID: 683322975}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 135.5, y: -40}
m_SizeDelta: {x: 51, y: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1184808158
MonoBehaviour:
@@ -147690,10 +147546,10 @@ RectTransform:
m_Children: []
m_Father: {fileID: 2130578398}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 108.5, y: -11.255}
m_SizeDelta: {x: 150, y: 22.51}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1219993393
MonoBehaviour:
@@ -156803,10 +156659,10 @@ RectTransform:
- {fileID: 204805813}
m_Father: {fileID: 2109805906}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 86.75, y: -15}
m_SizeDelta: {x: 173.5, y: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1294722532
MonoBehaviour:
@@ -157342,10 +157198,10 @@ RectTransform:
- {fileID: 379702011}
m_Father: {fileID: 2109805906}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 345.25, y: -15}
m_SizeDelta: {x: 293.5, y: 30}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1300785859
MonoBehaviour:
@@ -167090,10 +166946,10 @@ RectTransform:
m_Children: []
m_Father: {fileID: 2130578398}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 196, y: -11.255}
m_SizeDelta: {x: 15, y: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1390131026
MonoBehaviour:
@@ -170159,144 +170015,6 @@ 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
@@ -170907,10 +170625,10 @@ RectTransform:
m_Children: []
m_Father: {fileID: 2039520752}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 111, y: -11.255}
m_SizeDelta: {x: 170, y: 22.51}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1415802613
MonoBehaviour:
@@ -171066,10 +170784,10 @@ RectTransform:
m_Children: []
m_Father: {fileID: 2130578398}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 448.5, y: -11.255}
m_SizeDelta: {x: 20, y: 20}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1415887397
MonoBehaviour:
@@ -174475,10 +174193,10 @@ RectTransform:
m_Children: []
m_Father: {fileID: 1590704177}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 108.5, y: -11.255}
m_SizeDelta: {x: 150, y: 22.51}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1448655991
MonoBehaviour:
@@ -174814,7 +174532,7 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_text: Server down. Reconnecting in 10s
m_text: 3440
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: ec2736df0379a45bfa7349b652fd07d6, type: 2}
m_sharedMaterial: {fileID: 1026247389042360723, guid: ec2736df0379a45bfa7349b652fd07d6,
@@ -174842,8 +174560,8 @@ MonoBehaviour:
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 12
m_fontSizeBase: 12
m_fontSize: 18
m_fontSizeBase: 18
m_fontWeight: 400
m_enableAutoSizing: 0
m_fontSizeMin: 18
@@ -174927,7 +174645,6 @@ 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
@@ -189022,10 +188739,10 @@ RectTransform:
- {fileID: 1122913432}
m_Father: {fileID: 683322975}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 50, y: -20}
m_SizeDelta: {x: 100, y: 40}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1557328375
MonoBehaviour:
@@ -190772,7 +190489,7 @@ GameObject:
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 2147483647
m_IsActive: 0
m_IsActive: 1
--- !u!224 &1566150938
RectTransform:
m_ObjectHideFlags: 0
@@ -192261,10 +191978,10 @@ RectTransform:
- {fileID: 2123746368}
m_Father: {fileID: 1566150938}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 256, y: -108.774994}
m_SizeDelta: {x: 492, y: 22.51}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1590704178
MonoBehaviour:
@@ -210361,10 +210078,10 @@ RectTransform:
- {fileID: 543540660}
m_Father: {fileID: 683322975}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 221, y: -20}
m_SizeDelta: {x: 100, y: 40}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1775111780
MonoBehaviour:
@@ -212616,7 +212333,7 @@ RectTransform:
m_Father: {fileID: 1110049512}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 10, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
@@ -233034,10 +232751,10 @@ RectTransform:
m_Children: []
m_Father: {fileID: 2109805906}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 186, y: -15}
m_SizeDelta: {x: 15, y: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1989333571
MonoBehaviour:
@@ -237254,10 +236971,10 @@ RectTransform:
- {fileID: 806086201}
m_Father: {fileID: 1566150938}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 256, y: -281.285}
m_SizeDelta: {x: 492, y: 22.51}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &2039520753
MonoBehaviour:
@@ -241615,10 +241332,10 @@ RectTransform:
m_Children: []
m_Father: {fileID: 1590704177}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 218.5, y: -11.255}
m_SizeDelta: {x: 20, y: 20}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &2096030393
MonoBehaviour:
@@ -244714,10 +244431,10 @@ RectTransform:
- {fileID: 1300785858}
m_Father: {fileID: 1566150938}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 256, y: -165.03}
m_SizeDelta: {x: 492, y: 30}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &2109805907
MonoBehaviour:
@@ -246144,10 +245861,10 @@ RectTransform:
m_Children: []
m_Father: {fileID: 1590704177}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 448.5, y: -11.255}
m_SizeDelta: {x: 20, y: 20}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &2123746369
MonoBehaviour:
@@ -247265,10 +246982,10 @@ RectTransform:
- {fileID: 1415887396}
m_Father: {fileID: 1566150938}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 256, y: -76.26499}
m_SizeDelta: {x: 492, y: 22.51}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &2130578399
MonoBehaviour:
@@ -385,99 +385,98 @@ 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() {
if (Model == null) { return; }
SetHeroLabels();
SetModifiers();
UpdateReserves();
MainQueue.Q.Enqueue(() => {
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;
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);
} else {
endTurnButton.GetComponentInChildren<TMP_Text>().text = "End Turn";
}
if (Model.HasAvailableCommandWhere(
command => commandTypeUIManager.CommandGroupForType(command.Type) ==
CommandTypeUIManager.EndTurnCommandGroup)) {
endTurnButton.interactable = true;
}
SetDisplayedCommandGroup(0);
SelectAppropriateDefaultCommand();
} else {
endTurnButton.GetComponentInChildren<TMP_Text>().text = "End Turn";
turnStatusLabel.text = $"{Model.CurrentPlayerName}'s Turn";
}
if (Model.HasAvailableCommandWhere(
command => commandTypeUIManager.CommandGroupForType(command.Type) ==
CommandTypeUIManager.EndTurnCommandGroup)) {
endTurnButton.interactable = true;
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);
}
}
SelectAppropriateDefaultCommand();
} else {
turnStatusLabel.text = $"{Model.CurrentPlayerName}'s Turn";
}
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));
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);
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();
}
}
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();
SetupArmiesTable();
});
}
private void SetupArmiesTable() {
@@ -708,33 +707,34 @@ namespace Shardok {
}
void SetModifiers() {
// Called from ModelUpdated which runs on MainQueue - no need to re-enqueue
hexGrid.ClearCellModifierImages();
MainQueue.Q.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; } = new();
public List<CommandDescriptor> AvailableCommands { get; private set; }
public Action UpdateAction { get; set; }
public List<PlayerWithHostility> players;
public String GetPlayerName(PlayerId playerId) {
@@ -217,8 +217,6 @@ 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) {
@@ -138,9 +138,9 @@ namespace common {
// Use TMP's calculated preferred height - this is the actual text size
contentHeight = tmpText.preferredHeight;
// 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) {
// Ensure Content RectTransform is tall enough to contain the text
// This is necessary for ScrollRect to scroll properly
if (scrollRect.content.rect.height < contentHeight) {
scrollRect.content.SetSizeWithCurrentAnchors(
RectTransform.Axis.Vertical,
contentHeight);
@@ -54,43 +54,18 @@ namespace common.GUIUtils {
Type = type,
Time = DateTime.UtcNow
});
// 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();
MainQueue.Q.Enqueue(() => {
errorTextField.text = AllMessageText;
panel.gameObject.SetActive(true);
});
}
}
// Use this for initialization
void Start() { panel.gameObject.SetActive(false); }
void Start() {
panel.gameObject.SetActive(false);
Application.logMessageReceivedThreaded += HandleLog;
}
public void DismissClicked() {
lock (_messages) {
@@ -21,24 +21,14 @@ 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;
@@ -58,8 +58,4 @@ 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;
}
@@ -4,7 +4,6 @@ filegroup(
name = "settings",
srcs = ["settings.tsv"],
visibility = [
"//ci:__pkg__",
"//src/main/scala/net/eagle0/eagle:__pkg__",
"//src/main/scala/net/eagle0/eagle/service:__pkg__",
],
@@ -14,7 +13,6 @@ filegroup(
name = "game_parameters",
srcs = ["game_parameters.json"],
visibility = [
"//ci:__pkg__",
"//src/main/scala/net/eagle0:__subpackages__",
"//src/test:__subpackages__",
],
@@ -24,7 +22,6 @@ 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__",
@@ -35,7 +32,6 @@ 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__",
@@ -50,7 +46,6 @@ 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__",
@@ -61,7 +56,6 @@ filegroup(
name = "province_map",
srcs = ["province_map.tsv"],
visibility = [
"//ci:__pkg__",
"//src/main/scala/net/eagle0:__subpackages__",
"//src/test:__subpackages__",
],
@@ -5,7 +5,6 @@ filegroup(
name = "settings",
srcs = ["settings.tsv"],
visibility = [
"//ci:__pkg__",
"//src/main/cpp/net/eagle0/shardok:__subpackages__",
"//src/test/cpp/net/eagle0/shardok:__subpackages__",
],
@@ -45,7 +44,6 @@ 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,7 +114,6 @@ 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,7 +54,6 @@ 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",
@@ -107,6 +106,7 @@ 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,7 +114,6 @@ 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",
],
)
@@ -155,7 +154,6 @@ 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",
@@ -171,7 +169,6 @@ 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",
@@ -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.model.view.province.ProvinceView
import net.eagle0.eagle.views.province_view.ProvinceView
object GameStateFactionExtensions {
@@ -1,7 +1,6 @@
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
@@ -76,22 +75,5 @@ 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)
}
}
@@ -207,6 +207,9 @@ 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",
@@ -800,6 +803,7 @@ 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",
@@ -1284,6 +1288,7 @@ 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",
@@ -1832,6 +1837,35 @@ 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"],
@@ -22,6 +22,7 @@ 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
@@ -349,10 +350,15 @@ case class EndBattleAftermathPhaseAction(
actionResultApplier: ActionResultApplier
) extends ProtolessRandomSequentialResultsAction {
// Revelation changes still need proto for ProvinceViewFilter
private def revelationChange(
battleRevelation: BattleRevelation,
gs: GameState
): RevelationChange =
): RevelationChange = {
// Lazy conversion to proto for ProvinceViewFilter calls
lazy val gsProto = GameStateConverter.toProto(gs)
lazy val provinceProto = gsProto.provinces(battleRevelation.provinceId)
RevelationChange(
changedFaction = battleRevelation.revelationType match {
case Unknown =>
@@ -363,21 +369,25 @@ case class EndBattleAftermathPhaseAction(
updatedReconnedProvinces = Vector(
ProvinceViewFilter
.withdrawnFromProvinceView(
province = gs.provinces(battleRevelation.provinceId),
gs = gs,
province = provinceProto,
gs = gsProto,
factionId = battleRevelation.revealedToFactionId
)
.copy(asOf = gs.currentDate)
.withAsOf(gsProto.currentDate.get)
)
)
case DidBattle =>
ChangedFactionC(
factionId = battleRevelation.revealedToFactionId,
updatedReconnedProvinces = Vector(
ProvinceViewFilter.filteredProvinceView(
gs.provinces(battleRevelation.provinceId),
gs
)
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)
)
)
},
@@ -386,6 +396,7 @@ case class EndBattleAftermathPhaseAction(
removedBattleRevelations = Vector(battleRevelation)
)
)
}
def revelationChanges(gs: GameState): Vector[RevelationChange] =
for {
@@ -1,11 +1,14 @@
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(
@@ -22,4 +25,8 @@ object HeroBackstoryUpdateActionGenerator {
.flatMap(_.rulingFactionId)
)
}
/** Creates action from proto GameState (for sequencer callbacks) */
def fromGameState(gameState: GameStateProto): ProtolessSequentialResultsAction =
apply(GameStateConverter.fromProto(gameState))
}
@@ -1,6 +1,7 @@
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
@@ -10,6 +11,7 @@ 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
@@ -20,6 +22,9 @@ 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]] =
@@ -89,10 +94,14 @@ case class PerformReconResolutionAction(
ChangedFactionC(
factionId = fromFactionId,
updatedReconnedProvinces = Vector(
ProvinceViewFilter.filteredProvinceView(
gameState.provinces(provinceId),
gameState
)
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)
)
)
),
@@ -0,0 +1,84 @@
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
)
}
}
@@ -119,8 +119,8 @@ object ResolveRansomOfferCommand {
case Accepted =>
NotificationDetails.RansomPaid(
ransomedHeroId = ransomOffer.prisonerToBeRansomed.prisonerHeroId,
ransomPaidByFactionId = ransomOffer.originatingFactionId,
ransomPaidToFactionId = actingFactionId
ransomPaidByFactionId = actingFactionId,
ransomPaidToFactionId = ransomOffer.originatingFactionId
)
case Rejected =>
@@ -386,12 +386,8 @@ 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",
],
)
@@ -1,6 +1,5 @@
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}
@@ -12,13 +11,6 @@ object StatWithConditionUtils {
minHigh = 55
)
def supportScala(support: Double): StatWithConditionT =
scWithRangesScala(
support.floor.toInt,
minMedium = 40,
minHigh = 55
)
def scWithRanges(
stat: Int,
minMedium: Int,
@@ -31,17 +23,4 @@ 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
)
}
@@ -56,13 +56,6 @@ 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)
@@ -42,13 +42,11 @@ 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",
@@ -9,13 +9,11 @@ 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,
@@ -76,15 +74,6 @@ 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],
@@ -1,34 +1,14 @@
package net.eagle0.eagle.library.util.view_filters
import net.eagle0.eagle.{BattalionId, FactionId}
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.internal.army.Army
import net.eagle0.eagle.internal.battalion.Battalion
import net.eagle0.eagle.views.army_view.ArmyView
object ArmyFilter {
// Pure Scala version
def filterArmy(
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],
army: Army,
battalions: Map[BattalionId, Battalion],
factionId: Option[FactionId]
): ArmyView =
ArmyView(
@@ -39,18 +19,4 @@ 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,18 +8,11 @@ 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",
],
)
@@ -123,7 +116,6 @@ 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",
],
)
@@ -154,15 +146,10 @@ 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",
],
)
@@ -174,41 +161,20 @@ 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,15 +13,4 @@ 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,7 +1,6 @@
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
@@ -17,11 +16,9 @@ object GameStateViewFilter {
currentDate = gs.currentDate,
provinces = gs.provinces.map {
case (pid, prov) =>
pid -> ProvinceViewConverter.toProto(
factionId
.map(fid => ProvinceViewFilter.filteredProvinceView(prov, gs, fid))
.getOrElse(ProvinceViewFilter.filteredProvinceView(prov, gs))
)
pid -> factionId
.map(fid => ProvinceViewFilter.filteredProvinceView(prov, gs, fid))
.getOrElse(ProvinceViewFilter.filteredProvinceView(prov, gs))
},
heroes = (gs.heroes ++ gs.killedHeroes).map {
case (hid, hero) =>
@@ -1,8 +1,6 @@
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 {
@@ -16,16 +14,6 @@ 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,
@@ -35,14 +23,4 @@ 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
)
}
@@ -8,191 +8,14 @@ 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.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.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.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,
@@ -213,19 +36,21 @@ object ProvinceViewFilter {
name = province.name,
rulingFactionId = province.rulingFactionId,
fullInfo = Some(fullProvinceInfo(province, gs, None)),
knownEvents = province.activeEvents.toVector,
asOf = Some(DateConverter.fromProto(gs.currentDate))
knownEvents = province.activeEvents,
asOf = Some(gs.currentDate.get)
)
private def deadHeroesFilteredOut(
fullProvinceInfo: FullProvinceInfo,
gameState: GameState
): FullProvinceInfo =
fullProvinceInfo.copy(
rulingFactionHeroIds =
fullProvinceInfo.rulingFactionHeroIds.filterNot(hid => gameState.killedHeroes.contains(hid)),
unaffiliatedHeroes =
fullProvinceInfo.unaffiliatedHeroes.filterNot(uh => gameState.killedHeroes.contains(uh.heroId))
fullProvinceInfo.update(
_.rulingFactionHeroIds.modify(
_.filterNot(hid => gameState.killedHeroes.contains(hid))
),
_.unaffiliatedHeroes.modify(
_.filterNot(uh => gameState.killedHeroes.contains(uh.heroId))
)
)
def filteredProvinceView(
@@ -240,13 +65,11 @@ 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)
@@ -280,7 +103,7 @@ object ProvinceViewFilter {
food = shipment.supplies.map(_.food).getOrElse(0),
gold = shipment.supplies.map(_.gold).getOrElse(0)
)
}.toVector
}
else Vector()
val fullInfo = Option
@@ -294,9 +117,9 @@ object ProvinceViewFilter {
name = province.name,
rulingFactionId = province.rulingFactionId,
fullInfo = fullInfo,
knownEvents = knownEvents.toVector,
knownEvents = knownEvents,
asOf =
if showAll then Some(DateConverter.fromProto(gs.currentDate))
if showAll then Some(gs.currentDate.get)
else reconnedView.flatMap(_.asOf),
knownIncomingArmies = incomingAttackers ++ incomingSupplies
)
@@ -308,11 +131,10 @@ 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 ProvinceViewConverter.fromProto(pv)
} yield pv
val knownEvents = province.activeEvents.filter { pe =>
if reconnedView.exists(_.knownEvents.contains(pe)) then true
@@ -326,13 +148,10 @@ object ProvinceViewFilter {
val fullInfo = reconnedView
.flatMap(_.fullInfo)
.getOrElse(fullProvinceInfo(province, gs, Some(factionId)))
.copy(
battalions = province.battalionIds
.map(gs.battalions)
.map {
LegacyBattalionViewFilter.limitedBattalionViewScala
}
.toVector
.update(
_.battalions := province.battalionIds.map(gs.battalions).map {
LegacyBattalionViewFilter.limitedBattalionView
}
)
ProvinceView(
@@ -340,7 +159,7 @@ object ProvinceViewFilter {
name = province.name,
rulingFactionId = province.rulingFactionId,
fullInfo = Some(fullInfo),
knownEvents = knownEvents.toVector,
knownEvents = knownEvents,
asOf = reconnedView.flatMap(_.asOf),
knownIncomingArmies = incomingAttackers ++ incomingSupplies
)
@@ -353,15 +172,13 @@ object ProvinceViewFilter {
): FullProvinceInfo = FullProvinceInfo(
rulingHeroId = province.rulingHeroId,
rulingFactionHeroIds = province.rulingFactionHeroIds
.sortWith(LegacyHeroUtils.sortOrderer(gs))
.toVector,
.sortWith(LegacyHeroUtils.sortOrderer(gs)),
battalions = province.battalionIds
.map(gs.battalions)
.map(LegacyBattalionViewFilter.filteredBattalionViewBelongingToPlayerScala)
.sortBy(_.typeId.value)
.toVector,
.map(LegacyBattalionViewFilter.filteredBattalionViewBelongingToPlayer)
.sortBy(_.`type`.index),
defendingArmy = province.defendingArmy
.map(ArmyFilter.filterArmyScala(_, gs.battalions, factionId)),
.map(ArmyFilter.filterArmy(_, gs.battalions, factionId)),
economy = province.economy.toFloat,
agriculture = province.agriculture.toFloat,
infrastructure = province.infrastructure.toFloat,
@@ -373,13 +190,13 @@ object ProvinceViewFilter {
priceIndex = province.priceIndex,
goldCap = LegacyProvinceUtils.goldCap(province),
foodCap = LegacyProvinceUtils.foodCap(province),
support = Some(supportScala(province.support)),
support = Some(supportSc(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)).toVector,
unaffiliatedHeroes = province.unaffiliatedHeroes.map(unaffiliatedHeroInfo(_, gs)),
rulerIsTraveling = province.rulerIsTraveling
)
@@ -407,7 +224,6 @@ object ProvinceViewFilter {
)
)
.map(army => incomingArmyInfo(ma = army, gs = gs, showDetails = false))
.toVector
private def incomingArmyInfo(
ma: MovingArmy,
@@ -430,14 +246,14 @@ object ProvinceViewFilter {
unitDetails =
if showDetails then
ma.getArmy.units.map { cu =>
UnitDetails(
IncomingArmyView.UnitDetails(
heroId = cu.heroId,
battalion = cu.battalionId.map(gs.battalions).map { b =>
LegacyBattalionViewFilter
.filteredBattalionViewBelongingToPlayerScala(b)
.filteredBattalionViewBelongingToPlayer(b)
}
)
}.toVector
}
else Vector()
)
@@ -449,11 +265,10 @@ object ProvinceViewFilter {
UnaffiliatedHeroBasics(
heroId = hero.id,
nameTextId = hero.nameTextId,
profession = ProfessionConverter.fromProto(hero.profession),
status = UnaffiliatedHeroConverter.unaffiliatedHeroTypeFromProto(uh.`type`),
profession = hero.profession,
status = uh.`type`,
isFactionLeader = LegacyFactionUtils.isFactionLeader(hero.id, gs),
recruitmentInfo =
uh.recruitmentInfo.fold(RecruitmentInfo.Unknown)(UnaffiliatedHeroConverter.recruitmentInfoFromProto)
recruitmentInfo = uh.recruitmentInfo
)
}
}
@@ -1,7 +1,6 @@
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
@@ -28,7 +27,7 @@ trait ActionResultT {
def newRoundId: Option[RoundId]
def newDate: Option[Date]
def lastCommandTypeForActingProvince: Option[SelectedCommand]
// def lastCommandTypeForActingProvince: Option[CommandType]
def newBattle: Option[ShardokBattle]
// def resolvedBattle: Option[String]
@@ -74,7 +73,7 @@ trait ActionResultT {
newRoundPhase: Option[RoundPhase] = newRoundPhase,
newRoundId: Option[RoundId] = newRoundId,
newDate: Option[Date] = newDate,
lastCommandTypeForActingProvince: Option[SelectedCommand] = lastCommandTypeForActingProvince,
// lastCommandTypeForActingProvince: Option[CommandType] = None,
newBattle: Option[ShardokBattle] = newBattle,
// resolvedBattle: Option[String] = None,
// resolvedImminentBattle: Option[Int] = None,
@@ -51,7 +51,6 @@ 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,7 +1,6 @@
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,
@@ -35,7 +34,6 @@ 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,
@@ -70,7 +68,6 @@ 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,
@@ -102,7 +99,6 @@ case class ActionResultC(
newRoundPhase = newRoundPhase,
newRoundId = newRoundId,
newDate = newDate,
lastCommandTypeForActingProvince = lastCommandTypeForActingProvince,
newBattle = newBattle,
newChronicleEntry = newChronicleEntry,
changedBattalions = changedBattalions,
@@ -16,7 +16,6 @@ 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",
@@ -72,12 +71,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.model.view.province.ProvinceView
import net.eagle0.eagle.views.province_view.ProvinceView
case class TrustLevelUpdate(
targetFactionId: FactionId,
@@ -1,7 +1,6 @@
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
@@ -54,7 +53,6 @@ object ActionResultProtoConverter {
newRoundPhase: Option[RoundPhase],
newRoundId: Option[RoundId],
newDate: Option[Date],
lastCommandTypeForActingProvince: Option[SelectedCommand],
newBattle: Option[ShardokBattle],
newChronicleEntry: Option[ChronicleEntry],
changedBattalions: Vector[ChangedBattalionT],
@@ -88,7 +86,6 @@ 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,7 +24,6 @@ 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",
@@ -111,9 +110,6 @@ scala_library(
"//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",
@@ -215,13 +211,10 @@ 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",
],
)
@@ -264,14 +257,10 @@ 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",
@@ -437,15 +426,11 @@ 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",
@@ -18,11 +18,9 @@ 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.model.view.province.ProvinceView
import net.eagle0.eagle.views.province_view.ProvinceView as ProvinceViewProto
import net.eagle0.eagle.views.province_view.ProvinceView
object ChangedFactionConverter {
def toProto(changedFaction: ChangedFactionT): ChangedFactionProto =
@@ -75,7 +73,7 @@ object ChangedFactionConverter {
addedOutgoingRansomOfferPartners = newOutgoingRansomOfferFactionIds.map(_.fid),
newFocusProvinceId = newFocusProvinceId,
clearFocusProvinceId = clearFocusProvinceId,
updatedReconnedProvinces = updatedReconnedProvinces.map(ProvinceViewConverter.toProto),
updatedReconnedProvinces = updatedReconnedProvinces,
removedReconnedProvinceIds = removedReconnedProvinceIds,
trustLevelUpdates = trustLevelUpdates.map { tlu =>
TrustLevelUpdateProto(
@@ -107,7 +105,7 @@ object ChangedFactionConverter {
addedOutgoingRansomOfferPartners: Seq[FactionId],
newFocusProvinceId: Option[ProvinceId],
clearFocusProvinceId: Boolean,
updatedReconnedProvinces: Seq[ProvinceViewProto],
updatedReconnedProvinces: Seq[ProvinceView],
removedReconnedProvinceIds: Seq[ProvinceId],
trustLevelUpdates: Seq[TrustLevelUpdateProto],
newLastActedProvinceId: Option[ProvinceId],
@@ -143,7 +141,7 @@ object ChangedFactionConverter {
.toVector,
newFocusProvinceId = newFocusProvinceId,
clearFocusProvinceId = clearFocusProvinceId,
updatedReconnedProvinces = updatedReconnedProvinces.map(ProvinceViewConverter.fromProto).toVector,
updatedReconnedProvinces = updatedReconnedProvinces.toVector,
removedReconnedProvinceIds = removedReconnedProvinceIds.toVector,
trustLevelUpdates = trustLevelUpdates.map { tlu =>
TrustLevelUpdate(
@@ -31,11 +31,6 @@ 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,7 +7,6 @@ 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,17 +15,14 @@ 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",
],
)
@@ -7,15 +7,13 @@ 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.model.view.province.ProvinceView
import net.eagle0.eagle.views.province_view.ProvinceView as ProvinceViewProto
import net.eagle0.eagle.views.province_view.ProvinceView
object FactionConverter {
def toProto(faction: FactionT): FactionProto = faction match {
@@ -45,7 +43,7 @@ object FactionConverter {
factionRelationships = factionRelationships.map(toProto),
incomingDiplomacyOffers = incomingDiplomacyOffers.map(DiplomacyOfferConverter.toProto),
focusProvinceId = focusProvinceId,
reconnedProvinces = reconnedProvinces.map(ProvinceViewConverter.toProto),
reconnedProvinces = reconnedProvinces,
lastActedProvinceIdThisRound = lastActedProvinceIdThisRound,
lastOutgoingTruceOfferRounds = lastOutgoingTruceOfferRounds.map(toProto),
lastOutgoingAllianceOfferRounds = lastOutgoingAllianceOfferRounds.map(toProto),
@@ -119,7 +117,7 @@ object FactionConverter {
factionRelationships: Seq[FactionRelationshipProto],
incomingDiplomacyOffers: Seq[DiplomacyOfferProto],
focusProvinceId: Option[ProvinceId],
reconnedProvinces: Seq[ProvinceViewProto],
reconnedProvinces: Seq[ProvinceView],
lastActedProvinceIdThisRound: ProvinceId,
lastOutgoingTruceOfferRounds: Seq[OutgoingOfferRoundProto],
lastOutgoingAllianceOfferRounds: Seq[OutgoingOfferRoundProto],
@@ -139,7 +137,7 @@ object FactionConverter {
.map(DiplomacyOfferConverter.fromProto)
.toVector,
focusProvinceId = focusProvinceId,
reconnedProvinces = reconnedProvinces.map(ProvinceViewConverter.fromProto).toVector,
reconnedProvinces = reconnedProvinces.toVector,
lastActedProvinceIdThisRound = lastActedProvinceIdThisRound,
lastOutgoingTruceOfferRounds = lastOutgoingTruceOfferRounds.map(fromProto).toVector,
lastOutgoingAllianceOfferRounds = lastOutgoingAllianceOfferRounds.map(fromProto).toVector,
@@ -85,7 +85,6 @@ 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__",
@@ -36,8 +36,7 @@ object HeroConverter {
imagePath: String,
personalityWords: Vector[String],
backstoryVersions: Vector[BackstoryVersion],
backstoryEvents: Vector[EventForHeroBackstoryT],
ledFactionName: Option[String]
backstoryEvents: Vector[EventForHeroBackstoryT]
) =>
HeroProto(
id = id,
@@ -68,8 +67,7 @@ object HeroConverter {
case BackstoryVersion(textId, date) =>
BackstoryVersionProto(textId, Some(DateConverter.toProto(date)))
},
newBackstoryEvents = backstoryEvents.map(EventForHeroBackstoryConverter.toProto),
ledFactionName = ledFactionName.getOrElse("")
newBackstoryEvents = backstoryEvents.map(EventForHeroBackstoryConverter.toProto)
)
}
@@ -101,7 +99,6 @@ object HeroConverter {
backstoryVersions,
newBackstoryEvents,
pronounGender,
ledFactionName,
_ /* unknownFields */
) =>
HeroC(
@@ -135,8 +132,7 @@ object HeroConverter {
}.toVector,
backstoryEvents = newBackstoryEvents
.map(EventForHeroBackstoryConverter.fromProto)
.toVector,
ledFactionName = Option(ledFactionName).filter(_.nonEmpty)
.toVector
)
}
}
@@ -45,7 +45,6 @@ scala_library(
srcs = ["ProvinceEventConverter.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:__pkg__",
"//src/test/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
],
@@ -66,7 +65,6 @@ 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/view_filters:__pkg__",
"//src/test/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
],
deps = [
@@ -100,7 +100,7 @@ object ProvinceConverter {
hexMapName: String,
castleCount: Int,
heroCap: Int,
lastCommand: Option[SelectedCommand]
lastCommand: Any
) =>
ProvinceProto(
id = id,
@@ -131,7 +131,15 @@ object ProvinceConverter {
activeEvents = activeEvents.map(ProvinceEventConverter.toProto),
lastBeastsDate = lastBeastsDate.map(DateConverter.toProto),
lastRiotDate = lastRiotDate.map(DateConverter.toProto),
lastCommand = lastCommand.getOrElse(SelectedCommand.Empty),
lastCommand = lastCommand match {
case Some(sc: SelectedCommand) =>
sc
case Some(x) =>
throw new ProtoConversionException(
s"Unknown lastCommand type: ${x.getClass}"
)
case None => SelectedCommand.Empty
},
incomingArmies = incomingArmies.map(ArmyConverter.toProto),
hostileArmies = hostileArmies.map(ArmyConverter.toProto),
defendingArmy = defendingArmy.map(ArmyConverter.toProto),
@@ -220,7 +228,7 @@ object ProvinceConverter {
activeEvents = activeEvents.map(ProvinceEventConverter.fromProto).toVector,
lastBeastsDate = lastBeastsDate.map(date => DateConverter.fromProto(Some(date))),
lastRiotDate = lastRiotDate.map(date => DateConverter.fromProto(Some(date))),
lastCommand = Option.when(!lastCommand.isEmpty)(lastCommand),
lastCommand = Some(lastCommand),
incomingArmies = incomingArmies.map(ArmyConverter.fromProto).toVector,
hostileArmies = hostileArmies.map(ArmyConverter.fromProto).toVector,
defendingArmy = defendingArmy.map(ArmyConverter.fromProto),
@@ -12,7 +12,7 @@ import net.eagle0.eagle.common.unaffiliated_hero_type.UnaffiliatedHeroType.{
import net.eagle0.eagle.common.unaffiliated_hero_type.UnaffiliatedHeroType as UnaffiliatedHeroTypeProto
import net.eagle0.eagle.model.proto_converters.hero.ProfessionConverter
import net.eagle0.eagle.model.proto_converters.UnaffiliatedHeroConverter
import net.eagle0.eagle.model.state.unaffiliated_hero.{RecruitmentInfo, UnaffiliatedHeroType}
import net.eagle0.eagle.model.state.unaffiliated_hero.UnaffiliatedHeroType
import net.eagle0.eagle.model.view.province.UnaffiliatedHeroBasics
import net.eagle0.eagle.views.province_view.UnaffiliatedHeroBasics as UnaffiliatedHeroBasicsProto
@@ -50,6 +50,6 @@ object UnaffiliatedHeroBasicsConverter {
isFactionLeader = proto.isFactionLeader,
recruitmentInfo = proto.recruitmentInfo
.map(ri => UnaffiliatedHeroConverter.recruitmentInfoFromProto(ri))
.getOrElse(RecruitmentInfo.Unknown)
.getOrElse(throw new IllegalArgumentException("RecruitmentInfo is required"))
)
}
@@ -9,7 +9,6 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/library/util:__pkg__",
"//src/main/scala/net/eagle0/eagle/library/util/province:__pkg__",
"//src/main/scala/net/eagle0/eagle/library/util/validations:__pkg__",
"//src/main/scala/net/eagle0/eagle/library/util/view_filters:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/action_result:__subpackages__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/shardok_battle:__pkg__",
@@ -42,7 +41,6 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/model/view/battalion:__pkg__",
"//src/test/scala/net/eagle0/eagle/library:__subpackages__",
"//src/test/scala/net/eagle0/eagle/library/util:__subpackages__",
"//src/test/scala/net/eagle0/eagle/model/proto_converters/view:__subpackages__",
],
deps = [
],
@@ -123,7 +121,6 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/model/view:__subpackages__",
"//src/test/scala/net/eagle0/eagle/library:__subpackages__",
"//src/test/scala/net/eagle0/eagle/library/util:__subpackages__",
"//src/test/scala/net/eagle0/eagle/model/proto_converters/view:__subpackages__",
],
deps = [
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
@@ -41,9 +41,9 @@ scala_library(
deps = [
":faction_relationship",
":prestige_modifier",
"//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/state/diplomacy_offer",
"//src/main/scala/net/eagle0/eagle/model/view/province:province_view",
],
)
@@ -3,7 +3,7 @@ package net.eagle0.eagle.model.state.faction
import net.eagle0.eagle.{FactionId, HeroId, ProvinceId, RoundId}
import net.eagle0.eagle.model.state.diplomacy_offer.DiplomacyOffer
import net.eagle0.eagle.model.state.faction.FactionT.OutgoingOfferRound
import net.eagle0.eagle.model.view.province.ProvinceView
import net.eagle0.eagle.views.province_view.ProvinceView
object FactionT {
case class OutgoingOfferRound(
@@ -24,7 +24,7 @@ trait FactionT {
def focusProvinceId: Option[ProvinceId]
def reconnedProvinces: Vector[ProvinceView]
def reconnedProvinces: Vector[ProvinceView] // FIXME: this is just using the proto for now
def lastActedProvinceIdThisRound: ProvinceId
@@ -13,10 +13,10 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/model/state/faction",
],
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/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/view/province:province_view",
],
)
@@ -4,7 +4,7 @@ import net.eagle0.eagle.{FactionId, HeroId, ProvinceId, RoundId}
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.FactionT.OutgoingOfferRound
import net.eagle0.eagle.model.view.province.ProvinceView
import net.eagle0.eagle.views.province_view.ProvinceView
case class FactionC(
id: FactionId,
@@ -84,7 +84,6 @@ scala_library(
"//src/main/scala/net/eagle0/util/hero_generation:__pkg__",
"//src/test/scala/net/eagle0/eagle/library:__subpackages__",
"//src/test/scala/net/eagle0/eagle/library/actions:__subpackages__",
"//src/test/scala/net/eagle0/eagle/model/proto_converters/view:__subpackages__",
"//src/test/scala/net/eagle0/eagle/service/new_game_creation:__pkg__",
],
deps = [],
@@ -40,10 +40,4 @@ trait HeroT {
final def backstoryTextId: ClientTextId = backstoryVersions.last.textId
def backstoryEvents: Vector[EventForHeroBackstoryT]
/**
* The name of the faction this hero would lead, if they led one. Populated for "great person" heroes from the heroes
* TSV; None for others.
*/
def ledFactionName: Option[String]
}
@@ -30,8 +30,7 @@ case class HeroC(
imagePath: String = "",
personalityWords: Vector[String] = Vector(),
backstoryVersions: Vector[BackstoryVersion] = Vector(),
backstoryEvents: Vector[EventForHeroBackstoryT] = Vector(),
ledFactionName: Option[String] = None
backstoryEvents: Vector[EventForHeroBackstoryT] = Vector()
) extends HeroT {
def withProfession(profession: Profession): HeroC =
copy(profession = profession)
@@ -63,7 +63,6 @@ scala_library(
":event",
":incoming_end_turn_action",
":orders",
"//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/state:army",
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type_id",
@@ -1,7 +1,6 @@
package net.eagle0.eagle.model.state.province
import net.eagle0.eagle.{BattalionId, FactionId, HeroId, ProvinceId}
import net.eagle0.eagle.api.selected_command.SelectedCommand
import net.eagle0.eagle.model.state.{
Army,
BattalionTypeId,
@@ -87,8 +86,6 @@ trait ProvinceT {
def castleCount: Int
def heroCap: Int
def lastCommand: Option[SelectedCommand]
def updateWith(
name: String = name,
neighbors: Vector[Neighbor] = neighbors,
@@ -125,8 +122,7 @@ trait ProvinceT {
battleRevelations: Vector[BattleRevelation] = battleRevelations,
hexMapName: String = hexMapName,
castleCount: Int = castleCount,
heroCap: Int = heroCap,
lastCommand: Option[SelectedCommand] = lastCommand
heroCap: Int = heroCap
): ProvinceT
def withRulingFactionId(rulingFactionId: FactionId): ProvinceT =
@@ -18,7 +18,6 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/model/state/province:orders",
],
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/state:army",
"//src/main/scala/net/eagle0/eagle/model/state:battle_revelation",
@@ -1,7 +1,6 @@
package net.eagle0.eagle.model.state.province.concrete
import net.eagle0.eagle.{BattalionId, FactionId, HeroId, ProvinceId}
import net.eagle0.eagle.api.selected_command.SelectedCommand
import net.eagle0.eagle.model.state.{
Army,
BattalionTypeId,
@@ -62,7 +61,10 @@ case class ProvinceC(
hexMapName: String = "",
castleCount: Int = 0,
heroCap: Int = 0,
lastCommand: Option[SelectedCommand] = None
// This is super gross, but I don't want to import this proto here when only one use case needs it,
// so let's just match for it when we need it until I can replace the command proto with something
// more lightweight.
lastCommand: Option[Any] = None
) extends ProvinceT {
override def updateWith(
name: String = name,
@@ -100,8 +102,7 @@ case class ProvinceC(
battleRevelations: Vector[BattleRevelation] = battleRevelations,
hexMapName: String = hexMapName,
castleCount: Int = castleCount,
heroCap: Int = heroCap,
lastCommand: Option[SelectedCommand] = lastCommand
heroCap: Int = heroCap
): ProvinceT =
ProvinceC(
id = this.id,
@@ -8,7 +8,6 @@ scala_library(
"//src/main/scala/net/eagle0/eagle:__subpackages__",
"//src/test/scala/net/eagle0/eagle/library/actions:__subpackages__",
"//src/test/scala/net/eagle0/eagle/library/util:__subpackages__",
"//src/test/scala/net/eagle0/eagle/model/proto_converters/view:__subpackages__",
],
deps = [
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
@@ -10,11 +10,6 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/model/proto_converters/view:__subpackages__",
"//src/main/scala/net/eagle0/eagle/model/state/hero:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/view:__subpackages__",
"//src/test/scala/net/eagle0/eagle/library/util/view_filters:__pkg__",
"//src/test/scala/net/eagle0/eagle/model/proto_converters/view:__subpackages__",
],
exports = [
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type_id",
],
deps = [
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
@@ -15,7 +15,7 @@ case class IncomingArmyView(
originProvinceId: ProvinceId,
destinationProvinceId: ProvinceId,
arrivalRoundId: RoundId,
food: Int = 0,
gold: Int = 0,
food: Int,
gold: Int,
unitDetails: Vector[UnitDetails] = Vector.empty
)
@@ -4,10 +4,6 @@ scala_library(
name = "unaffiliated_hero_basics",
srcs = ["UnaffiliatedHeroBasics.scala"],
visibility = ["//visibility:public"],
exports = [
"//src/main/scala/net/eagle0/eagle/model/state/hero:profession",
"//src/main/scala/net/eagle0/eagle/model/state/unaffiliated_hero",
],
deps = [
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/model/state/hero:profession",
@@ -160,8 +160,6 @@ class CustomBattleManager(
) extends BattleUpdateReceiver {
import CustomBattleManager.CreateCustomBattleResponse
private var customBattleCounter: Long = 0
val shardokClient =
new ShardokInterfaceGrpcClient(shardokInternalInterface, this)
@@ -175,8 +173,7 @@ class CustomBattleManager(
newGameRequest: NewGameRequest,
eagleGameId: Long
): CreateCustomBattleResponse = {
customBattleCounter += 1
val shardokGameId = s"custom_${eagleGameId}_$customBattleCounter"
val shardokGameId = s"${eagleGameId}_1"
val newController = CustomBattleGameController(
gameId = eagleGameId,
@@ -378,14 +378,8 @@ class GamesManager(
shardokGameId: ShardokGameId
): Boolean =
this.synchronized {
val controller = gameControllerInfos(eagleGameId).controller
val humanFactionIds = controller.humanClients.map(_.factionId)
val history = controller.engine.history
humanFactionIds.exists { fid =>
history
.shardokPlayerAvailableCommands(shardokGameId, fid)
.exists(_.currentCommand.nonEmpty)
}
gameControllerInfos(eagleGameId).controller.engine.history
.hasShardokPlayerAvailableCommands(shardokGameId)
}
def gamesFor(name: String): Vector[GamePlayerInfo] = this.synchronized {
@@ -59,8 +59,7 @@ object FixedHeroes {
private def personalityWords(commaDelimited: String): Vector[String] =
commaDelimited.split(", ").toVector
private def heroFromMap(map: Map[String, String]): LoadedHero = {
val factionName = map.getOrElse("faction_name", "").trim
private def heroFromMap(map: Map[String, String]): LoadedHero =
LoadedHero(
id = -1, // Placeholder ID, will be set later
backstory = map.getOrElse("backstory", ""),
@@ -91,8 +90,6 @@ object FixedHeroes {
case "other" => Gender.Other
case _ => throw new EagleInternalException("unspecified gender")
},
personalityWords = personalityWords(map.getOrElse("personality", "")),
ledFactionName = Option(factionName).filter(_.nonEmpty)
personalityWords = personalityWords(map.getOrElse("personality", ""))
)
}
}
@@ -38,8 +38,7 @@ object LoadedHeroConversion {
profession: Profession,
imagePath: String,
pronounGender: Gender,
personalityWords: Vector[String],
ledFactionName: Option[String]
personalityWords: Vector[String]
) =>
HeroC(
nameTextId = loadedHero.nameTextId,
@@ -64,8 +63,7 @@ object LoadedHeroConversion {
profession = profession,
imagePath = imagePath,
pronounGender = pronounGender,
personalityWords = personalityWords,
ledFactionName = ledFactionName
personalityWords = personalityWords
)
}
@@ -88,8 +86,7 @@ object LoadedHeroConversion {
profession: Profession,
imagePath: String,
pronounGender: Gender,
personalityWords: Vector[String],
ledFactionName: Option[String]
personalityWords: Vector[String]
) =>
Hero(
nameTextId = loadedHero.nameTextId,
@@ -114,8 +111,7 @@ object LoadedHeroConversion {
profession = ProfessionConverter.toProto(profession),
imagePath = imagePath,
pronounGender = GenderConverter.toProto(pronounGender),
personalityWords = personalityWords,
ledFactionName = ledFactionName.getOrElse("")
personalityWords = personalityWords
)
}
}
@@ -4,7 +4,6 @@ import scala.collection.mutable
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success}
import io.grpc.stub.StreamObserver
import net.eagle0.common.common_unit.*
import net.eagle0.common.game_setup_info.{GameSetupInfo, NewGameRequest}
import net.eagle0.common.player_info.{AlliedPlayer, PlayerSetupInfo}
@@ -49,113 +48,13 @@ class ShardokInterfaceGrpcClient(
resolveNewGameRequest(battle, newGameRequest)
}
private def playerInfo(
shardokPlayer: ShardokPlayer,
eagleGameState: GameState,
playerToUserMap: Map[FactionId, String]
): PlayerSetupInfo =
PlayerSetupInfo(
`type` = if playerToUserMap.contains(shardokPlayer.eagleFid) then HUMAN else AI,
eagleFactionId = shardokPlayer.eagleFid,
userName = playerToUserMap.getOrElse(shardokPlayer.eagleFid, ""),
units = toCommon(
shardokPlayer.getArmyGroup.armies.toVector,
eagleGameState
),
defender = shardokPlayer.isDefender,
victoryConditions = shardokPlayer.victoryConditions,
food = shardokPlayer.food,
allies = eagleGameState
.factions(shardokPlayer.eagleFid)
.factionRelationships
.filter(_.relationshipLevel != HOSTILE)
.map(_.targetFactionId)
.map(fid => AlliedPlayer(eagleFactionId = fid))
private def waitingForHumanPlayer(battle: ShardokBattle): Boolean =
resolutionReceiver.hasHumanPlayerCommands(
eagleGameId = battle.eagleGameId,
shardokGameId = battle.shardokGameId
)
/**
* Subscribes to game updates using server-side streaming.
*
* This replaces the previous polling approach (gameStatusRunner) with a push-based model. The server pushes updates
* as they occur, reducing latency and eliminating polling overhead.
*/
private def subscribeToGame(
eagleGameId: GameId,
shardokGameId: ShardokGameId,
newGameRequest: Option[NewGameRequest]
): Unit = {
val request = GameSubscriptionRequest(
gameId = shardokGameId,
gameSetupInfo = Some(
GameSetupInfo(
knownResultCount = resolutionReceiver.knownResultCount(
eagleGameId = eagleGameId,
shardokGameId = shardokGameId
),
currentGameState = resolutionReceiver.currentGameState(
eagleGameId = eagleGameId,
shardokGameId = shardokGameId
),
newGameRequest = newGameRequest
)
)
)
val responseObserver = new StreamObserver[GameStatusResponse] {
override def onNext(response: GameStatusResponse): Unit =
try handleStreamingResponse(response, eagleGameId, shardokGameId)
catch {
case e: Exception =>
println(s"Exception processing stream response for $shardokGameId: $e")
e.printStackTrace()
}
override def onError(t: Throwable): Unit = {
println(s"Stream error for $shardokGameId: $t")
// Reconnect after a delay if the battle is still pending
scheduleReconnect(eagleGameId, shardokGameId)
}
override def onCompleted(): Unit =
println(s"Stream completed for $shardokGameId")
}
grpcClient.subscribeToGame(request, responseObserver)
}
/** Schedules a reconnection attempt after stream disconnection with exponential backoff. */
private def scheduleReconnect(
eagleGameId: GameId,
shardokGameId: ShardokGameId,
delayMs: Long = 1000
): Unit =
Future {
Thread.sleep(delayMs)
pendingBattles.synchronized {
if pendingBattles.contains(shardokGameId) then
println(s"Reconnecting stream for $shardokGameId (delay was ${delayMs}ms)")
try subscribeToGame(eagleGameId, shardokGameId, None)
catch {
case e: Exception =>
println(s"Reconnect failed for $shardokGameId: $e")
// Exponential backoff with max 10 seconds
val nextDelay = Math.min(delayMs * 2, 10000)
scheduleReconnect(eagleGameId, shardokGameId, nextDelay)
}
}
}
/**
* Handles responses from the streaming subscription.
*
* Unlike handleBattleResponse, this does not trigger further polling since updates are pushed automatically via the
* stream.
*/
private def handleStreamingResponse(
update: GameStatusResponse,
eagleGameId: GameId,
shardokGameId: ShardokGameId
): Unit = {
private def handleBattleResponse(update: GameStatusResponse): Unit = {
val maybeBattle = pendingBattles.synchronized {
pendingBattles.get(update.gameId)
}
@@ -203,15 +102,85 @@ class ShardokInterfaceGrpcClient(
currentGameStateBytes = gameUpdateResponse.currentGameState.toByteArray
)
)
// Note: No polling needed - updates are pushed via stream
// Only do the gameStatusRunner if there are no available commands. If there are available commands, just wait
// for the next client post.
if !waitingForHumanPlayer(battle) then
gameStatusRunner(
eagleGameId = battle.eagleGameId,
shardokGameId = battle.shardokGameId,
newGameRequest = None
)
case Specific.Empty =>
// Empty response - nothing to process
// FIXME: maybe a delay
if !waitingForHumanPlayer(battle) then
gameStatusRunner(
eagleGameId = battle.eagleGameId,
shardokGameId = battle.shardokGameId,
newGameRequest = None
)
}
case None =>
println(s"No pending battle found for ${update.gameId}")
}
}
private def playerInfo(
shardokPlayer: ShardokPlayer,
eagleGameState: GameState,
playerToUserMap: Map[FactionId, String]
): PlayerSetupInfo =
PlayerSetupInfo(
`type` = if playerToUserMap.contains(shardokPlayer.eagleFid) then HUMAN else AI,
eagleFactionId = shardokPlayer.eagleFid,
userName = playerToUserMap.getOrElse(shardokPlayer.eagleFid, ""),
units = toCommon(
shardokPlayer.getArmyGroup.armies.toVector,
eagleGameState
),
defender = shardokPlayer.isDefender,
victoryConditions = shardokPlayer.victoryConditions,
food = shardokPlayer.food,
allies = eagleGameState
.factions(shardokPlayer.eagleFid)
.factionRelationships
.filter(_.relationshipLevel != HOSTILE)
.map(_.targetFactionId)
.map(fid => AlliedPlayer(eagleFactionId = fid))
)
private def gameStatusRunner(
eagleGameId: GameId,
shardokGameId: ShardokGameId,
newGameRequest: Option[NewGameRequest]
): Unit =
retrier
.withRetries(
_.getGameStatus(
GameStatusRequest(
gameId = shardokGameId,
gameSetupInfo = Some(
GameSetupInfo(
knownResultCount = resolutionReceiver.knownResultCount(
eagleGameId = eagleGameId,
shardokGameId = shardokGameId
),
currentGameState = resolutionReceiver.currentGameState(
eagleGameId = eagleGameId,
shardokGameId = shardokGameId
),
newGameRequest = newGameRequest
)
)
)
)
)
.onComplete {
case Success(gameStatus) => handleBattleResponse(gameStatus)
case Failure(e) =>
print(s"Caught exception $e\n")
gameStatusRunner(eagleGameId, shardokGameId, newGameRequest)
}
override def resolveNewGameRequest(
battle: ShardokBattle,
newGameRequest: NewGameRequest
@@ -220,7 +189,7 @@ class ShardokInterfaceGrpcClient(
pendingBattles(newGameRequest.gameId) = (battle, newGameRequest)
)
subscribeToGame(
gameStatusRunner(
eagleGameId = newGameRequest.eagleGameId,
shardokGameId = newGameRequest.gameId,
newGameRequest = Some(newGameRequest)
@@ -295,10 +264,10 @@ class ShardokInterfaceGrpcClient(
)
)
.onComplete {
case Success(_) =>
// Response acknowledged - actual updates come via stream
case Failure(e) =>
println(s"Failed to post command: $e")
case Success(gameStatusResponse) =>
handleBattleResponse(gameStatusResponse)
case Failure(e) =>
print("failed to post a command\n")
throw e
}
else print("Invalid token\n")
@@ -342,10 +311,10 @@ class ShardokInterfaceGrpcClient(
)
)
.onComplete {
case Success(_) =>
// Response acknowledged - actual updates come via stream
case Failure(e) =>
println(s"Failed to post placement command: $e")
case Success(gameStatusResponse) =>
handleBattleResponse(gameStatusResponse)
case Failure(e) =>
print("failed to post a placement command")
throw e
}
else print("Invalid token\n")
@@ -28,8 +28,7 @@ case class LoadedHero(
profession: Profession = Profession.NoProfession,
imagePath: String = "",
pronounGender: Gender = Gender.Other,
personalityWords: Vector[String] = Vector.empty,
ledFactionName: Option[String] = None
personalityWords: Vector[String] = Vector.empty
) {
def withId(newId: HeroId): LoadedHero = copy(id = newId)
def withFactionId(newFactionId: FactionId): LoadedHero =
@@ -138,7 +138,6 @@ scala_test(
"//src/main/scala/net/eagle0/eagle/model/state/quest",
"//src/main/scala/net/eagle0/eagle/model/state/unaffiliated_hero",
"//src/main/scala/net/eagle0/eagle/model/state/unaffiliated_hero/concrete",
"//src/main/scala/net/eagle0/eagle/model/view/province:province_view",
"//src/test/scala/net/eagle0/eagle/library/actions/impl:action_impl_pkg",
],
)
@@ -626,10 +625,8 @@ scala_test(
"//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:incoming_end_turn_action",
"//src/main/scala/net/eagle0/eagle/model/state/date",
"//src/main/scala/net/eagle0/eagle/model/state/quest",
"//src/main/scala/net/eagle0/eagle/model/state/unaffiliated_hero",
"//src/main/scala/net/eagle0/eagle/model/view/province:province_view",
"//src/test/scala/net/eagle0/eagle/library/actions/impl:action_impl_pkg",
"//src/test/scala/net/eagle0/eagle/library/util/validations:testing_noop_validator",
],
@@ -1028,6 +1025,20 @@ scala_test(
],
)
scala_test(
name = "unaffiliated_hero_moved_action_test",
srcs = ["UnaffiliatedHeroMovedActionTest.scala"],
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/actions/impl/action:unaffiliated_hero_moved_action",
"//src/main/scala/net/eagle0/eagle/library/settings:free_hero_move_vigor_cost",
"//src/main/scala/net/eagle0/eagle/library/util:idable",
"//src/test/scala/net/eagle0/common:proto_matchers",
"//src/test/scala/net/eagle0/eagle/library/actions/impl:action_impl_pkg",
],
)
scala_test(
name = "withdrawn_armies_return_home_action_test",
srcs = ["WithdrawnArmiesReturnHomeActionTest.scala"],
@@ -20,7 +20,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.proto_converters.game_state.GameStateConverter
import net.eagle0.eagle.model.state.date.Date as ScalaDate
import net.eagle0.eagle.model.state.province.{
IncomingEndTurnAction as IncomingEndTurnActionT,
IncomingRecon as IncomingReconT
@@ -203,7 +202,7 @@ class PerformReconResolutionActionTest extends AnyFlatSpec with BeforeAndAfterEa
cf.updatedReconnedProvinces should have size 1
val rp = cf.updatedReconnedProvinces.head
rp.asOf should contain(ScalaDate(year = 7238, month = ScalaDate.Month.April))
rp.asOf should contain(currentDate)
rp.id shouldBe 3
}
@@ -0,0 +1,156 @@
package net.eagle0.eagle
package library.actions.impl.action
import net.eagle0.common.ProtoMatchers.equalProto
import net.eagle0.common.SeededRandom
import net.eagle0.eagle.common.action_result_notification_details.{Notification, OutlawSpottedDetails}
import net.eagle0.eagle.common.action_result_notification_details.Notification.TargetFaction
import net.eagle0.eagle.common.action_result_type.ActionResultType.HERO_MOVED
import net.eagle0.eagle.common.recruitment_info.RecruitmentInfo
import net.eagle0.eagle.common.recruitment_info.RecruitmentStatus.{
RECRUITMENT_STATUS_NO_RULER_IN_PROVINCE,
RECRUITMENT_STATUS_OUTLAW,
RECRUITMENT_STATUS_TRAVELER
}
import net.eagle0.eagle.common.round_phase.RoundPhase.UNAFFILIATED_HERO_ACTIONS
import net.eagle0.eagle.common.unaffiliated_hero_type.UnaffiliatedHeroType.{
UNAFFILIATED_HERO_OUTLAW,
UNAFFILIATED_HERO_TRAVELER
}
import net.eagle0.eagle.internal.changed_hero.ChangedHero
import net.eagle0.eagle.internal.changed_hero.ChangedHero.Vigor.VigorDelta
import net.eagle0.eagle.internal.faction.Faction
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.unaffiliated_hero.UnaffiliatedHero
import net.eagle0.eagle.library.settings.FreeHeroMoveVigorCost
import net.eagle0.eagle.library.util.IDable.{mapifyFactions, mapifyHeroes, mapifyProvinces}
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
import org.scalatest.BeforeAndAfterEach
class UnaffiliatedHeroMovedActionTest extends AnyFlatSpec with Matchers with BeforeAndAfterEach {
private val outlawUH = UnaffiliatedHero(
heroId = 9,
`type` = UNAFFILIATED_HERO_OUTLAW,
recruitmentInfo = Some(RecruitmentInfo(status = RECRUITMENT_STATUS_OUTLAW))
)
private val travelerUH = UnaffiliatedHero(
heroId = 19,
`type` = UNAFFILIATED_HERO_TRAVELER,
recruitmentInfo = Some(RecruitmentInfo(status = RECRUITMENT_STATUS_TRAVELER))
)
private val fromPid = 22
private val toPid = 33
private val fromFid = 7
private val toFid = 9
private val gs = GameState(
heroes = mapifyHeroes(
Hero(id = 9),
Hero(id = 19)
),
provinces = mapifyProvinces(
Province(id = fromPid, rulingFactionId = Some(fromFid)),
Province(id = toPid, rulingFactionId = Some(toFid))
),
factions = mapifyFactions(
Faction(id = fromFid),
Faction(id = toFid)
),
currentPhase = UNAFFILIATED_HERO_ACTIONS
)
override def beforeEach(): Unit =
FreeHeroMoveVigorCost.setDoubleValue(12)
"immediateExecute" should "include a notification if it's an outlaw" in {
val result = UnaffiliatedHeroMovedAction(
uh = outlawUH,
fromProvinceId = fromPid,
toProvinceId = toPid,
gameState = gs
).immediateExecute(functionalRandom = SeededRandom(0xcafeface))
result.newValue.notificationsToDeliver shouldBe Vector(
Notification(
details = OutlawSpottedDetails(
outlawHeroId = outlawUH.heroId,
provinceId = toPid
),
targetFactions = Vector(TargetFaction(7), TargetFaction(9))
)
)
}
it should "not include a notification for a traveler" in {
val result = UnaffiliatedHeroMovedAction(
uh = travelerUH,
fromProvinceId = fromPid,
toProvinceId = toPid,
gameState = gs
).immediateExecute(functionalRandom = SeededRandom(0xcafeface)).newValue
result.newDeferredNotification shouldBe empty
}
it should "set the basics" in {
val result = UnaffiliatedHeroMovedAction(
uh = outlawUH,
fromProvinceId = fromPid,
toProvinceId = toPid,
gameState = gs
).immediateExecute(functionalRandom = SeededRandom(0xcafeface)).newValue
result.`type` shouldBe HERO_MOVED
result.changedProvinces should have size 2
result.changedProvinces.map(_.id) should contain(22)
result.changedProvinces.map(_.id) should contain(33)
}
it should "spend vigor for the moving hero" in {
val result = UnaffiliatedHeroMovedAction(
uh = outlawUH,
fromProvinceId = fromPid,
toProvinceId = toPid,
gameState = gs
).immediateExecute(functionalRandom = SeededRandom(0xcafeface)).newValue
result.`type` shouldBe HERO_MOVED
result.changedHeroes should have size 1
result.changedHeroes.head should equalProto(
ChangedHero(
id = outlawUH.heroId,
vigor = VigorDelta(-12)
)
)
}
it should "clear the divination and quest status for the moving hero" in {
val result = UnaffiliatedHeroMovedAction(
uh = outlawUH,
fromProvinceId = fromPid,
toProvinceId = toPid,
gameState = gs.update(
_.provinces(toPid).optionalRulingFactionId := None
)
).immediateExecute(functionalRandom = SeededRandom(0xcafeface)).newValue
result.`type` shouldBe HERO_MOVED
result.changedHeroes should have size 1
result.changedProvinces should have size 2
result.changedProvinces
.find(_.id == toPid)
.get
.newUnaffiliatedHeroes should have size 1
result.changedProvinces.last.newUnaffiliatedHeroes.head should equalProto(
outlawUH.update(
_.recruitmentInfo := RecruitmentInfo(
status = RECRUITMENT_STATUS_NO_RULER_IN_PROVINCE,
quest = None
)
)
)
}
}
@@ -8,7 +8,7 @@ import net.eagle0.eagle.common.recruitment_info.RecruitmentInfo
import net.eagle0.eagle.common.recruitment_info.RecruitmentStatus.*
import net.eagle0.eagle.common.round_phase.RoundPhase
import net.eagle0.eagle.common.round_phase.RoundPhase.PROVINCE_EVENTS
import net.eagle0.eagle.common.unaffiliated_hero_quest.{Quest, WealthQuest}
import net.eagle0.eagle.common.unaffiliated_hero_quest.Quest
import net.eagle0.eagle.common.unaffiliated_hero_type.UnaffiliatedHeroType.{
UNAFFILIATED_HERO_OUTLAW,
UNAFFILIATED_HERO_PRISONER,
@@ -113,13 +113,7 @@ class GameStateViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with M
recruitmentInfo = Some(
RecruitmentInfo(
status = RECRUITMENT_STATUS_HAS_QUEST,
quest = Some(
Quest(
componentCount = 1,
componentsFulfilled = 0,
details = WealthQuest(gold = 100, food = 50)
)
)
quest = Some(Quest())
)
)
),
@@ -299,8 +293,7 @@ class GameStateViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with M
recruitmentInfo = Some(
RecruitmentInfo(
status = RECRUITMENT_STATUS_HAS_QUEST,
// Note: componentCount is 0 because Scala WealthQuest doesn't preserve it
quest = Some(Quest(details = WealthQuest(gold = 100, food = 50)))
quest = Some(Quest())
)
)
),
@@ -34,21 +34,13 @@ scala_test(
name = "province_view_filter_test",
srcs = ["ProvinceViewFilterTest.scala"],
deps = [
"//src/main/protobuf/net/eagle0/eagle/common:combat_unit_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/protobuf/net/eagle0/eagle/views:province_view_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/settings:base_resource_limit",
"//src/main/scala/net/eagle0/eagle/library/settings:per_development_resource_limit",
"//src/main/scala/net/eagle0/eagle/library/util:idable",
"//src/main/scala/net/eagle0/eagle/library/util/view_filters:province_view_filter",
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type_id",
"//src/main/scala/net/eagle0/eagle/model/view/battalion",
"//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",
"//src/test/scala/net/eagle0/common:proto_matchers",
],
)
@@ -1,6 +1,7 @@
package net.eagle0.eagle.library.util.view_filters
import net.eagle0.eagle.common.battalion_type.BattalionTypeId as BattalionTypeIdProto
import net.eagle0.common.ProtoMatchers.equalProto
import net.eagle0.eagle.common.battalion_type.BattalionTypeId
import net.eagle0.eagle.common.combat_unit.CombatUnit
import net.eagle0.eagle.common.date.Date
import net.eagle0.eagle.internal.army.{Army, MovingArmy}
@@ -15,17 +16,9 @@ import net.eagle0.eagle.internal.supplies.{MovingSupplies, Supplies}
import net.eagle0.eagle.internal.unaffiliated_hero.UnaffiliatedHero
import net.eagle0.eagle.library.settings.{BaseResourceLimit, PerDevelopmentResourceLimit}
import net.eagle0.eagle.library.util.IDable.{mapifyFactions, mapifyHeroes, mapifyProvinces}
import net.eagle0.eagle.model.state.hero.Profession
import net.eagle0.eagle.model.state.unaffiliated_hero.{RecruitmentInfo, UnaffiliatedHeroType}
import net.eagle0.eagle.model.state.BattalionTypeId
import net.eagle0.eagle.model.view.battalion.BattalionView
import net.eagle0.eagle.model.view.incoming_army.{IncomingArmyView, UnitDetails}
import net.eagle0.eagle.model.view.province.{FullProvinceInfo, ProvinceView, UnaffiliatedHeroBasics}
import net.eagle0.eagle.views.province_view.{
FullProvinceInfo as FullProvinceInfoProto,
ProvinceView as ProvinceViewProto,
UnaffiliatedHeroBasics as UnaffiliatedHeroBasicsProto
}
import net.eagle0.eagle.views.battalion_view.BattalionView
import net.eagle0.eagle.views.incoming_army_view.IncomingArmyView
import net.eagle0.eagle.views.province_view.{FullProvinceInfo, ProvinceView, UnaffiliatedHeroBasics}
import net.eagle0.eagle.HeroId
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
@@ -43,14 +36,14 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
19 -> Battalion(
id = 19,
size = 253,
`type` = BattalionTypeIdProto.HEAVY_CAVALRY,
`type` = BattalionTypeId.HEAVY_CAVALRY,
training = 51.0,
armament = 75.0
),
21 -> Battalion(
id = 21,
size = 101,
`type` = BattalionTypeIdProto.LONGBOWMEN,
`type` = BattalionTypeId.LONGBOWMEN,
training = 12.0,
armament = 15.0
)
@@ -70,7 +63,7 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
factionId = 3
)
provinceView.fullInfo.get.economyDevastation shouldBe 0
provinceView.getFullInfo.economyDevastation shouldBe 0
}
it should "put a devastation of 2.1 at 2.1" in {
@@ -87,7 +80,7 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
factionId = 3
)
provinceView.fullInfo.get.economyDevastation shouldBe 2.1f
provinceView.getFullInfo.economyDevastation shouldBe 2.1f
}
it should "put an economy of 0.1 at 0.1" in {
@@ -104,7 +97,7 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
factionId = 3
)
provinceView.fullInfo.get.economy shouldBe 0.1f
provinceView.getFullInfo.economy shouldBe 0.1f
}
it should "show ruler is traveling if they are" in {
@@ -121,7 +114,7 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
factionId = 3
)
provinceView.fullInfo.get.rulerIsTraveling shouldBe true
provinceView.getFullInfo.rulerIsTraveling shouldBe true
}
it should "not show ruler is traveling if they are not" in {
@@ -138,7 +131,7 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
factionId = 3
)
provinceView.fullInfo.get.rulerIsTraveling shouldBe false
provinceView.getFullInfo.rulerIsTraveling shouldBe false
}
it should "show your own incoming armies into your own province" in {
@@ -175,37 +168,38 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
ProvinceViewFilter.filteredProvinceView(province, gameState, 3)
filtered.knownIncomingArmies should have size 1
filtered.knownIncomingArmies.head shouldBe IncomingArmyView(
factionId = 3,
heroCount = 2,
troopCount = 354,
originProvinceId = 1,
destinationProvinceId = 5,
arrivalRoundId = 4,
unitDetails = Vector(
UnitDetails(
heroId = 5,
battalion = Some(
BattalionView(
id = 19,
typeId = BattalionTypeId.HeavyCavalry,
size = 253,
training = Some(51),
armament = Some(75),
name = ""
)
)
),
UnitDetails(
heroId = 9,
battalion = Some(
BattalionView(
id = 21,
typeId = BattalionTypeId.Longbowmen,
size = 101,
training = Some(12),
armament = Some(15),
name = ""
filtered.knownIncomingArmies.head should equalProto(
IncomingArmyView(
factionId = 3,
heroCount = 2,
troopCount = 354,
originProvinceId = 1,
destinationProvinceId = 5,
arrivalRoundId = 4,
unitDetails = Vector(
IncomingArmyView
.UnitDetails(
heroId = 5,
battalion = Some(
BattalionView(
id = 19,
size = 253,
`type` = BattalionTypeId.HEAVY_CAVALRY,
training = Some(51),
armament = Some(75)
)
)
),
IncomingArmyView.UnitDetails(
heroId = 9,
battalion = Some(
BattalionView(
id = 21,
size = 101,
`type` = BattalionTypeId.LONGBOWMEN,
training = Some(12),
armament = Some(15)
)
)
)
)
@@ -283,13 +277,15 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
ProvinceViewFilter.filteredProvinceView(province, gameState, 3)
filtered.knownIncomingArmies should have size 1
filtered.knownIncomingArmies.head shouldBe IncomingArmyView(
factionId = 8,
heroCount = 2,
troopCount = 354,
originProvinceId = 1,
destinationProvinceId = 5,
arrivalRoundId = 4
filtered.knownIncomingArmies.head should equalProto(
IncomingArmyView(
factionId = 8,
heroCount = 2,
troopCount = 354,
originProvinceId = 1,
destinationProvinceId = 5,
arrivalRoundId = 4
)
)
}
@@ -327,37 +323,38 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
ProvinceViewFilter.filteredProvinceView(province, gameState, 3)
filtered.knownIncomingArmies should have size 1
filtered.knownIncomingArmies.head shouldBe IncomingArmyView(
factionId = 3,
heroCount = 2,
troopCount = 354,
originProvinceId = 1,
destinationProvinceId = 5,
arrivalRoundId = 4,
unitDetails = Vector(
UnitDetails(
heroId = 5,
battalion = Some(
BattalionView(
id = 19,
typeId = BattalionTypeId.HeavyCavalry,
size = 253,
training = Some(51),
armament = Some(75),
name = ""
)
)
),
UnitDetails(
heroId = 9,
battalion = Some(
BattalionView(
id = 21,
typeId = BattalionTypeId.Longbowmen,
size = 101,
training = Some(12),
armament = Some(15),
name = ""
filtered.knownIncomingArmies.head should equalProto(
IncomingArmyView(
factionId = 3,
heroCount = 2,
troopCount = 354,
originProvinceId = 1,
destinationProvinceId = 5,
arrivalRoundId = 4,
unitDetails = Vector(
IncomingArmyView
.UnitDetails(
heroId = 5,
battalion = Some(
BattalionView(
id = 19,
size = 253,
`type` = BattalionTypeId.HEAVY_CAVALRY,
training = Some(51),
armament = Some(75)
)
)
),
IncomingArmyView.UnitDetails(
heroId = 9,
battalion = Some(
BattalionView(
id = 21,
size = 101,
`type` = BattalionTypeId.LONGBOWMEN,
training = Some(12),
armament = Some(15)
)
)
)
)
@@ -435,37 +432,38 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
ProvinceViewFilter.filteredProvinceView(province, gameState, 3)
filtered.knownIncomingArmies should have size 1
filtered.knownIncomingArmies.head shouldBe IncomingArmyView(
factionId = 3,
heroCount = 2,
troopCount = 354,
originProvinceId = 1,
destinationProvinceId = 5,
arrivalRoundId = 4,
unitDetails = Vector(
UnitDetails(
heroId = 5,
battalion = Some(
BattalionView(
id = 19,
typeId = BattalionTypeId.HeavyCavalry,
size = 253,
training = Some(51),
armament = Some(75),
name = ""
)
)
),
UnitDetails(
heroId = 9,
battalion = Some(
BattalionView(
id = 21,
typeId = BattalionTypeId.Longbowmen,
size = 101,
training = Some(12),
armament = Some(15),
name = ""
filtered.knownIncomingArmies.head should equalProto(
IncomingArmyView(
factionId = 3,
heroCount = 2,
troopCount = 354,
originProvinceId = 1,
destinationProvinceId = 5,
arrivalRoundId = 4,
unitDetails = Vector(
IncomingArmyView
.UnitDetails(
heroId = 5,
battalion = Some(
BattalionView(
id = 19,
size = 253,
`type` = BattalionTypeId.HEAVY_CAVALRY,
training = Some(51),
armament = Some(75)
)
)
),
IncomingArmyView.UnitDetails(
heroId = 9,
battalion = Some(
BattalionView(
id = 21,
size = 101,
`type` = BattalionTypeId.LONGBOWMEN,
training = Some(12),
armament = Some(15)
)
)
)
)
@@ -533,24 +531,16 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
val filtered =
ProvinceViewFilter.filteredProvinceView(province, gameState, 11)
inside(filtered.fullInfo.get.unaffiliatedHeroes) {
inside(filtered.getFullInfo.unaffiliatedHeroes) {
case uhs: Vector[UnaffiliatedHeroBasics] =>
uhs should contain theSameElementsAs Vector(
UnaffiliatedHeroBasics(
heroId = 1,
nameTextId = "hn_1",
profession = Profession.NoProfession,
status = UnaffiliatedHeroType.Unknown,
isFactionLeader = false,
recruitmentInfo = RecruitmentInfo.Unknown
nameTextId = "hn_1"
),
UnaffiliatedHeroBasics(
heroId = 2,
nameTextId = "hn_2",
profession = Profession.NoProfession,
status = UnaffiliatedHeroType.Unknown,
isFactionLeader = false,
recruitmentInfo = RecruitmentInfo.Unknown
nameTextId = "hn_2"
)
)
}
@@ -635,10 +625,10 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
val askingFaction = Faction(
id = 4,
reconnedProvinces = Vector(
ProvinceViewProto(
ProvinceView(
id = 5,
fullInfo = Some(
FullProvinceInfoProto(
FullProvinceInfo(
rulingFactionHeroIds = Vector(2, 3)
)
)
@@ -657,7 +647,7 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
val filtered =
ProvinceViewFilter.filteredProvinceView(province, gameState, 4)
inside(filtered.fullInfo.get.rulingFactionHeroIds) {
inside(filtered.getFullInfo.rulingFactionHeroIds) {
case ids: Vector[HeroId] =>
ids should contain theSameElementsAs Vector(2, 3)
}
@@ -693,11 +683,11 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
)
),
reconnedProvinces = Vector(
ProvinceViewProto(
ProvinceView(
id = 5,
asOf = Some(Date(year = 2024, month = 1)),
fullInfo = Some(
FullProvinceInfoProto(
FullProvinceInfo(
rulingFactionHeroIds = Vector(2, 3)
)
)
@@ -716,7 +706,7 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
val filtered =
ProvinceViewFilter.filteredProvinceView(province, gameState, 4)
inside(filtered.fullInfo.get.rulingFactionHeroIds) {
inside(filtered.getFullInfo.rulingFactionHeroIds) {
case ids: Vector[HeroId] =>
ids should contain theSameElementsAs Vector(2, 3)
}
@@ -740,11 +730,11 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
)
),
reconnedProvinces = Vector(
ProvinceViewProto(
ProvinceView(
id = 5,
asOf = Some(Date(year = 2024, month = 4)),
fullInfo = Some(
FullProvinceInfoProto(
FullProvinceInfo(
rulingFactionHeroIds = Vector(2, 3, 4)
)
)
@@ -763,11 +753,11 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
)
),
reconnedProvinces = Vector(
ProvinceViewProto(
ProvinceView(
id = 5,
asOf = Some(Date(year = 2024, month = 1)),
fullInfo = Some(
FullProvinceInfoProto(
FullProvinceInfo(
rulingFactionHeroIds = Vector(2, 3)
)
)
@@ -786,7 +776,7 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
val filtered =
ProvinceViewFilter.filteredProvinceView(province, gameState, 4)
inside(filtered.fullInfo.get.rulingFactionHeroIds) {
inside(filtered.getFullInfo.rulingFactionHeroIds) {
case ids: Vector[HeroId] =>
ids should contain theSameElementsAs Vector(2, 3, 4)
}
@@ -810,11 +800,11 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
)
),
reconnedProvinces = Vector(
ProvinceViewProto(
ProvinceView(
id = 5,
asOf = Some(Date(year = 2024, month = 4)),
fullInfo = Some(
FullProvinceInfoProto(
FullProvinceInfo(
rulingFactionHeroIds = Vector(2, 3, 4)
)
)
@@ -833,11 +823,11 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
)
),
reconnedProvinces = Vector(
ProvinceViewProto(
ProvinceView(
id = 5,
asOf = Some(Date(year = 2024, month = 6)),
fullInfo = Some(
FullProvinceInfoProto(
FullProvinceInfo(
rulingFactionHeroIds = Vector(2, 3)
)
)
@@ -856,7 +846,7 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
val filtered =
ProvinceViewFilter.filteredProvinceView(province, gameState, 4)
inside(filtered.fullInfo.get.rulingFactionHeroIds) {
inside(filtered.getFullInfo.rulingFactionHeroIds) {
case ids: Vector[HeroId] =>
ids should contain theSameElementsAs Vector(2, 3)
}
@@ -872,10 +862,10 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
val askingFaction = Faction(
id = 4,
reconnedProvinces = Vector(
ProvinceViewProto(
ProvinceView(
id = 5,
fullInfo = Some(
FullProvinceInfoProto(
FullProvinceInfo(
rulingFactionHeroIds = Vector(2, 3)
)
)
@@ -895,7 +885,7 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
val filtered =
ProvinceViewFilter.filteredProvinceView(province, gameState, 4)
inside(filtered.fullInfo.get.rulingFactionHeroIds) {
inside(filtered.getFullInfo.rulingFactionHeroIds) {
case ids: Vector[HeroId] =>
ids should contain theSameElementsAs Vector(3)
}
@@ -916,16 +906,16 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
val askingFaction = Faction(
id = 4,
reconnedProvinces = Vector(
ProvinceViewProto(
ProvinceView(
id = 5,
fullInfo = Some(
FullProvinceInfoProto(
FullProvinceInfo(
unaffiliatedHeroes = Vector(
UnaffiliatedHeroBasicsProto(
UnaffiliatedHeroBasics(
heroId = 4,
nameTextId = "hn_4"
),
UnaffiliatedHeroBasicsProto(
UnaffiliatedHeroBasics(
heroId = 6,
nameTextId = "hn_6"
)
@@ -951,24 +941,16 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
val filtered =
ProvinceViewFilter.filteredProvinceView(province, gameState, 4)
inside(filtered.fullInfo.get.unaffiliatedHeroes) {
inside(filtered.getFullInfo.unaffiliatedHeroes) {
case uhs: Vector[UnaffiliatedHeroBasics] =>
uhs should contain theSameElementsAs Vector(
UnaffiliatedHeroBasics(
heroId = 4,
nameTextId = "hn_4",
profession = Profession.NoProfession,
status = UnaffiliatedHeroType.Unknown,
isFactionLeader = false,
recruitmentInfo = RecruitmentInfo.Unknown
nameTextId = "hn_4"
),
UnaffiliatedHeroBasics(
heroId = 6,
nameTextId = "hn_6",
profession = Profession.NoProfession,
status = UnaffiliatedHeroType.Unknown,
isFactionLeader = false,
recruitmentInfo = RecruitmentInfo.Unknown
nameTextId = "hn_6"
)
)
}
@@ -989,13 +971,13 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
val askingFaction = Faction(
id = 4,
reconnedProvinces = Vector(
ProvinceViewProto(
ProvinceView(
id = 5,
fullInfo = Some(
FullProvinceInfoProto(
FullProvinceInfo(
unaffiliatedHeroes = Vector(
UnaffiliatedHeroBasicsProto(heroId = 4),
UnaffiliatedHeroBasicsProto(heroId = 6)
UnaffiliatedHeroBasics(heroId = 4),
UnaffiliatedHeroBasics(heroId = 6)
)
)
)
@@ -1015,7 +997,7 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
val filtered =
ProvinceViewFilter.filteredProvinceView(province, gameState, 4)
inside(filtered.fullInfo.get.unaffiliatedHeroes) {
inside(filtered.getFullInfo.unaffiliatedHeroes) {
case uhs: Vector[UnaffiliatedHeroBasics] =>
uhs.map(_.heroId) should contain theSameElementsAs Vector(6)
}

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