mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-29 09:35:42 +00:00
Compare commits
67
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0633cc911 | ||
|
|
d1e4ba4efe | ||
|
|
03c26b01c9 | ||
|
|
830c524c53 | ||
|
|
c348085439 | ||
|
|
b0f5f05362 | ||
|
|
92b41c9b36 | ||
|
|
5c83fa479d | ||
|
|
831b72b382 | ||
|
|
d3c1542eaa | ||
|
|
c7b5e2db0d | ||
|
|
9ac664659f | ||
|
|
4c37d33682 | ||
|
|
71c7d700a5 | ||
|
|
ab485919ed | ||
|
|
8725ccfb34 | ||
|
|
73372d82bf | ||
|
|
f21839df81 | ||
|
|
fc1d5aa7ad | ||
|
|
c7cacb4915 | ||
|
|
d26a2322e3 | ||
|
|
c9d252093a | ||
|
|
7c31faaee0 | ||
|
|
3fab4b94f1 | ||
|
|
0ff8a3a8e1 | ||
|
|
ea70bbb2ac | ||
|
|
8e14f4cdbb | ||
|
|
6ffbbe242e | ||
|
|
84108b995a | ||
|
|
e4adac2eed | ||
|
|
3b83abe4c6 | ||
|
|
355c06d342 | ||
|
|
dfd1da8b8a | ||
|
|
ded08187b2 | ||
|
|
8747682842 | ||
|
|
ac5fa9a495 | ||
|
|
d65e486f36 | ||
|
|
9b719cafa8 | ||
|
|
87218d7a72 | ||
|
|
f7a0d562fe | ||
|
|
01ffbb8234 | ||
|
|
aa4f58c0c4 | ||
|
|
61c2d92b01 | ||
|
|
08dedebf7d | ||
|
|
c673f57179 | ||
|
|
29ec88c6e2 | ||
|
|
d50de9da3f | ||
|
|
9340500b22 | ||
|
|
1433f3d29c | ||
|
|
331702bf3c | ||
|
|
fbb04270a5 | ||
|
|
b8599d9088 | ||
|
|
d7e29eeb89 | ||
|
|
6f6c205f11 | ||
|
|
15e5d82504 | ||
|
|
b7ff76a38e | ||
|
|
e0d5a9f2ec | ||
|
|
bf02de3190 | ||
|
|
cbc14e5131 | ||
|
|
9974f32ebb | ||
|
|
ee6189a9d7 | ||
|
|
e0304125b8 | ||
|
|
11eac42e30 | ||
|
|
d0a63f1c61 | ||
|
|
97df984189 | ||
|
|
6c771df84f | ||
|
|
7e40420fe1 |
@@ -26,8 +26,8 @@ common --host_cxxopt="--std=c++23"
|
||||
common --javacopt="-Xlint:-options"
|
||||
|
||||
# suppress warnings due to https://developer.apple.com/forums/thread/733317
|
||||
common --linkopt=-Wl
|
||||
common:macos --linkopt=-Wl,-no_warn_duplicate_libraries
|
||||
# Use host_linkopt for macOS-specific flags to avoid passing them to Linux cross-compilation
|
||||
common:macos --host_linkopt=-Wl,-no_warn_duplicate_libraries
|
||||
|
||||
# Fix Xcode version caching issue - avoids need for `bazel clean --expunge` after Xcode updates
|
||||
common:macos --repo_env=DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer
|
||||
|
||||
@@ -3,14 +3,14 @@ name: Build Linux Sysroot
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
create_release:
|
||||
description: 'Create a GitHub release with the sysroot'
|
||||
version:
|
||||
description: 'Sysroot version (e.g., v2, v3)'
|
||||
required: true
|
||||
default: 'true'
|
||||
type: boolean
|
||||
default: 'v2'
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build-sysroot:
|
||||
@@ -28,23 +28,39 @@ jobs:
|
||||
name: ubuntu-noble-sysroot
|
||||
path: tools/sysroot/output/
|
||||
|
||||
- name: Create Release
|
||||
if: ${{ inputs.create_release }}
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
tag_name: sysroot-noble-v1
|
||||
name: Ubuntu 24.04 Noble Sysroot
|
||||
body: |
|
||||
Ubuntu 24.04 (Noble) sysroot for cross-compilation from macOS to Linux.
|
||||
- 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
|
||||
|
||||
Contains:
|
||||
- libstdc++-13 (C++23 support)
|
||||
- libc6-dev
|
||||
- linux-libc-dev
|
||||
- zlib1g-dev
|
||||
- libssl-dev
|
||||
- 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
|
||||
|
||||
Use with toolchains_llvm sysroot rule.
|
||||
files: |
|
||||
tools/sysroot/output/ubuntu_noble_amd64_sysroot.tar.xz
|
||||
tools/sysroot/output/ubuntu_noble_amd64_sysroot.sha256
|
||||
# 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 ")"
|
||||
|
||||
@@ -25,6 +25,8 @@ permissions:
|
||||
jobs:
|
||||
build-eagle:
|
||||
runs-on: self-hosted
|
||||
outputs:
|
||||
image_tag: ${{ steps.push-eagle.outputs.image_tag }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
@@ -42,21 +44,128 @@ jobs:
|
||||
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
|
||||
id: push-eagle
|
||||
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
|
||||
run: bazel run //ci:eagle_server_push
|
||||
env:
|
||||
DOCKER_CONFIG: ${{ github.workspace }}/.docker
|
||||
run: |
|
||||
set -ex
|
||||
|
||||
# Build the push target to get crane in runfiles
|
||||
bazel build //ci:eagle_server_push
|
||||
|
||||
# Use crane directly for push (avoids OCI->Docker digest mismatch)
|
||||
CRANE="bazel-bin/ci/push_eagle_server_push.sh.runfiles/rules_oci~~oci~oci_crane_darwin_arm64/crane"
|
||||
echo "Using crane: $CRANE"
|
||||
|
||||
# Push with SHA tag
|
||||
GIT_SHA=$(git rev-parse --short=8 HEAD)
|
||||
IMAGE_TAG="registry.digitalocean.com/eagle0/eagle-server:${GIT_SHA}"
|
||||
echo "Pushing eagle image: $IMAGE_TAG"
|
||||
$CRANE push bazel-bin/ci/eagle_server_image "$IMAGE_TAG"
|
||||
|
||||
# Output the full image tag for deploy step
|
||||
echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT
|
||||
|
||||
# Also update :latest for convenience (but deploy won't use it)
|
||||
echo "Copying to :latest tag"
|
||||
$CRANE copy "$IMAGE_TAG" "registry.digitalocean.com/eagle0/eagle-server:latest"
|
||||
|
||||
build-shardok:
|
||||
runs-on: self-hosted
|
||||
outputs:
|
||||
image_tag: ${{ steps.push-shardok.outputs.image_tag }}
|
||||
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: Build Shardok binary (cross-compile for Linux)
|
||||
run: |
|
||||
set -ex
|
||||
|
||||
# Step 1: Build JUST the binary with cross-compilation
|
||||
# We need --extra_toolchains to force the Linux toolchain to be used
|
||||
# because toolchains_llvm registers with dev_dependency=True
|
||||
echo "=== Building shardok-server binary for linux-x86_64 ==="
|
||||
bazel build \
|
||||
--platforms=//:linux_x86_64 \
|
||||
--extra_toolchains=@llvm_toolchain_linux//:all \
|
||||
//src/main/cpp/net/eagle0/shardok:shardok-server
|
||||
|
||||
# Step 2: Check the binary directly from bazel-bin
|
||||
# bazel-bin is a symlink that points to the correct output directory
|
||||
LINUX_BIN="bazel-bin/src/main/cpp/net/eagle0/shardok/shardok-server"
|
||||
echo "=== Checking binary at: $LINUX_BIN ==="
|
||||
|
||||
if [ ! -f "$LINUX_BIN" ]; then
|
||||
echo "ERROR: Binary not found at $LINUX_BIN"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Debug: show what bazel-bin points to
|
||||
echo "bazel-bin symlink target: $(readlink bazel-bin || echo 'not a symlink')"
|
||||
|
||||
# Step 3: Verify it's ELF (Linux) not Mach-O (macOS)
|
||||
echo "=== Verifying binary format ==="
|
||||
MAGIC=$(head -c 4 "$LINUX_BIN" | xxd -p)
|
||||
echo "Binary magic bytes: $MAGIC"
|
||||
|
||||
if [ "$MAGIC" = "7f454c46" ]; then
|
||||
echo "SUCCESS: Binary is ELF format (Linux)"
|
||||
elif [ "$MAGIC" = "cfaeedfe" ] || [ "$MAGIC" = "cffaedfe" ]; then
|
||||
echo "ERROR: Binary is Mach-O format (macOS) - cross-compilation failed!"
|
||||
echo ""
|
||||
echo "Debug info:"
|
||||
echo "- bazel-bin points to: $(readlink bazel-bin)"
|
||||
file "$LINUX_BIN" || true
|
||||
exit 1
|
||||
else
|
||||
echo "WARNING: Unknown binary format: $MAGIC"
|
||||
file "$LINUX_BIN" || true
|
||||
fi
|
||||
|
||||
- name: Build Shardok Docker image
|
||||
id: build-shardok
|
||||
run: |
|
||||
set -ex
|
||||
|
||||
# Build the OCI image with cross-compilation flags
|
||||
bazel build \
|
||||
--platforms=//:linux_x86_64 \
|
||||
--extra_toolchains=@llvm_toolchain_linux//:all \
|
||||
//ci:shardok_server_image
|
||||
|
||||
# The image is output to bazel-bin which is a symlink.
|
||||
# Resolve it now before any other bazel commands change where it points.
|
||||
IMAGE_PATH=$(readlink -f bazel-bin/ci/shardok_server_image)
|
||||
echo "Image path: $IMAGE_PATH"
|
||||
echo "image_path=$IMAGE_PATH" >> $GITHUB_OUTPUT
|
||||
|
||||
# Verify the binary inside the tar layer is ELF
|
||||
echo "=== Verifying binary in image tar ==="
|
||||
BINARY_TAR="bazel-bin/ci/shardok_binary_layer.tar"
|
||||
if [ -f "$BINARY_TAR" ]; then
|
||||
echo "Checking binary in $BINARY_TAR"
|
||||
# Extract just the first 4 bytes of the binary from the tar
|
||||
MAGIC=$(tar -xOf "$BINARY_TAR" app/shardok-server 2>/dev/null | head -c 4 | xxd -p)
|
||||
echo "Binary magic in tar: $MAGIC"
|
||||
if [ "$MAGIC" = "7f454c46" ]; then
|
||||
echo "SUCCESS: Binary in tar is ELF format (Linux)"
|
||||
else
|
||||
echo "ERROR: Binary in tar is NOT ELF format!"
|
||||
echo "This means pkg_tar is packaging the wrong binary."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "WARNING: Could not find $BINARY_TAR"
|
||||
fi
|
||||
|
||||
- name: Login to DigitalOcean Container Registry
|
||||
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
|
||||
@@ -66,7 +175,135 @@ jobs:
|
||||
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
|
||||
id: push-shardok
|
||||
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
|
||||
run: bazel run --platforms=//:linux_x86_64 //ci:shardok_server_push
|
||||
env:
|
||||
DOCKER_CONFIG: ${{ github.workspace }}/.docker
|
||||
run: |
|
||||
set -ex
|
||||
|
||||
# Use cross-compiled image path from build step
|
||||
CROSS_IMAGE="${{ steps.build-shardok.outputs.image_path }}"
|
||||
echo "Using cross-compiled image: $CROSS_IMAGE"
|
||||
|
||||
if [ -z "$CROSS_IMAGE" ] || [ ! -d "$CROSS_IMAGE" ]; then
|
||||
echo "ERROR: Cross-compiled image not found at: $CROSS_IMAGE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get crane from Eagle push target (which doesn't need cross-compilation)
|
||||
# This gives us a macOS crane binary we can actually run.
|
||||
# We can't build shardok_server_push with platform flags because it would
|
||||
# download a Linux crane that can't run on macOS.
|
||||
bazel build //ci:eagle_server_push
|
||||
|
||||
# Find the Darwin crane binary (may be a symlink)
|
||||
RUNFILES="bazel-bin/ci/push_eagle_server_push.sh.runfiles"
|
||||
CRANE=$(find "$RUNFILES" -path "*darwin*" -name crane 2>/dev/null | head -1)
|
||||
if [ -z "$CRANE" ]; then
|
||||
# Fallback to any crane
|
||||
CRANE=$(find "$RUNFILES" -name crane 2>/dev/null | head -1)
|
||||
fi
|
||||
if [ -z "$CRANE" ] || [ ! -e "$CRANE" ]; then
|
||||
echo "ERROR: crane not found. Listing runfiles:"
|
||||
find "$RUNFILES" -name crane 2>/dev/null || true
|
||||
exit 1
|
||||
fi
|
||||
echo "Using crane: $CRANE"
|
||||
|
||||
# Push the cross-compiled image with SHA tag
|
||||
GIT_SHA=$(git rev-parse --short=8 HEAD)
|
||||
IMAGE_TAG="registry.digitalocean.com/eagle0/shardok-server:${GIT_SHA}"
|
||||
echo "Pushing shardok image: $IMAGE_TAG"
|
||||
$CRANE push "$CROSS_IMAGE" "$IMAGE_TAG"
|
||||
|
||||
# Output the full image tag for deploy step
|
||||
echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT
|
||||
|
||||
# Also update :latest for convenience (but deploy won't use it)
|
||||
echo "Copying to :latest tag"
|
||||
$CRANE copy "$IMAGE_TAG" "registry.digitalocean.com/eagle0/shardok-server:latest"
|
||||
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-eagle, build-shardok]
|
||||
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
|
||||
environment: production
|
||||
env:
|
||||
EAGLE_IMAGE: ${{ needs.build-eagle.outputs.image_tag }}
|
||||
SHARDOK_IMAGE: ${{ needs.build-shardok.outputs.image_tag }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Copy config files to droplet
|
||||
uses: appleboy/scp-action@v0.1.7
|
||||
with:
|
||||
host: ${{ secrets.DO_DROPLET_IP }}
|
||||
username: deploy
|
||||
key: ${{ secrets.DO_SSH_KEY }}
|
||||
source: "docker-compose.prod.yml,nginx/nginx.conf"
|
||||
target: "/opt/eagle0"
|
||||
|
||||
- name: Deploy to production droplet
|
||||
uses: appleboy/ssh-action@v1.0.3
|
||||
with:
|
||||
host: ${{ secrets.DO_DROPLET_IP }}
|
||||
username: deploy
|
||||
key: ${{ secrets.DO_SSH_KEY }}
|
||||
script_stop: true
|
||||
envs: EAGLE_IMAGE,SHARDOK_IMAGE
|
||||
script: |
|
||||
set -x
|
||||
cd /opt/eagle0
|
||||
|
||||
# Login to registry
|
||||
echo "${{ secrets.DO_REGISTRY_TOKEN }}" | docker login registry.digitalocean.com -u "${{ secrets.DO_REGISTRY_TOKEN }}" --password-stdin
|
||||
|
||||
# Use exact image tags passed from build jobs (no :latest fallback)
|
||||
echo "Using images: $EAGLE_IMAGE, $SHARDOK_IMAGE"
|
||||
|
||||
# Clear Docker's content store cache to avoid digest mismatch errors
|
||||
# docker image rm isn't enough - need to clear buildkit and containerd cache
|
||||
echo "Clearing Docker cache to avoid digest conflicts..."
|
||||
docker builder prune -a -f 2>/dev/null || true
|
||||
docker image rm "$EAGLE_IMAGE" "$SHARDOK_IMAGE" 2>/dev/null || true
|
||||
# Nuclear option: clear all unused images/cache if digest issues persist
|
||||
docker system prune -f 2>/dev/null || true
|
||||
|
||||
# Pull fresh images with exact SHA tags
|
||||
# Use --platform to be explicit about architecture
|
||||
echo "Pulling Eagle image: $EAGLE_IMAGE"
|
||||
docker pull --platform linux/amd64 "${EAGLE_IMAGE}" || { echo "ERROR: Failed to pull eagle image"; exit 1; }
|
||||
|
||||
echo "Pulling Shardok image: $SHARDOK_IMAGE"
|
||||
docker pull --platform linux/amd64 "${SHARDOK_IMAGE}" || { echo "ERROR: Failed to pull shardok image"; exit 1; }
|
||||
|
||||
# Also pull other compose images
|
||||
docker pull nginx:alpine || true
|
||||
docker pull certbot/certbot || true
|
||||
|
||||
echo "All images pulled successfully"
|
||||
|
||||
# Force recreate containers to ensure new image is used
|
||||
docker compose -f docker-compose.prod.yml up -d --force-recreate --remove-orphans
|
||||
|
||||
# Restart nginx to pick up new container IPs
|
||||
# (nginx caches DNS at startup, so it needs restart after eagle/shardok)
|
||||
docker compose -f docker-compose.prod.yml restart nginx
|
||||
|
||||
# Wait for health checks
|
||||
sleep 10
|
||||
docker compose -f docker-compose.prod.yml ps
|
||||
|
||||
# Verify containers are using correct images
|
||||
echo "=== Verifying container image tags ==="
|
||||
docker compose -f docker-compose.prod.yml images
|
||||
|
||||
# Cleanup old images
|
||||
docker image prune -f
|
||||
|
||||
+18
-4
@@ -68,12 +68,12 @@ 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 and update sha256
|
||||
# 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 = "3812d376beba78a301eb64dfe2ad01d5896500826054ebb6aa4e00607fa0e347",
|
||||
urls = ["https://github.com/nolen777/eagle0/releases/download/sysroot-noble-v1/ubuntu_noble_amd64_sysroot.tar.xz"],
|
||||
sha256 = "a06475004fe8003ae7ccb4fe1d5511feb9b27cce4a8826eb1dfd686ed83f3dba",
|
||||
urls = ["https://eagle0-windows.sfo3.digitaloceanspaces.com/sysroot/v3/ubuntu_noble_amd64_sysroot.tar.xz"],
|
||||
)
|
||||
|
||||
#
|
||||
@@ -205,9 +205,12 @@ maven.install(
|
||||
"org.reactivestreams:reactive-streams:1.0.4",
|
||||
"javax.xml.bind:jaxb-api:2.3.1",
|
||||
|
||||
# OkHttp (for SSE with read timeout support)
|
||||
# OkHttp (for SSE with read timeout support, OAuth HTTP calls)
|
||||
"com.squareup.okhttp3:okhttp:4.12.0",
|
||||
"com.squareup.okhttp3:okhttp-sse:4.12.0",
|
||||
|
||||
# JWT (for OAuth token handling)
|
||||
"com.nimbusds:nimbus-jose-jwt:9.37.3",
|
||||
],
|
||||
duplicate_version_warning = "error",
|
||||
fail_if_repin_required = True,
|
||||
@@ -223,6 +226,7 @@ use_repo(maven, "maven", "unpinned_maven")
|
||||
#
|
||||
|
||||
http_archive = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
|
||||
http_file = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_file")
|
||||
|
||||
# GTL (for parallel_hashmap)
|
||||
GTL_VERSION = "1.2.0"
|
||||
@@ -251,6 +255,16 @@ http_archive(
|
||||
],
|
||||
)
|
||||
|
||||
# Busybox static binary for Docker health checks (provides nc, wget, etc.)
|
||||
# https://busybox.net/downloads/binaries/
|
||||
http_file(
|
||||
name = "busybox_x86_64",
|
||||
sha256 = "6e123e7f3202a8c1e9b1f94d8941580a25135382b99e8d3e34fb858bba311348",
|
||||
urls = ["https://busybox.net/downloads/binaries/1.35.0-x86_64-linux-musl/busybox"],
|
||||
downloaded_file_path = "busybox",
|
||||
executable = True,
|
||||
)
|
||||
|
||||
#
|
||||
# Toolchain Registration
|
||||
#
|
||||
|
||||
@@ -1,6 +1,22 @@
|
||||
load("@rules_oci//oci:defs.bzl", "oci_image", "oci_load", "oci_push")
|
||||
load("@rules_pkg//pkg:tar.bzl", "pkg_tar")
|
||||
|
||||
#
|
||||
# Shared utilities layer (busybox for nc, wget, etc.)
|
||||
#
|
||||
|
||||
pkg_tar(
|
||||
name = "busybox_layer",
|
||||
srcs = ["@busybox_x86_64//file"],
|
||||
package_dir = "/usr/local/bin",
|
||||
remap_paths = {
|
||||
"file/busybox": "busybox",
|
||||
},
|
||||
symlinks = {
|
||||
"/usr/local/bin/nc": "busybox",
|
||||
},
|
||||
)
|
||||
|
||||
#
|
||||
# Eagle Server Docker Image
|
||||
#
|
||||
@@ -45,6 +61,7 @@ oci_image(
|
||||
},
|
||||
exposed_ports = ["40032/tcp"],
|
||||
tars = [
|
||||
":busybox_layer",
|
||||
":eagle_server_jar_layer",
|
||||
":eagle_resources_layer",
|
||||
],
|
||||
@@ -59,6 +76,9 @@ oci_load(
|
||||
)
|
||||
|
||||
# Push to DigitalOcean Container Registry
|
||||
# Note: No remote_tags here - DigitalOcean converts OCI to Docker format,
|
||||
# changing the digest and breaking oci_push's tag-by-digest logic.
|
||||
# Tagging is handled in the CI workflow using crane copy/tag.
|
||||
oci_push(
|
||||
name = "eagle_server_push",
|
||||
image = ":eagle_server_image",
|
||||
@@ -106,6 +126,7 @@ oci_image(
|
||||
"40052/tcp",
|
||||
],
|
||||
tars = [
|
||||
":busybox_layer",
|
||||
":shardok_binary_layer",
|
||||
":shardok_resources_layer",
|
||||
":shardok_maps_layer",
|
||||
@@ -121,6 +142,9 @@ oci_load(
|
||||
)
|
||||
|
||||
# Push to DigitalOcean Container Registry
|
||||
# Note: No remote_tags here - DigitalOcean converts OCI to Docker format,
|
||||
# changing the digest and breaking oci_push's tag-by-digest logic.
|
||||
# Tagging is handled in the CI workflow using crane copy/tag.
|
||||
oci_push(
|
||||
name = "shardok_server_push",
|
||||
image = ":shardok_server_image",
|
||||
|
||||
+57
-15
@@ -1,47 +1,89 @@
|
||||
# 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
|
||||
# Docker Compose for production deployment
|
||||
#
|
||||
# Local testing:
|
||||
# Build images: bazel run //ci:eagle_server_load && bazel run //ci:shardok_server_load
|
||||
# Run: docker compose -f docker-compose.prod.yml up
|
||||
#
|
||||
# Production deployment:
|
||||
# Run: docker compose -f docker-compose.prod.yml up -d
|
||||
|
||||
services:
|
||||
eagle:
|
||||
image: eagle0/eagle-server:latest
|
||||
image: ${EAGLE_IMAGE:-registry.digitalocean.com/eagle0/eagle-server:latest}
|
||||
container_name: eagle-server
|
||||
command:
|
||||
- "--gpt-model-name"
|
||||
- "${GPT_MODEL_NAME:-gpt-5.1}"
|
||||
- "--shardok-interface-remote-address"
|
||||
- "shardok:40042"
|
||||
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"
|
||||
OPENAI_API_KEY: "${OPENAI_API_KEY:-}"
|
||||
volumes:
|
||||
# Mount saves directory for persistence
|
||||
- ./saves:/app/saves
|
||||
depends_on:
|
||||
- shardok
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "100m"
|
||||
max-file: "5"
|
||||
healthcheck:
|
||||
test: ["CMD", "nc", "-z", "localhost", "40032"]
|
||||
test: ["CMD-SHELL", "nc -z localhost 40032 || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
|
||||
shardok:
|
||||
image: eagle0/shardok-server:latest
|
||||
image: ${SHARDOK_IMAGE:-registry.digitalocean.com/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"
|
||||
SHARDOK_EAGLE_INTERFACE_ADDRESS: "0.0.0.0:40042"
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "100m"
|
||||
max-file: "5"
|
||||
healthcheck:
|
||||
test: ["CMD", "nc", "-z", "localhost", "40042"]
|
||||
test: ["CMD-SHELL", "nc -z localhost 40042 || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
container_name: nginx
|
||||
ports:
|
||||
- "443:443"
|
||||
- "80:80"
|
||||
volumes:
|
||||
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
- ./certbot/conf:/etc/letsencrypt:ro
|
||||
- ./certbot/www:/var/www/certbot:ro
|
||||
- ./auth:/etc/nginx/auth:ro
|
||||
depends_on:
|
||||
- eagle
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
|
||||
certbot:
|
||||
image: certbot/certbot
|
||||
container_name: certbot
|
||||
volumes:
|
||||
- ./certbot/conf:/etc/letsencrypt
|
||||
- ./certbot/www:/var/www/certbot
|
||||
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"
|
||||
|
||||
+39
-7
@@ -197,9 +197,9 @@ 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.**
|
||||
**Progress: 47 of 52 action files (90%) are fully protoless.**
|
||||
|
||||
The following 6 actions still have proto usage:
|
||||
The following 5 actions still have proto usage:
|
||||
|
||||
| Action | Proto Usages | Blocker | Effort |
|
||||
|--------|--------------|---------|--------|
|
||||
@@ -208,7 +208,8 @@ The following 6 actions still have proto usage:
|
||||
| `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 |
|
||||
|
||||
**Note:** `NewRoundAction` is now fully protoless after converting `ChronicleEventGenerator` to return Scala `ChronicleEvent` types directly.
|
||||
|
||||
**Deleted Dead Code:**
|
||||
- `UnaffiliatedHeroMovedAction` - Was never called; `PerformUnaffiliatedHeroesAction.heroMovedResult` constructs `ActionResultC` directly
|
||||
@@ -224,16 +225,47 @@ The following 6 actions still have proto usage:
|
||||
|-----------|-------|------------|--------|
|
||||
| `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** | | |
|
||||
| **Total Remaining** | **~2600** | | |
|
||||
|
||||
**Completed:**
|
||||
- `ChronicleEventGenerator` converted to return Scala `ChronicleEvent` types directly
|
||||
|
||||
### CommandChoiceHelpers Migration Status
|
||||
|
||||
Several command selectors have already been converted to use Scala types:
|
||||
|
||||
| File | Status | Notes |
|
||||
|------|--------|-------|
|
||||
| `AttackCommandChooser.scala` | ✅ **Protoless** | Uses Scala `GameState`, `HeroT`, `ProvinceT` |
|
||||
| `AlmsCommandSelector.scala` | ✅ **Protoless** | Uses Scala `GameState`, `HeroT`, `ProvinceT` |
|
||||
| `FoodConsumptionUtils.scala` | ✅ **Protoless** | Uses Scala `GameState`, `ProvinceT`, `RoundPhase` |
|
||||
| `MarchSuppliesHelpers.scala` | ✅ **Protoless** | Uses `BattalionT` |
|
||||
| `CombatUnitSelector.scala` | ✅ **Protoless** | Uses `HeroT`, `BattalionT`, `BattalionType` |
|
||||
| `ExpandCommandSelector.scala` | ✅ **Protoless** | Uses Scala `GameState`, `ProvinceT`, `FactionT` |
|
||||
| `ImproveCommandSelector.scala` | ✅ **Protoless** | Uses Scala `GameState`, `ProvinceT`, `HeroT` |
|
||||
| `OrganizeCommandSelector.scala` | ✅ **Protoless** | Uses Scala `GameState`, `BattalionT`, `BattalionType` |
|
||||
| `RansomOfferHelpers.scala` | ✅ **Protoless** | Uses Scala `GameState`, `FactionT` |
|
||||
| `CommandChoiceHelpers.scala` | ❌ Proto | Main entry point, converts to Scala when calling converted selectors |
|
||||
| `ProvinceGoldSurplusCalculator.scala` | **Partial** | Has both Scala and proto overloads |
|
||||
| Other selectors | ❌ Proto | Various proto dependencies |
|
||||
|
||||
**Pattern**: `CommandChoiceHelpers` currently uses `GameStateConverter.fromProto(gameState)` when calling already-converted selectors like `AlmsCommandSelector` and `AttackCommandChooser`. This allows incremental migration.
|
||||
|
||||
**Next Steps**:
|
||||
1. ~~Convert `ExpandCommandSelector` to Scala types~~ ✅ Done
|
||||
2. ~~Convert `ImproveCommandSelector` to Scala types~~ ✅ Done
|
||||
3. ~~Convert `OrganizeCommandSelector` to Scala types~~ ✅ Done (PR #4812)
|
||||
4. ~~Convert `RansomOfferHelpers` to Scala types~~ ✅ Done (PR #4821)
|
||||
5. Convert remaining selectors one at a time
|
||||
6. Update `CommandChoiceHelpers` to accept Scala `GameState` once all selectors are converted
|
||||
|
||||
### Progress Summary
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Action files fully protoless | 46 / 52 (88%) |
|
||||
| Proto usages in remaining actions | 33 total |
|
||||
| Action files fully protoless | 47 / 52 (90%) |
|
||||
| Proto usages in remaining actions | 32 total |
|
||||
| Biggest blocker | `ResolveBattleAction` (24 usages) |
|
||||
| Second biggest blocker | `CommandChoiceHelpers` (blocks 4 actions) |
|
||||
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
# Discord + Google OAuth Implementation Plan
|
||||
|
||||
## Overview
|
||||
|
||||
Replace HTTP Basic Auth with OAuth 2.0 (Discord + Google) for Eagle0. Users authenticate via system browser, receive JWT tokens, and choose their own display names.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Unity Client Eagle Server
|
||||
| |
|
||||
| 1. Click "Login with Discord/Google" |
|
||||
| -------------------------------------------------> |
|
||||
| GetOAuthUrl(provider) -> auth_url + state |
|
||||
| |
|
||||
| 2. Open system browser -> OAuth consent |
|
||||
| 3. User authenticates with provider |
|
||||
| 4. Redirect to eagle0://auth/callback?code=xxx |
|
||||
| |
|
||||
| 5. ExchangeCode(code, state) |
|
||||
| -------------------------------------------------> |
|
||||
| Exchange code with provider |
|
||||
| Fetch user info (id, email, avatar) |
|
||||
| Create/update user record |
|
||||
| Issue JWT + refresh token |
|
||||
| <------------------------------------------------- |
|
||||
| (jwt, refresh_token, user_info, is_new_user) |
|
||||
| |
|
||||
| 6. [If new user] SetDisplayName(name) |
|
||||
| -------------------------------------------------> |
|
||||
| |
|
||||
| 7. Subsequent gRPC calls |
|
||||
| Authorization: Bearer <jwt> |
|
||||
| -------------------------------------------------> |
|
||||
```
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
| Decision | Choice | Rationale |
|
||||
|----------|--------|-----------|
|
||||
| OAuth flow | System browser + deep link | Secure, supports password managers |
|
||||
| Code exchange | Eagle server directly | No separate auth service needed |
|
||||
| JWT signing | RS256 (asymmetric) | Future flexibility for token verification |
|
||||
| User storage | Protobuf file via Persister | Consistent with existing patterns |
|
||||
| Token expiry | 7-day access, 30-day refresh | Balance security and gaming UX |
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1: Proto Definitions & Infrastructure
|
||||
|
||||
**New files:**
|
||||
- `src/main/protobuf/net/eagle0/eagle/api/auth.proto` - Auth API messages
|
||||
- `src/main/protobuf/net/eagle0/eagle/internal/user.proto` - User storage schema
|
||||
|
||||
**Key proto messages:**
|
||||
```protobuf
|
||||
// API
|
||||
GetOAuthUrlRequest/Response // Get OAuth URL to open in browser
|
||||
ExchangeCodeRequest/Response // Exchange auth code for JWT
|
||||
SetDisplayNameRequest/Response // Set user's display name
|
||||
RefreshTokenRequest/Response // Refresh expired access token
|
||||
|
||||
// Internal storage
|
||||
User // user_id, display_name, oauth_identities
|
||||
UserDatabase // All users + indexes for lookup
|
||||
```
|
||||
|
||||
### Phase 2: Eagle Server Auth Services
|
||||
|
||||
**New Scala files:**
|
||||
- `src/main/scala/net/eagle0/eagle/auth/OAuthConfig.scala` - Discord/Google config from env vars
|
||||
- `src/main/scala/net/eagle0/eagle/auth/JwtService.scala` - JWT creation/validation (RS256)
|
||||
- `src/main/scala/net/eagle0/eagle/auth/UserService.scala` - User CRUD, display name validation
|
||||
- `src/main/scala/net/eagle0/eagle/auth/OAuthService.scala` - OAuth code exchange
|
||||
- `src/main/scala/net/eagle0/eagle/service/AuthServiceImpl.scala` - gRPC service implementation
|
||||
|
||||
**Modify:**
|
||||
- `src/main/scala/net/eagle0/eagle/service/AuthorizationInterceptor.scala`
|
||||
- Replace Basic Auth parsing with JWT validation
|
||||
- Skip auth for public endpoints (GetOAuthUrl, ExchangeCode, RefreshToken)
|
||||
- `src/main/scala/net/eagle0/eagle/service/AuthorizationUtils.scala`
|
||||
- Change context keys from `userName` to `userId` + `displayName`
|
||||
- `src/main/scala/net/eagle0/eagle/service/Main.scala`
|
||||
- Wire up new auth services and JWT key loading
|
||||
|
||||
### Phase 3: Unity Client OAuth Flow
|
||||
|
||||
**New C# files:**
|
||||
- `Assets/Auth/OAuthManager.cs` - OAuth flow + deep link handling
|
||||
- `Assets/Auth/TokenStorage.cs` - Secure token persistence
|
||||
- `Assets/Auth/AuthClient.cs` - gRPC client for auth service
|
||||
|
||||
**Modify:**
|
||||
- `Assets/EagleConnection.cs`
|
||||
- Replace `AuthInterceptor` (Basic Auth) with `JwtAuthInterceptor` (Bearer token)
|
||||
- `Assets/ConnectionHandler/ConnectionHandler.cs`
|
||||
- Replace username/password UI with Discord/Google login buttons
|
||||
- Add display name setup flow for new users
|
||||
|
||||
### Phase 4: Platform Configuration
|
||||
|
||||
**Deep link registration:**
|
||||
- iOS: Add `eagle0://` to CFBundleURLSchemes in Info.plist
|
||||
- Android: Add intent-filter for `eagle0://auth` in AndroidManifest.xml
|
||||
- Desktop: Register URL scheme (Windows registry / macOS plist)
|
||||
|
||||
**OAuth provider setup:**
|
||||
1. Discord Developer Portal: Create app, add redirect URI `eagle0://auth/callback`
|
||||
2. Google Cloud Console: Create OAuth client, add redirect URI
|
||||
|
||||
**Environment variables (server):**
|
||||
```
|
||||
DISCORD_CLIENT_ID
|
||||
DISCORD_CLIENT_SECRET
|
||||
GOOGLE_CLIENT_ID
|
||||
GOOGLE_CLIENT_SECRET
|
||||
JWT_PRIVATE_KEY_PATH
|
||||
JWT_PUBLIC_KEY_PATH
|
||||
```
|
||||
|
||||
### Phase 5: Testing
|
||||
|
||||
**Unit tests:**
|
||||
- `JwtServiceSpec.scala` - Token creation/validation
|
||||
- `UserServiceSpec.scala` - Display name validation, uniqueness
|
||||
- `OAuthServiceSpec.scala` - OAuth flow with mocked providers
|
||||
|
||||
**Integration tests:**
|
||||
- Full OAuth flow with mock provider
|
||||
- JWT validation in AuthorizationInterceptor
|
||||
- gRPC calls with valid/invalid tokens
|
||||
|
||||
**Manual testing:**
|
||||
- [ ] Discord login (Windows, macOS)
|
||||
- [ ] Google login (Windows, macOS)
|
||||
- [ ] Deep link callback works
|
||||
- [ ] Display name validation
|
||||
- [ ] Session persistence across restarts
|
||||
- [ ] Token refresh
|
||||
|
||||
## Files Summary
|
||||
|
||||
### Create
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `src/main/protobuf/net/eagle0/eagle/api/auth.proto` | Auth API definitions |
|
||||
| `src/main/protobuf/net/eagle0/eagle/internal/user.proto` | User storage schema |
|
||||
| `src/main/scala/net/eagle0/eagle/auth/OAuthConfig.scala` | Provider config |
|
||||
| `src/main/scala/net/eagle0/eagle/auth/JwtService.scala` | JWT handling |
|
||||
| `src/main/scala/net/eagle0/eagle/auth/UserService.scala` | User management |
|
||||
| `src/main/scala/net/eagle0/eagle/auth/OAuthService.scala` | OAuth flow |
|
||||
| `src/main/scala/net/eagle0/eagle/service/AuthServiceImpl.scala` | gRPC service |
|
||||
| `Assets/Auth/OAuthManager.cs` | Unity OAuth manager |
|
||||
| `Assets/Auth/TokenStorage.cs` | Token storage |
|
||||
| `Assets/Auth/AuthClient.cs` | Auth gRPC client |
|
||||
|
||||
### Modify
|
||||
| File | Changes |
|
||||
|------|---------|
|
||||
| `AuthorizationInterceptor.scala` | Basic Auth -> JWT validation |
|
||||
| `AuthorizationUtils.scala` | userName -> userId + displayName |
|
||||
| `Main.scala` | Wire auth services |
|
||||
| `EagleConnection.cs` | AuthInterceptor -> JwtAuthInterceptor |
|
||||
| `ConnectionHandler.cs` | Login UI -> OAuth buttons + display name |
|
||||
|
||||
### Delete
|
||||
- nginx htpasswd configuration (no longer needed)
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **State parameter** - CSRF protection in OAuth flow
|
||||
2. **PKCE** - Consider adding for mobile (enhancement)
|
||||
3. **Secure storage** - Use Keychain (iOS) / Keystore (Android) for tokens
|
||||
4. **Token refresh** - 7-day access tokens with 30-day refresh
|
||||
5. **Rate limiting** - Limit login attempts per IP
|
||||
|
||||
## Dependencies to Add
|
||||
|
||||
**Scala (MODULE.bazel):**
|
||||
- JWT library (e.g., `jwt-scala` or `nimbus-jose-jwt`)
|
||||
- HTTP client (e.g., `sttp` for OAuth requests)
|
||||
|
||||
**Unity:**
|
||||
- Deep linking is built-in (Unity 2021+)
|
||||
- No additional packages required
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
Keep Basic Auth code in a feature branch. Both auth methods can coexist during transition via feature flag if needed.
|
||||
@@ -348,6 +348,68 @@ services:
|
||||
|
||||
---
|
||||
|
||||
## Docker Networking & Resource Configuration
|
||||
|
||||
### Container Network Binding
|
||||
|
||||
In Docker, each container has its own network namespace. When a server binds to `localhost` (127.0.0.1), it only accepts connections from within the same container. Other containers on the Docker bridge network cannot connect, even if they can resolve the hostname.
|
||||
|
||||
**Shardok must bind to `0.0.0.0:40042`** to accept connections from Eagle running in a separate container. This is configured in `ServerConfiguration.cpp`:
|
||||
|
||||
```cpp
|
||||
// Default to 0.0.0.0 for container networking (accepts connections from any interface)
|
||||
{ServerConfiguration::kEagleInterfaceGrpcAddress, "0.0.0.0:40042"}
|
||||
```
|
||||
|
||||
This still works on the local Mac because `0.0.0.0` means "all interfaces", which includes the loopback interface that localhost uses.
|
||||
|
||||
### gRPC Plaintext for Internal Communication
|
||||
|
||||
By default, gRPC's `ManagedChannelBuilder` attempts TLS connections. Since Shardok runs in plaintext mode, Eagle must use `.usePlaintext()` for internal container-to-container communication:
|
||||
|
||||
```scala
|
||||
// ServerSetupHelpers.scala
|
||||
val channelBuilder = ManagedChannelBuilder
|
||||
.forAddress(shardokInterfaceAddress, shardokInterfacePort)
|
||||
.usePlaintext() // Required for internal Docker communication
|
||||
```
|
||||
|
||||
TLS termination happens at nginx for external clients. Internal traffic between Eagle and Shardok stays within the Docker network and doesn't need encryption.
|
||||
|
||||
**Future consideration:** If Shardok moves to a separate droplet, traffic would traverse the network. Options:
|
||||
1. Enable TLS between Eagle and Shardok (configure Shardok with certificates)
|
||||
2. Use DigitalOcean VPC (private network, still unencrypted but isolated)
|
||||
3. Use WireGuard/VPN tunnel between hosts
|
||||
|
||||
### Bazel Runfiles in Docker
|
||||
|
||||
Bazel's runfiles system locates resources relative to the executable using a manifest or directory structure that Bazel sets up at runtime. This doesn't exist in Docker containers.
|
||||
|
||||
**Solution:** Environment variable fallbacks in `FilesystemUtils.cpp`:
|
||||
|
||||
```cpp
|
||||
// Check for Docker deployment env vars first
|
||||
const char* resourcesPath = getenv("SHARDOK_RESOURCES_PATH");
|
||||
if (resourcesPath != nullptr) {
|
||||
return string(resourcesPath) + "/";
|
||||
}
|
||||
// Fall back to Bazel runfiles for local development
|
||||
return rLocation + "/src/main/resources/net/eagle0/shardok/";
|
||||
```
|
||||
|
||||
The Docker Compose configuration sets these:
|
||||
```yaml
|
||||
environment:
|
||||
SHARDOK_RESOURCES_PATH: "/app/resources"
|
||||
SHARDOK_MAPS_PATH: "/app/resources/maps"
|
||||
```
|
||||
|
||||
This allows the same binary to work in both environments:
|
||||
- **Local dev:** No env var → uses Bazel runfiles
|
||||
- **Docker:** Env var set → uses `/app/resources/` directly
|
||||
|
||||
---
|
||||
|
||||
## Build Pipeline
|
||||
|
||||
### Build Artifacts
|
||||
|
||||
+54
-2
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"__AUTOGENERATED_FILE_DO_NOT_MODIFY_THIS_FILE_MANUALLY": "THERE_IS_NO_DATA_ONLY_ZUUL",
|
||||
"__INPUT_ARTIFACTS_HASH": 289080209,
|
||||
"__RESOLVED_ARTIFACTS_HASH": -131178107,
|
||||
"__INPUT_ARTIFACTS_HASH": -1064460283,
|
||||
"__RESOLVED_ARTIFACTS_HASH": -1574144850,
|
||||
"conflict_resolution": {
|
||||
"com.google.guava:failureaccess:1.0.1": "com.google.guava:failureaccess:1.0.2",
|
||||
"com.squareup.okio:okio:2.10.0": "com.squareup.okio:okio:3.6.0",
|
||||
@@ -48,6 +48,12 @@
|
||||
},
|
||||
"version": "2.12.7"
|
||||
},
|
||||
"com.github.stephenc.jcip:jcip-annotations": {
|
||||
"shasums": {
|
||||
"jar": "4fccff8382aafc589962c4edb262f6aa595e34f1e11e61057d1c6a96e8fc7323"
|
||||
},
|
||||
"version": "1.0-1"
|
||||
},
|
||||
"com.google.android:annotations": {
|
||||
"shasums": {
|
||||
"jar": "ba734e1e84c09d615af6a09d33034b4f0442f8772dec120efb376d86a565ae15"
|
||||
@@ -156,6 +162,12 @@
|
||||
},
|
||||
"version": "1.4.2"
|
||||
},
|
||||
"com.nimbusds:nimbus-jose-jwt": {
|
||||
"shasums": {
|
||||
"jar": "12ae4a3a260095d7aeba2adea7ae396e8b9570db8b7b409e09a824c219cc0444"
|
||||
},
|
||||
"version": "9.37.3"
|
||||
},
|
||||
"com.squareup.okhttp3:okhttp": {
|
||||
"shasums": {
|
||||
"jar": "b1050081b14bb7a3a7e55a4d3ef01b5dcfabc453b4573a4fc019767191d5f4e0"
|
||||
@@ -810,6 +822,9 @@
|
||||
"org.checkerframework:checker-qual",
|
||||
"org.ow2.asm:asm"
|
||||
],
|
||||
"com.nimbusds:nimbus-jose-jwt": [
|
||||
"com.github.stephenc.jcip:jcip-annotations"
|
||||
],
|
||||
"com.squareup.okhttp3:okhttp": [
|
||||
"com.squareup.okio:okio",
|
||||
"org.jetbrains.kotlin:kotlin-stdlib-jdk8"
|
||||
@@ -1355,6 +1370,9 @@
|
||||
"com.fasterxml.jackson.databind.type",
|
||||
"com.fasterxml.jackson.databind.util"
|
||||
],
|
||||
"com.github.stephenc.jcip:jcip-annotations": [
|
||||
"net.jcip.annotations"
|
||||
],
|
||||
"com.google.android:annotations": [
|
||||
"android.annotation"
|
||||
],
|
||||
@@ -1500,6 +1518,38 @@
|
||||
"com.google.truth:truth": [
|
||||
"com.google.common.truth"
|
||||
],
|
||||
"com.nimbusds:nimbus-jose-jwt": [
|
||||
"com.nimbusds.jose",
|
||||
"com.nimbusds.jose.crypto",
|
||||
"com.nimbusds.jose.crypto.bc",
|
||||
"com.nimbusds.jose.crypto.factories",
|
||||
"com.nimbusds.jose.crypto.impl",
|
||||
"com.nimbusds.jose.crypto.opts",
|
||||
"com.nimbusds.jose.crypto.utils",
|
||||
"com.nimbusds.jose.jca",
|
||||
"com.nimbusds.jose.jwk",
|
||||
"com.nimbusds.jose.jwk.gen",
|
||||
"com.nimbusds.jose.jwk.source",
|
||||
"com.nimbusds.jose.mint",
|
||||
"com.nimbusds.jose.proc",
|
||||
"com.nimbusds.jose.produce",
|
||||
"com.nimbusds.jose.shaded.gson",
|
||||
"com.nimbusds.jose.shaded.gson.annotations",
|
||||
"com.nimbusds.jose.shaded.gson.internal",
|
||||
"com.nimbusds.jose.shaded.gson.internal.bind",
|
||||
"com.nimbusds.jose.shaded.gson.internal.bind.util",
|
||||
"com.nimbusds.jose.shaded.gson.internal.reflect",
|
||||
"com.nimbusds.jose.shaded.gson.internal.sql",
|
||||
"com.nimbusds.jose.shaded.gson.reflect",
|
||||
"com.nimbusds.jose.shaded.gson.stream",
|
||||
"com.nimbusds.jose.util",
|
||||
"com.nimbusds.jose.util.cache",
|
||||
"com.nimbusds.jose.util.events",
|
||||
"com.nimbusds.jose.util.health",
|
||||
"com.nimbusds.jwt",
|
||||
"com.nimbusds.jwt.proc",
|
||||
"com.nimbusds.jwt.util"
|
||||
],
|
||||
"com.squareup.okhttp3:okhttp": [
|
||||
"okhttp3",
|
||||
"okhttp3.internal",
|
||||
@@ -2337,6 +2387,7 @@
|
||||
"com.fasterxml.jackson.core:jackson-annotations",
|
||||
"com.fasterxml.jackson.core:jackson-core",
|
||||
"com.fasterxml.jackson.core:jackson-databind",
|
||||
"com.github.stephenc.jcip:jcip-annotations",
|
||||
"com.google.android:annotations",
|
||||
"com.google.api.grpc:proto-google-common-protos",
|
||||
"com.google.auth:google-auth-library-credentials",
|
||||
@@ -2355,6 +2406,7 @@
|
||||
"com.google.protobuf:protobuf-java",
|
||||
"com.google.re2j:re2j",
|
||||
"com.google.truth:truth",
|
||||
"com.nimbusds:nimbus-jose-jwt",
|
||||
"com.squareup.okhttp3:okhttp",
|
||||
"com.squareup.okhttp3:okhttp-sse",
|
||||
"com.squareup.okhttp:okhttp",
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
# Logging
|
||||
log_format grpc_json escape=json '{'
|
||||
'"time":"$time_iso8601",'
|
||||
'"client":"$remote_addr",'
|
||||
'"uri":"$uri",'
|
||||
'"status":$status,'
|
||||
'"grpc_status":"$sent_http_grpc_status",'
|
||||
'"request_time":$request_time,'
|
||||
'"upstream_time":"$upstream_response_time"'
|
||||
'}';
|
||||
|
||||
access_log /var/log/nginx/access.log grpc_json;
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
|
||||
# Rate limiting zone
|
||||
limit_req_zone $binary_remote_addr zone=grpc_limit:10m rate=100r/s;
|
||||
|
||||
# Docker DNS resolver - re-resolve hostnames every 10s
|
||||
# This prevents stale IP caching when containers restart
|
||||
resolver 127.0.0.11 valid=10s ipv6=off;
|
||||
|
||||
# Upstream for Eagle gRPC server
|
||||
upstream eagle_grpc {
|
||||
server eagle:40032;
|
||||
keepalive 100;
|
||||
}
|
||||
|
||||
# HTTP server for Let's Encrypt challenge and redirect
|
||||
server {
|
||||
listen 80;
|
||||
server_name prod.eagle0.net;
|
||||
|
||||
# Let's Encrypt challenge
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/certbot;
|
||||
}
|
||||
|
||||
# Redirect all other HTTP to HTTPS
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
# HTTPS server for gRPC
|
||||
server {
|
||||
listen 443 ssl;
|
||||
http2 on;
|
||||
server_name prod.eagle0.net;
|
||||
|
||||
# SSL certificates (managed by certbot)
|
||||
ssl_certificate /etc/letsencrypt/live/prod.eagle0.net/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/prod.eagle0.net/privkey.pem;
|
||||
|
||||
# SSL configuration
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
|
||||
ssl_prefer_server_ciphers off;
|
||||
ssl_session_timeout 1d;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_session_tickets off;
|
||||
|
||||
# gRPC proxy for Eagle service
|
||||
location /net.eagle0.eagle.api.Eagle {
|
||||
# Rate limiting
|
||||
limit_req zone=grpc_limit burst=50 nodelay;
|
||||
|
||||
# gRPC proxy
|
||||
grpc_pass grpc://eagle_grpc;
|
||||
|
||||
# Timeouts for long-running streams
|
||||
grpc_read_timeout 1200s;
|
||||
grpc_send_timeout 1200s;
|
||||
grpc_socket_keepalive on;
|
||||
|
||||
# Error handling
|
||||
error_page 502 = /error502grpc;
|
||||
}
|
||||
|
||||
# Health check endpoint
|
||||
location /health {
|
||||
access_log off;
|
||||
return 200 "OK\n";
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
|
||||
# gRPC error handling
|
||||
location = /error502grpc {
|
||||
internal;
|
||||
default_type application/grpc;
|
||||
add_header grpc-status 14;
|
||||
add_header grpc-message "unavailable";
|
||||
return 204;
|
||||
}
|
||||
}
|
||||
}
|
||||
+268
-30
@@ -3,10 +3,25 @@
|
||||
# generate_changelog.sh
|
||||
#
|
||||
# Generates a weekly changelog from merged PRs, uses Claude to create a synopsis,
|
||||
# and opens an email draft in Mac Mail.
|
||||
# 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.
|
||||
|
||||
@@ -19,6 +34,56 @@ 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
|
||||
@@ -118,23 +183,30 @@ generate_synopsis() {
|
||||
|
||||
# Create a prompt file to avoid shell escaping issues
|
||||
local prompt_file="/tmp/eagle0_prompt_$$.txt"
|
||||
cat > "$prompt_file" <<'PROMPT_HEADER'
|
||||
# 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.
|
||||
|
||||
Guidelines:
|
||||
- Group related changes together under clear headings (ALL CAPS with a blank line before)
|
||||
- Use "- " bullet points for individual changes
|
||||
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
|
||||
- Aim for a summary that takes 1-2 minutes to read
|
||||
- Don't include PR numbers in the summary - focus on what changed and why it matters
|
||||
- Don't include PR numbers in the synopsis - focus on what changed and why it matters
|
||||
|
||||
IMPORTANT: Output plain text only. No markdown, no HTML. Use simple formatting:
|
||||
- ALL CAPS for section headers
|
||||
- Dashes (-) for bullet points
|
||||
- Blank lines between sections
|
||||
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:
|
||||
|
||||
@@ -144,32 +216,161 @@ PROMPT_HEADER
|
||||
echo "" >> "$prompt_file"
|
||||
echo "Generate the synopsis now:" >> "$prompt_file"
|
||||
|
||||
# Use Claude CLI to generate the synopsis (read from stdin)
|
||||
cat "$prompt_file" | claude --print > "$output_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"
|
||||
|
||||
rm -f "$prompt_file"
|
||||
# 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"
|
||||
}
|
||||
|
||||
# Create email in Mac Mail
|
||||
create_email() {
|
||||
# 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 since_date="$2"
|
||||
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 "Creating email draft in Mac Mail..."
|
||||
echo "Sending email via Fastmail JMAP API..."
|
||||
|
||||
osascript <<EOF
|
||||
set textContent to read POSIX file "$synopsis_file" as «class utf8»
|
||||
# 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")
|
||||
|
||||
tell application "Mail"
|
||||
set newMessage to make new outgoing message with properties {subject:"$subject", content:textContent, visible:true}
|
||||
activate
|
||||
end tell
|
||||
EOF
|
||||
# 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
|
||||
|
||||
echo "Email draft created"
|
||||
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
|
||||
@@ -192,13 +393,34 @@ 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).txt"
|
||||
local synopsis_file="/tmp/eagle0_synopsis_$(date +%s).html"
|
||||
|
||||
# Fetch PRs
|
||||
if ! fetch_merged_prs "$cutoff_date" "$pr_file"; then
|
||||
@@ -217,10 +439,26 @@ main() {
|
||||
echo "=== DRY RUN - Synopsis content ==="
|
||||
cat "$synopsis_file"
|
||||
echo ""
|
||||
echo "=== DRY RUN - Skipping email creation and tag update ==="
|
||||
echo "=== DRY RUN - Skipping email send and tag update ==="
|
||||
else
|
||||
# Create email
|
||||
create_email "$synopsis_file" "$cutoff_date"
|
||||
# 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
|
||||
|
||||
Executable
+149
@@ -0,0 +1,149 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Setup script for Eagle0 production droplet
|
||||
# Run this on a fresh DigitalOcean droplet (Ubuntu 24.04)
|
||||
#
|
||||
# Usage: curl -sSL https://raw.githubusercontent.com/nolen777/eagle0/main/scripts/setup_droplet.sh | sudo bash
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
DOMAIN="${DOMAIN:-eagle0.net}"
|
||||
DEPLOY_USER="${DEPLOY_USER:-deploy}"
|
||||
APP_DIR="/opt/eagle0"
|
||||
|
||||
echo "=== Eagle0 Production Server Setup ==="
|
||||
echo "Domain: ${DOMAIN}"
|
||||
echo "Deploy user: ${DEPLOY_USER}"
|
||||
echo ""
|
||||
|
||||
# Check if running as root
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
echo "This script must be run as root (use sudo)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=== Updating system ==="
|
||||
apt-get update
|
||||
apt-get upgrade -y
|
||||
|
||||
echo "=== Installing Docker ==="
|
||||
if ! command -v docker &> /dev/null; then
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
systemctl enable docker
|
||||
systemctl start docker
|
||||
else
|
||||
echo "Docker already installed"
|
||||
fi
|
||||
|
||||
echo "=== Installing Docker Compose plugin ==="
|
||||
apt-get install -y docker-compose-plugin
|
||||
|
||||
echo "=== Installing additional utilities ==="
|
||||
apt-get install -y \
|
||||
curl \
|
||||
wget \
|
||||
git \
|
||||
netcat-openbsd \
|
||||
jq \
|
||||
htop \
|
||||
unattended-upgrades
|
||||
|
||||
echo "=== Configuring automatic security updates ==="
|
||||
cat > /etc/apt/apt.conf.d/20auto-upgrades << 'EOF'
|
||||
APT::Periodic::Update-Package-Lists "1";
|
||||
APT::Periodic::Unattended-Upgrade "1";
|
||||
APT::Periodic::AutocleanInterval "7";
|
||||
EOF
|
||||
|
||||
echo "=== Creating deploy user ==="
|
||||
if ! id "${DEPLOY_USER}" &>/dev/null; then
|
||||
useradd -m -s /bin/bash -G docker "${DEPLOY_USER}"
|
||||
mkdir -p "/home/${DEPLOY_USER}/.ssh"
|
||||
chmod 700 "/home/${DEPLOY_USER}/.ssh"
|
||||
chown -R "${DEPLOY_USER}:${DEPLOY_USER}" "/home/${DEPLOY_USER}/.ssh"
|
||||
echo ""
|
||||
echo "*** IMPORTANT: Add your SSH public key to /home/${DEPLOY_USER}/.ssh/authorized_keys ***"
|
||||
echo ""
|
||||
else
|
||||
echo "User ${DEPLOY_USER} already exists"
|
||||
# Ensure user is in docker group
|
||||
usermod -aG docker "${DEPLOY_USER}"
|
||||
fi
|
||||
|
||||
echo "=== Creating application directory ==="
|
||||
mkdir -p "${APP_DIR}"/{nginx,certbot/conf,certbot/www,saves}
|
||||
chown -R "${DEPLOY_USER}:${DEPLOY_USER}" "${APP_DIR}"
|
||||
|
||||
echo "=== Configuring Docker registry authentication ==="
|
||||
echo ""
|
||||
echo "*** IMPORTANT: Run the following command to authenticate with DigitalOcean Container Registry: ***"
|
||||
echo " docker login registry.digitalocean.com"
|
||||
echo ""
|
||||
|
||||
echo "=== Creating systemd service ==="
|
||||
cat > /etc/systemd/system/eagle0.service << EOF
|
||||
[Unit]
|
||||
Description=Eagle0 Game Servers
|
||||
Requires=docker.service
|
||||
After=docker.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
WorkingDirectory=${APP_DIR}
|
||||
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_USER}
|
||||
Group=${DEPLOY_USER}
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable eagle0
|
||||
|
||||
echo "=== Configuring firewall (UFW) ==="
|
||||
if ! command -v ufw &> /dev/null; then
|
||||
apt-get install -y ufw
|
||||
fi
|
||||
|
||||
ufw default deny incoming
|
||||
ufw default allow outgoing
|
||||
ufw allow ssh
|
||||
ufw allow 80/tcp
|
||||
ufw allow 443/tcp
|
||||
ufw --force enable
|
||||
|
||||
echo "=== Setting up log rotation ==="
|
||||
cat > /etc/logrotate.d/eagle0 << EOF
|
||||
/var/log/eagle0/*.log {
|
||||
daily
|
||||
missingok
|
||||
rotate 14
|
||||
compress
|
||||
delaycompress
|
||||
notifempty
|
||||
create 0640 ${DEPLOY_USER} ${DEPLOY_USER}
|
||||
sharedscripts
|
||||
}
|
||||
EOF
|
||||
|
||||
mkdir -p /var/log/eagle0
|
||||
chown "${DEPLOY_USER}:${DEPLOY_USER}" /var/log/eagle0
|
||||
|
||||
echo ""
|
||||
echo "=== Setup Complete ==="
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo "1. Add SSH public key to /home/${DEPLOY_USER}/.ssh/authorized_keys"
|
||||
echo "2. Copy docker-compose.prod.yml to ${APP_DIR}/"
|
||||
echo "3. Copy nginx/nginx.conf to ${APP_DIR}/nginx/"
|
||||
echo "4. Create .env file in ${APP_DIR}/ with OPENAI_API_KEY"
|
||||
echo "5. Run: docker login registry.digitalocean.com"
|
||||
echo "6. Get SSL certificate: (see init_ssl.sh)"
|
||||
echo "7. Start services: systemctl start eagle0"
|
||||
echo ""
|
||||
echo "Server IP: $(curl -s ifconfig.me)"
|
||||
echo ""
|
||||
@@ -26,6 +26,13 @@ namespace fs = std::filesystem;
|
||||
static string rLocation;
|
||||
|
||||
auto rloc(const string& execPath) -> string {
|
||||
// First check for environment variable override for Docker deployment
|
||||
const char* resourcesPath = getenv("SHARDOK_RESOURCES_PATH");
|
||||
if (resourcesPath != nullptr) {
|
||||
return ""; // Return empty so StaticShardokFilesDirectory uses env var directly
|
||||
}
|
||||
|
||||
// Fall back to Bazel runfiles for development
|
||||
string error;
|
||||
const std::unique_ptr<Runfiles> runfiles(Runfiles::Create(execPath, &error));
|
||||
|
||||
@@ -58,10 +65,14 @@ auto FilesystemUtils::FileExistsAtPath(const string& path) -> bool { return fs::
|
||||
auto FilesystemUtils::StaticEagle0FilesDirectory() -> string { return "/usr/local/share/eagle0/"; }
|
||||
|
||||
auto FilesystemUtils::StaticShardokFilesDirectory() -> string {
|
||||
const char* resourcesPath = getenv("SHARDOK_RESOURCES_PATH");
|
||||
if (resourcesPath != nullptr) { return string(resourcesPath) + "/"; }
|
||||
return rLocation + "/src/main/resources/net/eagle0/shardok/";
|
||||
}
|
||||
|
||||
auto FilesystemUtils::MapFilesDirectory() -> string {
|
||||
const char* mapsPath = getenv("SHARDOK_MAPS_PATH");
|
||||
if (mapsPath != nullptr) { return string(mapsPath) + "/"; }
|
||||
return StaticShardokFilesDirectory() + "maps/";
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <bit>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
|
||||
#define ITERABLE_BITSET_INDEX_CHECKS false
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "MapUtils.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <stdexcept>
|
||||
|
||||
static inline std::string StringForKey(
|
||||
const std::unordered_map<std::string, std::string>& map,
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#ifndef byte_vector_h
|
||||
#define byte_vector_h
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "TranspositionTable.hpp"
|
||||
|
||||
#include <cinttypes>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
@@ -101,10 +102,10 @@ void TranspositionTable::clear() {
|
||||
|
||||
void TranspositionTable::printStats() const {
|
||||
printf("TranspositionTable Stats:\n");
|
||||
printf(" Probes: %llu\n", stats.probes.load());
|
||||
printf(" Hits: %llu (%.1f%%)\n", stats.hits.load(), stats.hitRate());
|
||||
printf(" Stores: %llu\n", stats.stores.load());
|
||||
printf(" Collisions: %llu\n", stats.collisions.load());
|
||||
printf(" Probes: %" PRIu64 "\n", stats.probes.load());
|
||||
printf(" Hits: %" PRIu64 " (%.1f%%)\n", stats.hits.load(), stats.hitRate());
|
||||
printf(" Stores: %" PRIu64 "\n", stats.stores.load());
|
||||
printf(" Collisions: %" PRIu64 "\n", stats.collisions.load());
|
||||
printf(" Table size: %zu entries (%.1f MB)\n",
|
||||
TABLE_SIZE,
|
||||
(TABLE_SIZE * sizeof(TTEntry)) / (1024.0 * 1024.0));
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#include "ServerConfiguration.hpp"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
@@ -30,9 +31,16 @@ using std::unordered_map;
|
||||
unordered_map<string, string> DefaultValues();
|
||||
|
||||
unordered_map<string, string> DefaultValues() {
|
||||
// Check environment variables for Docker deployment
|
||||
// Docker containers should set SHARDOK_EAGLE_INTERFACE_ADDRESS=0.0.0.0:40042
|
||||
// to listen on all interfaces for container networking
|
||||
const char* eagleInterfaceAddr = getenv("SHARDOK_EAGLE_INTERFACE_ADDRESS");
|
||||
const char* shardokAddr = getenv("SHARDOK_GRPC_ADDRESS");
|
||||
|
||||
return unordered_map<string, string>{
|
||||
{ServerConfiguration::kShardokGrpcAddress, "localhost"},
|
||||
{ServerConfiguration::kEagleInterfaceGrpcAddress, "localhost"}};
|
||||
{ServerConfiguration::kShardokGrpcAddress, shardokAddr ? shardokAddr : "localhost"},
|
||||
{ServerConfiguration::kEagleInterfaceGrpcAddress,
|
||||
eagleInterfaceAddr ? eagleInterfaceAddr : "localhost:40042"}};
|
||||
}
|
||||
|
||||
ServerConfiguration::ServerConfiguration(const string& filePath) {
|
||||
|
||||
+97
-6
@@ -77,11 +77,14 @@ public class AuthInterceptor : Interceptor {
|
||||
};
|
||||
|
||||
public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
public TMP_InputField urlField;
|
||||
public TMP_Dropdown environmentDropdown;
|
||||
public TMP_InputField nameField;
|
||||
public TMP_InputField passwordField;
|
||||
public TMP_Dropdown resolutionDropdown;
|
||||
|
||||
[Header("Status Display")]
|
||||
public TextMeshProUGUI connectionStatusText;
|
||||
|
||||
public GameObject connectionPanel;
|
||||
public GameObject gameSelectionPanel;
|
||||
public GameObject customBattlePanel;
|
||||
@@ -112,11 +115,22 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
|
||||
public ClientPregeneratedText clientPregeneratedText;
|
||||
|
||||
const String DefaultUrl = "eagle0.net";
|
||||
const String URLKey = "urlKey";
|
||||
const String BaseDomain = "eagle0.net";
|
||||
const String EnvironmentKey = "environmentKey";
|
||||
const String NameKey = "nameKey";
|
||||
const String PasswordKey = "passwordKey";
|
||||
|
||||
// Environment options: index 0 = prod., index 1 = qa., index 2 = (none)
|
||||
private static readonly List<string> EnvironmentOptions = new() { "prod.", "qa.", "" };
|
||||
private static readonly List<string> EnvironmentDisplayNames =
|
||||
new() { "prod.", "qa.", "(none)" };
|
||||
|
||||
private string GetUrlFromEnvironment() {
|
||||
int envIndex = environmentDropdown.value;
|
||||
string prefix = EnvironmentOptions[envIndex];
|
||||
return prefix + BaseDomain;
|
||||
}
|
||||
|
||||
public void ReceiveLobbyUpdate(LobbyResponse lobbyResponse) {
|
||||
_handleLobbyResponse(lobbyResponse);
|
||||
}
|
||||
@@ -133,7 +147,11 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
resolutionDropdown.AddOptions(resolutions.Select(r => $"{r.width} x {r.height}").ToList());
|
||||
resolutionDropdown.value = resolutions.ToList().IndexOf(currentResolution);
|
||||
|
||||
urlField.text = PlayerPrefs.GetString(URLKey, DefaultUrl);
|
||||
// Set up environment dropdown
|
||||
environmentDropdown.ClearOptions();
|
||||
environmentDropdown.AddOptions(EnvironmentDisplayNames);
|
||||
environmentDropdown.value = PlayerPrefs.GetInt(EnvironmentKey, 0);
|
||||
|
||||
nameField.text = PlayerPrefs.GetString(NameKey, "sample_name");
|
||||
passwordField.text = PlayerPrefs.GetString(PasswordKey, "sample_password");
|
||||
|
||||
@@ -147,6 +165,78 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
customBattlePanel.gameObject.SetActive(false);
|
||||
|
||||
errorHandler.gameObject.SetActive(true);
|
||||
|
||||
// Initialize status text
|
||||
UpdateConnectionStatus();
|
||||
}
|
||||
|
||||
private float _statusUpdateTimer = 0f;
|
||||
private const float StatusUpdateInterval = 0.5f;
|
||||
|
||||
void Update() {
|
||||
// Only update status periodically and when connection panel is visible
|
||||
if (!connectionPanel.activeInHierarchy) return;
|
||||
|
||||
_statusUpdateTimer += Time.deltaTime;
|
||||
if (_statusUpdateTimer >= StatusUpdateInterval) {
|
||||
_statusUpdateTimer = 0f;
|
||||
UpdateConnectionStatus();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateConnectionStatus() {
|
||||
if (connectionStatusText == null) return;
|
||||
|
||||
// No connection yet - show nothing
|
||||
if (_persistentClientConnection == null) {
|
||||
connectionStatusText.text = "";
|
||||
return;
|
||||
}
|
||||
|
||||
var circuitState = _persistentClientConnection.CircuitBreaker.CurrentState;
|
||||
var connState = _persistentClientConnection.CurrentState;
|
||||
|
||||
// Circuit breaker takes precedence
|
||||
if (circuitState == eagle.ConnectionCircuitBreaker.State.Open) {
|
||||
var nextTest = _persistentClientConnection.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));
|
||||
connectionStatusText.text =
|
||||
$"<color=red>●</color> Server unavailable. Retry in {seconds}s";
|
||||
return;
|
||||
}
|
||||
}
|
||||
connectionStatusText.text = "<color=red>●</color> Server unavailable";
|
||||
return;
|
||||
}
|
||||
|
||||
if (circuitState == eagle.ConnectionCircuitBreaker.State.HalfOpen) {
|
||||
connectionStatusText.text = "<color=yellow>●</color> Testing connection...";
|
||||
return;
|
||||
}
|
||||
|
||||
// Normal connection states
|
||||
connectionStatusText.text = connState switch {
|
||||
eagle.ConnectionState.Connected => "<color=green>●</color> Connected",
|
||||
eagle.ConnectionState.Connecting => "<color=yellow>●</color> Connecting...",
|
||||
eagle.ConnectionState.Disconnected => "<color=red>●</color> Disconnected",
|
||||
eagle.ConnectionState.Reconnecting => GetReconnectingText(),
|
||||
eagle.ConnectionState.SubscriptionPending => "<color=yellow>●</color> Subscribing...",
|
||||
_ => ""
|
||||
};
|
||||
}
|
||||
|
||||
private string GetReconnectingText() {
|
||||
var nextAttempt = _persistentClientConnection?.NextReconnectAttempt;
|
||||
if (!nextAttempt.HasValue) return "<color=yellow>●</color> Reconnecting...";
|
||||
|
||||
var timeUntilRetry = nextAttempt.Value - DateTime.UtcNow;
|
||||
if (timeUntilRetry.TotalSeconds <= 0) { return "<color=yellow>●</color> Reconnecting..."; }
|
||||
|
||||
int seconds = Math.Min(10, (int)Math.Ceiling(timeUntilRetry.TotalSeconds));
|
||||
return $"<color=orange>●</color> Retry in {seconds}s";
|
||||
}
|
||||
|
||||
public void EditorButtonClicked() { SceneManager.LoadScene("Map Editor"); }
|
||||
@@ -267,7 +357,7 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
}
|
||||
|
||||
private void _createConnection() {
|
||||
PlayerPrefs.SetString(URLKey, urlField.text);
|
||||
PlayerPrefs.SetInt(EnvironmentKey, environmentDropdown.value);
|
||||
PlayerPrefs.SetString(NameKey, nameField.text);
|
||||
PlayerPrefs.SetString(PasswordKey, passwordField.text);
|
||||
|
||||
@@ -280,7 +370,8 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
// Initialize cancellation token for thread management
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
eagleConnection = new EagleConnection(nameField.text, passwordField.text, urlField.text);
|
||||
string url = GetUrlFromEnvironment();
|
||||
eagleConnection = new EagleConnection(nameField.text, passwordField.text, url);
|
||||
_persistentClientConnection = new PersistentClientConnection(
|
||||
eagleConnection.EagleGrpcClient,
|
||||
eagleConnection.credentials,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
using UnityEngine;
|
||||
|
||||
public class ConnectionKiller : MonoBehaviour {
|
||||
@@ -10,6 +12,10 @@ public class ConnectionKiller : MonoBehaviour {
|
||||
public void OnApplicationQuit() { ShutdownAll(); }
|
||||
|
||||
#if UNITY_EDITOR
|
||||
void OnEnable() { EditorApplication.playModeStateChanged += OnPlayModeStateChanged; }
|
||||
|
||||
void OnDisable() { EditorApplication.playModeStateChanged -= OnPlayModeStateChanged; }
|
||||
|
||||
void OnPlayModeStateChanged(PlayModeStateChange state) {
|
||||
if (state == PlayModeStateChange.ExitingPlayMode) { ShutdownAll(); }
|
||||
}
|
||||
|
||||
+2
-1
@@ -57,7 +57,8 @@ namespace eagle.Notifications {
|
||||
{ BreakAllianceAcceptedDetails, BreakAllianceAcceptedNotificationGenerator.Generator },
|
||||
{ VassalExiledDetails, VassalExiledDetailsNotificationGenerator.Generator },
|
||||
{ WithdrewForTruce, WithdrewForTruceDetailsNotificationGenerator.Generator },
|
||||
{ ProfessionGainedDetails, ProfessionGainedDetailsNotificationGenerator.Generator }
|
||||
{ ProfessionGainedDetails, ProfessionGainedDetailsNotificationGenerator.Generator },
|
||||
{ NewFactionHeadDetails, NewFactionHeadDetailsNotificationGenerator.Generator }
|
||||
};
|
||||
|
||||
public static IEnumerable<Notification> GenerateNotifications(
|
||||
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
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 NewFactionHeadDetailsNotificationGenerator {
|
||||
public static readonly ARNNotificationGenerator Generator = GenerateNotifications;
|
||||
|
||||
private static readonly Random Random = new();
|
||||
|
||||
private static readonly List<string> NotificationTitles = new() {
|
||||
"New Leadership",
|
||||
"Succession",
|
||||
"A New Era",
|
||||
"Rise to Power",
|
||||
"The Mantle Passes",
|
||||
"Heir Ascendant",
|
||||
"Crown of Command",
|
||||
"Leadership Transferred",
|
||||
"The Torch Passes",
|
||||
"New Commander"
|
||||
};
|
||||
|
||||
private static string GetNotificationTitle() {
|
||||
return NotificationTitles[Random.Next(NotificationTitles.Count)];
|
||||
}
|
||||
|
||||
private static ProvinceId? FindProvinceForHero(HeroId heroId, IGameModel model) {
|
||||
foreach (var province in model.Provinces.Values) {
|
||||
if (province.FullInfo?.RulingFactionHeroIds.Contains(heroId) == true) {
|
||||
return province.Id;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static IEnumerable<Notification> GenerateNotifications(
|
||||
Net.Eagle0.Eagle.Common.Notification notification,
|
||||
IGameModel currentModel) {
|
||||
var details = notification.Details.NewFactionHeadDetails;
|
||||
var newHeadHero = currentModel.Heroes[details.NewHeadHeroId];
|
||||
var previousHeadHero =
|
||||
currentModel.Heroes.TryGetValue(details.PreviousHeadHeroId, out var prev)
|
||||
? prev
|
||||
: null;
|
||||
|
||||
string textTemplate;
|
||||
List<ProvinceId> affectedProvinces;
|
||||
|
||||
if (details.FactionId == currentModel.PlayerId) {
|
||||
// Player's own faction - their new faction head
|
||||
string previousHeadDescription =
|
||||
previousHeadHero != null ? $"Following the death of {{PreviousHead}}, your"
|
||||
: "Your";
|
||||
textTemplate =
|
||||
$"{previousHeadDescription} sworn {DisplayNames.SiblingDescription(newHeadHero.PronounGender)} {{NewHead}} now leads your faction.\n\n";
|
||||
|
||||
var heroProvinceId = FindProvinceForHero(details.NewHeadHeroId, currentModel);
|
||||
affectedProvinces = heroProvinceId.HasValue
|
||||
? new List<ProvinceId> { heroProvinceId.Value }
|
||||
: currentModel.ProvincesForFaction(details.FactionId);
|
||||
} else {
|
||||
// Another faction
|
||||
var factionName = currentModel.FactionName(details.FactionId);
|
||||
if (previousHeadHero != null) {
|
||||
textTemplate =
|
||||
$"Following the death of {{PreviousHead}}, {{NewHead}} now leads {factionName}.\n\n";
|
||||
} else {
|
||||
textTemplate = $"{{NewHead}} now leads {factionName}.\n\n";
|
||||
}
|
||||
affectedProvinces = currentModel.ProvincesForFaction(details.FactionId);
|
||||
}
|
||||
|
||||
var heroPlaceholders = new Dictionary<string, (string nameTextId, string fallback)> {
|
||||
{ "NewHead", (newHeadHero.NameTextId, "A hero") }
|
||||
};
|
||||
if (previousHeadHero != null) {
|
||||
heroPlaceholders["PreviousHead"] =
|
||||
(previousHeadHero.NameTextId, "the previous leader");
|
||||
}
|
||||
|
||||
var displayedHeroes = new List<HeroView> { newHeadHero };
|
||||
if (previousHeadHero != null) { displayedHeroes.Add(previousHeadHero); }
|
||||
|
||||
yield return DynamicTextNotification.StreamingDynamicNotification(
|
||||
title: GetNotificationTitle(),
|
||||
textTemplate: textTemplate,
|
||||
heroPlaceholders: heroPlaceholders,
|
||||
llmId: notification.LlmId,
|
||||
provinceIds: affectedProvinces,
|
||||
displayedHeroes: displayedHeroes);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -511,6 +511,7 @@ public class HexGrid : MonoBehaviour {
|
||||
}
|
||||
|
||||
public void ClearCellLabels() {
|
||||
if (cells == null) return;
|
||||
for (int i = 0; i < width * height; i++) {
|
||||
cells[i].UpperUnitInfoLabel.text = "";
|
||||
cells[i].LowerUnitInfoLabel.text = "";
|
||||
|
||||
@@ -3,6 +3,22 @@ load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
|
||||
load("@rules_proto//proto:defs.bzl", "proto_library")
|
||||
load("@rules_scala//scala_proto:scala_proto.bzl", "scala_proto_library")
|
||||
|
||||
# Auth service for OAuth login
|
||||
scala_proto_library(
|
||||
name = "auth_scala_grpc",
|
||||
visibility = [
|
||||
"//src/main/scala/net/eagle0:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle:__subpackages__",
|
||||
],
|
||||
deps = [":auth_grpc"],
|
||||
)
|
||||
|
||||
proto_library(
|
||||
name = "auth_grpc",
|
||||
srcs = ["auth.proto"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "eagle_scala_grpc",
|
||||
visibility = [
|
||||
@@ -242,6 +258,7 @@ go_proto_library(
|
||||
compilers = ["@io_bazel_rules_go//proto:go_grpc"], # keep
|
||||
importpath = "github.com/nolen777/eagle0/src/main/protobuf/net/eagle0/eagle/api/eagle", # keep
|
||||
protos = [
|
||||
":auth_grpc",
|
||||
":available_command_proto",
|
||||
":command_proto",
|
||||
":eagle_grpc",
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
//
|
||||
// Copyright 2025 Dan Crosby
|
||||
//
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package net.eagle0.eagle.api.auth;
|
||||
|
||||
option java_multiple_files = true;
|
||||
option java_package = "net.eagle0.eagle.api.auth";
|
||||
option java_outer_classname = "AuthProto";
|
||||
option objc_class_prefix = "E0A";
|
||||
|
||||
// Authentication service for OAuth login
|
||||
service Auth {
|
||||
// Get OAuth URL to open in system browser
|
||||
rpc GetOAuthUrl(GetOAuthUrlRequest) returns (GetOAuthUrlResponse) {}
|
||||
|
||||
// Exchange authorization code for JWT tokens
|
||||
rpc ExchangeCode(ExchangeCodeRequest) returns (ExchangeCodeResponse) {}
|
||||
|
||||
// Set or update display name (requires valid JWT)
|
||||
rpc SetDisplayName(SetDisplayNameRequest) returns (SetDisplayNameResponse) {}
|
||||
|
||||
// Refresh access token using refresh token
|
||||
rpc RefreshToken(RefreshTokenRequest) returns (RefreshTokenResponse) {}
|
||||
|
||||
// Get current user info (validates JWT)
|
||||
rpc GetCurrentUser(GetCurrentUserRequest) returns (GetCurrentUserResponse) {}
|
||||
|
||||
// Logout - invalidates refresh token
|
||||
rpc Logout(LogoutRequest) returns (LogoutResponse) {}
|
||||
}
|
||||
|
||||
// OAuth provider enumeration
|
||||
enum OAuthProvider {
|
||||
OAUTH_PROVIDER_UNSPECIFIED = 0;
|
||||
OAUTH_PROVIDER_DISCORD = 1;
|
||||
OAUTH_PROVIDER_GOOGLE = 2;
|
||||
}
|
||||
|
||||
// Request to initiate OAuth flow
|
||||
message GetOAuthUrlRequest {
|
||||
OAuthProvider provider = 1;
|
||||
string redirect_uri = 2; // e.g., "eagle0://auth/callback"
|
||||
}
|
||||
|
||||
message GetOAuthUrlResponse {
|
||||
string auth_url = 1; // Full OAuth URL to open in system browser
|
||||
string state = 2; // State parameter for CSRF protection
|
||||
}
|
||||
|
||||
// Exchange authorization code for JWT
|
||||
message ExchangeCodeRequest {
|
||||
OAuthProvider provider = 1;
|
||||
string authorization_code = 2;
|
||||
string state = 3; // Must match state from GetOAuthUrlResponse
|
||||
string redirect_uri = 4; // Must match exactly what was used in GetOAuthUrlRequest
|
||||
}
|
||||
|
||||
message ExchangeCodeResponse {
|
||||
string access_token = 1; // JWT for API access
|
||||
string refresh_token = 2; // Long-lived token for getting new access tokens
|
||||
int64 expires_at = 3; // Unix timestamp when access_token expires
|
||||
UserInfo user = 4;
|
||||
bool is_new_user = 5; // True if user needs to set display name
|
||||
}
|
||||
|
||||
// User information returned from auth endpoints
|
||||
message UserInfo {
|
||||
string user_id = 1; // Internal Eagle0 user ID (UUID)
|
||||
string display_name = 2; // Player-chosen display name (may be empty for new users)
|
||||
string avatar_url = 3; // Profile picture URL from OAuth provider
|
||||
OAuthProvider provider = 4;
|
||||
}
|
||||
|
||||
// Set or update display name
|
||||
message SetDisplayNameRequest {
|
||||
string display_name = 1; // 3-20 characters, alphanumeric + underscore
|
||||
}
|
||||
|
||||
message SetDisplayNameResponse {
|
||||
bool success = 1;
|
||||
string error_message = 2; // If not success: "Name taken", "Invalid characters", etc.
|
||||
UserInfo user = 3;
|
||||
}
|
||||
|
||||
// Refresh access token
|
||||
message RefreshTokenRequest {
|
||||
string refresh_token = 1;
|
||||
}
|
||||
|
||||
message RefreshTokenResponse {
|
||||
string access_token = 1;
|
||||
int64 expires_at = 2;
|
||||
}
|
||||
|
||||
// Get current user info
|
||||
message GetCurrentUserRequest {}
|
||||
|
||||
message GetCurrentUserResponse {
|
||||
UserInfo user = 1;
|
||||
}
|
||||
|
||||
// Logout
|
||||
message LogoutRequest {}
|
||||
|
||||
message LogoutResponse {}
|
||||
@@ -284,6 +284,12 @@ message ProfessionGainedDetails {
|
||||
Profession new_profession = 3;
|
||||
}
|
||||
|
||||
message NewFactionHeadDetails {
|
||||
int32 new_head_hero_id = 1;
|
||||
int32 faction_id = 2;
|
||||
int32 previous_head_hero_id = 3;
|
||||
}
|
||||
|
||||
message NotificationDetails {
|
||||
oneof sealed_value {
|
||||
AllianceAcceptedDetails alliance_accepted_details = 30;
|
||||
@@ -324,5 +330,6 @@ message NotificationDetails {
|
||||
VassalExiledDetails vassal_exiled_details = 8;
|
||||
WithdrewForTruceDetails withdrew_for_truce = 3;
|
||||
ProfessionGainedDetails profession_gained_details = 35;
|
||||
NewFactionHeadDetails new_faction_head_details = 39;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -772,6 +772,17 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "user_scala_proto",
|
||||
deps = [":user_proto"],
|
||||
)
|
||||
|
||||
proto_library(
|
||||
name = "user_proto",
|
||||
srcs = ["user.proto"],
|
||||
deps = ["@com_google_protobuf//:timestamp_proto"],
|
||||
)
|
||||
|
||||
go_proto_library(
|
||||
name = "internal_go_proto",
|
||||
importpath = "github.com/nolen777/eagle0/src/main/protobuf/net/eagle0/eagle/internal",
|
||||
@@ -802,6 +813,7 @@ go_proto_library(
|
||||
":shardok_results_proto",
|
||||
":supplies_proto",
|
||||
":unaffiliated_hero_proto",
|
||||
":user_proto",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/common:common_go_proto",
|
||||
|
||||
@@ -53,4 +53,6 @@ message ChangedFaction {
|
||||
|
||||
.google.protobuf.Int32Value new_last_acted_province_id = 22;
|
||||
bool clear_last_acted_province_id = 23;
|
||||
|
||||
.google.protobuf.StringValue new_name = 26;
|
||||
}
|
||||
|
||||
@@ -72,6 +72,7 @@ message GeneratedTextRequestDetails {
|
||||
PrisonerReleasedMessage prisoner_released_message = 32;
|
||||
PrisonerExiledMessage prisoner_exiled_message = 33;
|
||||
PrisonerReturnedMessage prisoner_returned_message = 34;
|
||||
NewFactionHeadMessage new_faction_head_message = 35;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,3 +335,13 @@ message PrisonerReturnedMessage {
|
||||
int32 to_faction_id = 4;
|
||||
int32 province_id = 5;
|
||||
}
|
||||
|
||||
message NewFactionHeadMessage {
|
||||
int32 new_head_hero_id = 1;
|
||||
int32 faction_id = 2;
|
||||
string previous_faction_name = 3;
|
||||
// If present, the faction is being renamed to this
|
||||
string new_faction_name = 4;
|
||||
// The previous faction head who was executed
|
||||
int32 previous_head_hero_id = 5;
|
||||
}
|
||||
|
||||
@@ -58,4 +58,8 @@ message Hero {
|
||||
repeated EventForHeroBackstory new_backstory_events = 29;
|
||||
|
||||
.net.eagle0.eagle.common.Gender pronoun_gender = 27;
|
||||
|
||||
// The name of the faction this hero would lead, if they led one.
|
||||
// Populated for "great person" heroes from the heroes TSV; empty for others.
|
||||
string led_faction_name = 30;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
//
|
||||
// Copyright 2025 Dan Crosby
|
||||
//
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package net.eagle0.eagle.internal.user;
|
||||
|
||||
import "google/protobuf/timestamp.proto";
|
||||
|
||||
option java_multiple_files = true;
|
||||
option java_package = "net.eagle0.eagle.internal.user";
|
||||
|
||||
// Internal user record stored in users.pb
|
||||
message User {
|
||||
string user_id = 1; // UUID generated by Eagle
|
||||
string display_name = 2; // Unique, player-chosen name
|
||||
string display_name_lower = 3; // Lowercase version for uniqueness checks
|
||||
|
||||
// OAuth identities (a user can have multiple linked providers)
|
||||
repeated OAuthIdentity oauth_identities = 4;
|
||||
|
||||
google.protobuf.Timestamp created_at = 5;
|
||||
google.protobuf.Timestamp last_login_at = 6;
|
||||
|
||||
string avatar_url = 7;
|
||||
|
||||
// Admin flag - allows impersonating other users for debugging
|
||||
// Set manually in the database, not via API
|
||||
bool is_admin = 8;
|
||||
}
|
||||
|
||||
// OAuth identity from a provider
|
||||
message OAuthIdentity {
|
||||
string provider = 1; // "discord" or "google"
|
||||
string provider_user_id = 2; // ID from the OAuth provider
|
||||
string email = 3; // Email from provider (for account recovery)
|
||||
google.protobuf.Timestamp linked_at = 4;
|
||||
}
|
||||
|
||||
// Container for all users (stored as single file)
|
||||
message UserDatabase {
|
||||
repeated User users = 1;
|
||||
|
||||
// Indexes for fast lookup (provider:provider_user_id -> user_id)
|
||||
map<string, string> oauth_index = 2;
|
||||
|
||||
// display_name_lower -> user_id
|
||||
map<string, string> display_name_index = 3;
|
||||
}
|
||||
|
||||
// Refresh token record
|
||||
message RefreshToken {
|
||||
string token_id = 1; // UUID
|
||||
string user_id = 2;
|
||||
string token_hash = 3; // SHA-256 hash of actual token
|
||||
google.protobuf.Timestamp created_at = 4;
|
||||
google.protobuf.Timestamp expires_at = 5;
|
||||
bool revoked = 6;
|
||||
}
|
||||
|
||||
// Container for refresh tokens
|
||||
message RefreshTokenDatabase {
|
||||
repeated RefreshToken tokens = 1;
|
||||
}
|
||||
@@ -43,4 +43,6 @@ message FactionViewDiff {
|
||||
repeated .net.eagle0.eagle.common.DiplomacyOffer removed_incoming_diplomacy_offers = 13;
|
||||
|
||||
.google.protobuf.Int32Value new_focus_province_id = 11;
|
||||
|
||||
.google.protobuf.StringValue new_name = 14;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,11 @@ object ApiKeys {
|
||||
private val apiKeyFilePath =
|
||||
"src/main/scala/net/eagle0/common/llm_integration/api_keys.txt"
|
||||
|
||||
// These must have valid values set in api_keys.txt
|
||||
// Environment variable names
|
||||
private val openAIEnvVar = "OPENAI_API_KEY"
|
||||
private val anthropicEnvVar = "ANTHROPIC_API_KEY"
|
||||
|
||||
// File key names
|
||||
private val openAIKeyName = "openai_api_key"
|
||||
private val anthropicKeyName = "anthropic_api_key"
|
||||
|
||||
@@ -23,25 +27,31 @@ object ApiKeys {
|
||||
)
|
||||
}.toMap
|
||||
}.getOrElse {
|
||||
throw new ApiKeyException(
|
||||
"Failed to read api_keys.txt. Make sure you have copied api_keys_template.txt to api_keys.txt and set the keys."
|
||||
)
|
||||
Map.empty // Return empty map if file doesn't exist; we'll check env vars
|
||||
}
|
||||
|
||||
private val dictionary = readDictionary()
|
||||
|
||||
private def getKey(key: String): String =
|
||||
dictionary.get(key) match {
|
||||
case None =>
|
||||
throw new ApiKeyException(s"$key not found")
|
||||
case Some(value) if value.startsWith("your_") =>
|
||||
throw new ApiKeyException(
|
||||
s"API key for $key is not set. Please set it in $apiKeyFilePath."
|
||||
)
|
||||
case Some(value) => value
|
||||
private def getKey(key: String, envVar: String): String =
|
||||
// First check environment variable
|
||||
Option(System.getenv(envVar)).filter(_.nonEmpty) match {
|
||||
case Some(value) => value
|
||||
case None =>
|
||||
// Fall back to file-based dictionary
|
||||
dictionary.get(key) match {
|
||||
case None =>
|
||||
throw new ApiKeyException(
|
||||
s"$key not found. Set $envVar environment variable or add to $apiKeyFilePath."
|
||||
)
|
||||
case Some(value) if value.startsWith("your_") =>
|
||||
throw new ApiKeyException(
|
||||
s"API key for $key is not set. Set $envVar environment variable or update $apiKeyFilePath."
|
||||
)
|
||||
case Some(value) => value
|
||||
}
|
||||
}
|
||||
|
||||
lazy val openAI: String = getKey(openAIKeyName)
|
||||
lazy val openAI: String = getKey(openAIKeyName, openAIEnvVar)
|
||||
|
||||
lazy val anthropic: String = getKey(anthropicKeyName)
|
||||
lazy val anthropic: String = getKey(anthropicKeyName, anthropicEnvVar)
|
||||
}
|
||||
|
||||
@@ -7,19 +7,26 @@ scala_binary(
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
":eagle_pkg",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:auth_scala_grpc",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:eagle_scala_grpc",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:user_scala_proto",
|
||||
"//src/main/scala/net/eagle0/common:functional_random",
|
||||
"//src/main/scala/net/eagle0/common:simple_timed_logger",
|
||||
"//src/main/scala/net/eagle0/eagle/auth:jwt_service",
|
||||
"//src/main/scala/net/eagle0/eagle/auth:oauth_service",
|
||||
"//src/main/scala/net/eagle0/eagle/auth:user_service",
|
||||
"//src/main/scala/net/eagle0/eagle/service",
|
||||
"//src/main/scala/net/eagle0/eagle/service:auth_service",
|
||||
"//src/main/scala/net/eagle0/eagle/service:authorization_interceptor",
|
||||
"//src/main/scala/net/eagle0/eagle/service:custom_battle_manager",
|
||||
"//src/main/scala/net/eagle0/eagle/service:exception_interceptor",
|
||||
"//src/main/scala/net/eagle0/eagle/service:games_manager",
|
||||
"//src/main/scala/net/eagle0/eagle/service:server_setup_helpers",
|
||||
"//src/main/scala/net/eagle0/eagle/service/persistence:local_file_persister",
|
||||
"//src/main/scala/net/eagle0/eagle/service/persistence:save_directory",
|
||||
"//src/main/scala/net/eagle0/eagle/shardok_interface:battle_resolution",
|
||||
"//src/main/scala/net/eagle0/eagle/shardok_interface:battle_update",
|
||||
"//src/main/scala/net/eagle0/eagle/shardok_interface:battle_update_receiver",
|
||||
# "@maven//:com_thesamet_scalapb_scalapb_runtime_3",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -6,8 +6,11 @@ import scala.util.Random
|
||||
|
||||
import io.grpc.{Server, ServerBuilder}
|
||||
import net.eagle0.common.{FunctionalRandom, SeededRandom, SimpleTimedLogger}
|
||||
import net.eagle0.eagle.api.auth.auth.AuthGrpc
|
||||
import net.eagle0.eagle.api.eagle.EagleGrpc
|
||||
import net.eagle0.eagle.auth.{JwtService, JwtServiceImpl, OAuthServiceImpl, UserServiceImpl}
|
||||
import net.eagle0.eagle.service.*
|
||||
import net.eagle0.eagle.service.persistence.{LocalFilePersister, SaveDirectory}
|
||||
|
||||
object Main {
|
||||
import ServerSetupHelpers.*
|
||||
@@ -92,6 +95,13 @@ object Main {
|
||||
): Server = {
|
||||
val serverBuilder = ServerBuilder.forPort(grpcPort)
|
||||
|
||||
// Initialize auth services
|
||||
val jwtService: Option[JwtService] = JwtServiceImpl.fromEnvironment()
|
||||
val authPersister = LocalFilePersister(SaveDirectory.saveDirectory)
|
||||
val userService = new UserServiceImpl(authPersister)
|
||||
val oauthService = new OAuthServiceImpl()
|
||||
|
||||
// Add Eagle game service
|
||||
serverBuilder.addService(
|
||||
EagleGrpc
|
||||
.bindService(
|
||||
@@ -99,8 +109,27 @@ object Main {
|
||||
ExecutionContext.global
|
||||
)
|
||||
)
|
||||
|
||||
// Add Auth service (only if JWT is configured)
|
||||
jwtService.foreach { jwt =>
|
||||
serverBuilder.addService(
|
||||
AuthGrpc
|
||||
.bindService(
|
||||
AuthServiceImpl(oauthService, userService, jwt),
|
||||
ExecutionContext.global
|
||||
)
|
||||
)
|
||||
SimpleTimedLogger.printLogger.logLine("OAuth auth service enabled")
|
||||
}
|
||||
|
||||
if jwtService.isEmpty then {
|
||||
SimpleTimedLogger.printLogger.logLine(
|
||||
"OAuth auth service disabled (no JWT keys configured). Using Basic Auth only."
|
||||
)
|
||||
}
|
||||
|
||||
serverBuilder.intercept(new ExceptionInterceptor)
|
||||
serverBuilder.intercept(new AuthorizationInterceptor)
|
||||
serverBuilder.intercept(new AuthorizationInterceptor(jwtService))
|
||||
|
||||
serverBuilder.build
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@ case class AIClient(
|
||||
AlmsCommandSelector
|
||||
.chosenAlmsForSupportWithMinimumCommandForProvince(
|
||||
actingFactionId = fid,
|
||||
gameState = gs,
|
||||
gameState = GameStateConverter.fromProto(gs),
|
||||
availableCommands = acs,
|
||||
minimumFood = 0,
|
||||
provinceId = opac.provinceId
|
||||
|
||||
@@ -74,6 +74,8 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:available_command_selector",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:province_gold_surplus_calculator",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:trust_for_diplomacy",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -187,14 +189,15 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:available_command_selector",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:command_chooser",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:expand_command_selector",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:food_consumption_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:fulfill_quests_command_selector",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:improve_command_selector",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:organize_command_selector",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:province_gold_surplus_calculator",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils:legacy_faction_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/food_consumption:legacy_food_consumption_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/hero:legacy_hero_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/province:legacy_province_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -236,7 +239,10 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:available_command_selector",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:command_chooser",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:ransom_offer_helpers",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils:legacy_faction_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/faction",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import net.eagle0.eagle.library.settings.MinChanceForAIInvite
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.{ProvinceGoldSurplusCalculator, TrustForDiplomacy}
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.AvailableCommandSelector.flatSelectionForType
|
||||
import net.eagle0.eagle.library.util.CommandSelection
|
||||
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
|
||||
import net.eagle0.eagle.FactionId
|
||||
|
||||
object InvitationCommandSelector {
|
||||
@@ -22,16 +23,16 @@ object InvitationCommandSelector {
|
||||
availableCommands: Vector[AvailableCommand]
|
||||
): Option[CommandSelection] =
|
||||
flatSelectionForType[DiplomacyAvailableCommand](acs = availableCommands) { diplomacyAvailableCommand =>
|
||||
val nativeGameState = GameStateConverter.fromProto(gameState)
|
||||
diplomacyAvailableCommand.options.collect { case io: InvitationOption => io }.filter { invitationOption =>
|
||||
TrustForDiplomacy.meetsConditionsForInvitation(
|
||||
actingFactionId = actingFactionId,
|
||||
targetFactionId = invitationOption.targetFactionId,
|
||||
gameState = gameState
|
||||
gameState = nativeGameState
|
||||
)
|
||||
}.filter { invitationOption =>
|
||||
ProvinceGoldSurplusCalculator.provinceGoldSurplus(
|
||||
diplomacyAvailableCommand.actingProvinceId,
|
||||
gameState
|
||||
nativeGameState.provinces(diplomacyAvailableCommand.actingProvinceId)
|
||||
) >= invitationOption.goldCost
|
||||
}.map { invitationOption =>
|
||||
InvitationOptionWithChance(
|
||||
@@ -39,7 +40,8 @@ object InvitationCommandSelector {
|
||||
acceptanceChance = ResolveDiplomacyCommandSelector.invitationAcceptanceChance(
|
||||
actingFactionId,
|
||||
invitationOption.targetFactionId,
|
||||
gameState
|
||||
nativeGameState.factions.values.toVector,
|
||||
nativeGameState.provinces.values.toVector
|
||||
)
|
||||
)
|
||||
}.maxByOption(_.acceptanceChance)
|
||||
|
||||
@@ -19,7 +19,6 @@ import net.eagle0.eagle.library.util.command_choice_helpers.{
|
||||
CommandChoiceHelpers,
|
||||
CommandChooser,
|
||||
ExpandCommandSelector,
|
||||
FoodConsumptionUtils,
|
||||
FulfillQuestsCommandSelector,
|
||||
ImproveCommandSelector,
|
||||
OrganizeCommandSelector,
|
||||
@@ -29,9 +28,11 @@ import net.eagle0.eagle.library.util.command_choice_helpers.AvailableCommandSele
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.CommandChoiceHelpers.*
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.CommandChooserImplicits.*
|
||||
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
|
||||
import net.eagle0.eagle.library.util.food_consumption.LegacyFoodConsumptionUtils
|
||||
import net.eagle0.eagle.library.util.hero.LegacyHeroUtils
|
||||
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
|
||||
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
|
||||
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
|
||||
import net.eagle0.eagle.views.battalion_view.BattalionView
|
||||
import net.eagle0.eagle.views.province_view.FullProvinceInfo
|
||||
|
||||
@@ -137,7 +138,7 @@ object MidGameAIClient {
|
||||
) =>
|
||||
val actingProvince = gameState.provinces(actingProvinceId)
|
||||
val foodMonthsToHoldBack =
|
||||
FoodConsumptionUtils.foodConsumptionMonthsToHold(
|
||||
LegacyFoodConsumptionUtils.foodConsumptionMonthsToHold(
|
||||
actingProvinceId,
|
||||
gameState
|
||||
)
|
||||
@@ -269,7 +270,7 @@ object MidGameAIClient {
|
||||
RandomState(
|
||||
ImproveCommandSelector.chosenImproveCommand(
|
||||
actingFactionId,
|
||||
gameState,
|
||||
GameStateConverter.fromProto(gameState),
|
||||
availableCommands
|
||||
),
|
||||
functionalRandom
|
||||
@@ -298,7 +299,7 @@ object MidGameAIClient {
|
||||
RandomState(
|
||||
ImproveCommandSelector.chosenImproveCommand(
|
||||
actingFactionId,
|
||||
gameState,
|
||||
GameStateConverter.fromProto(gameState),
|
||||
availableCommands
|
||||
),
|
||||
functionalRandom
|
||||
@@ -345,7 +346,18 @@ object MidGameAIClient {
|
||||
acs = availableCommands,
|
||||
choosersAndReasons = Vector(
|
||||
(
|
||||
ExpandCommandSelector.maybeChosenExpandCommand,
|
||||
(
|
||||
fid: FactionId,
|
||||
gs: GameState,
|
||||
acs: Vector[AvailableCommand],
|
||||
fr: FunctionalRandom
|
||||
) =>
|
||||
ExpandCommandSelector.maybeChosenExpandCommand(
|
||||
fid,
|
||||
GameStateConverter.fromProto(gs),
|
||||
acs,
|
||||
fr
|
||||
),
|
||||
"no hostile neighbors, expand"
|
||||
),
|
||||
(
|
||||
@@ -362,7 +374,7 @@ object MidGameAIClient {
|
||||
RandomState(
|
||||
ImproveCommandSelector.chosenImproveCommand(
|
||||
actingFactionId,
|
||||
gameState,
|
||||
GameStateConverter.fromProto(gameState),
|
||||
availableCommands
|
||||
),
|
||||
functionalRandom
|
||||
@@ -450,7 +462,7 @@ object MidGameAIClient {
|
||||
RandomState(
|
||||
ImproveCommandSelector.chosenImproveCommand(
|
||||
actingFactionId,
|
||||
gameState,
|
||||
GameStateConverter.fromProto(gameState),
|
||||
availableCommands
|
||||
),
|
||||
functionalRandom
|
||||
@@ -505,7 +517,7 @@ object MidGameAIClient {
|
||||
|
||||
AttackCommandChooser.chosenAttackCommand(
|
||||
actingFactionId,
|
||||
gs,
|
||||
GameStateConverter.fromProto(gs),
|
||||
acs,
|
||||
reconnedHeroCountFor,
|
||||
reconnedBattalionsFor
|
||||
@@ -525,13 +537,13 @@ object MidGameAIClient {
|
||||
AlmsCommandSelector
|
||||
.chosenAlmsForSupportCommand(
|
||||
actingFactionId = actingFactionId,
|
||||
gameState = gameState,
|
||||
gameState = GameStateConverter.fromProto(gameState),
|
||||
availableCommands = availableCommands
|
||||
)
|
||||
.orElse {
|
||||
ImproveCommandSelector.chosenImproveCommand(
|
||||
actingFactionId = actingFactionId,
|
||||
gameState = gameState,
|
||||
gameState = GameStateConverter.fromProto(gameState),
|
||||
availableCommands = availableCommands
|
||||
)
|
||||
}
|
||||
@@ -564,7 +576,7 @@ object MidGameAIClient {
|
||||
RandomState(
|
||||
AttackCommandChooser.chosenSupportAttackCommand(
|
||||
actingFactionId,
|
||||
gameState,
|
||||
GameStateConverter.fromProto(gameState),
|
||||
availableCommands
|
||||
),
|
||||
functionalRandom
|
||||
@@ -790,7 +802,7 @@ object MidGameAIClient {
|
||||
OrganizeCommandSelector
|
||||
.chosenOrganizeCommandWithFoodSurplus(
|
||||
actingFactionId = fid,
|
||||
gameState = gs,
|
||||
gameState = GameStateConverter.fromProto(gs),
|
||||
availableCommands = acs,
|
||||
extraFoodSurplus = foodSurplus(
|
||||
gs,
|
||||
|
||||
@@ -39,10 +39,12 @@ import net.eagle0.eagle.library.util.command_choice_helpers.AvailableCommandSele
|
||||
randomSelectionForType,
|
||||
selectionForType
|
||||
}
|
||||
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
|
||||
import net.eagle0.eagle.library.util.faction_utils.{FactionUtils, LegacyFactionUtils}
|
||||
import net.eagle0.eagle.library.util.CommandSelection
|
||||
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
|
||||
import net.eagle0.eagle.library.EagleInternalException
|
||||
import net.eagle0.eagle.model.state.faction.FactionT
|
||||
import net.eagle0.eagle.model.state.province.ProvinceT
|
||||
import net.eagle0.eagle.FactionId
|
||||
|
||||
object ResolveDiplomacyCommandSelector {
|
||||
@@ -140,6 +142,20 @@ object ResolveDiplomacyCommandSelector {
|
||||
- LegacyFactionUtils.prestige(invitedFid, gs)
|
||||
- MinimumPrestigeDifferenceToInvite.doubleValue).floor.toInt.max(0)
|
||||
|
||||
/** Protoless version */
|
||||
def invitationAcceptanceChance(
|
||||
originatingFid: FactionId,
|
||||
invitedFid: FactionId,
|
||||
factions: Vector[FactionT],
|
||||
provinces: Vector[ProvinceT]
|
||||
): Int = {
|
||||
val originatingFaction = factions.find(_.id == originatingFid).get
|
||||
val invitedFaction = factions.find(_.id == invitedFid).get
|
||||
(FactionUtils.prestige(originatingFaction, provinces)
|
||||
- FactionUtils.prestige(invitedFaction, provinces)
|
||||
- MinimumPrestigeDifferenceToInvite.doubleValue).floor.toInt.max(0)
|
||||
}
|
||||
|
||||
private def resolveTruceOfferSelectedCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
|
||||
@@ -97,7 +97,7 @@ object SeekMoreLeadersCommandChooser {
|
||||
.filter(_.rulingFactionHeroIds.length > 1)
|
||||
.flatMap { province =>
|
||||
SwornBrotherChooser
|
||||
.bestChoice(
|
||||
.bestChoiceProto(
|
||||
province.rulingFactionHeroIds.map(gameState.heroes),
|
||||
gameState.factions(actingFactionId).leaders.toVector
|
||||
)
|
||||
@@ -142,7 +142,7 @@ object SeekMoreLeadersCommandChooser {
|
||||
MoreOption
|
||||
.flatWhen(needsMoreLeaders(actingFactionId, gameState)) {
|
||||
// First try a hero that's already here
|
||||
val desiredHero = SwornBrotherChooser.bestChoice(
|
||||
val desiredHero = SwornBrotherChooser.bestChoiceProto(
|
||||
leaderProvince.rulingFactionHeroIds.map(gameState.heroes),
|
||||
faction.leaders.toVector
|
||||
)
|
||||
@@ -151,7 +151,6 @@ object SeekMoreLeadersCommandChooser {
|
||||
SwearBrotherhoodCommandSelector
|
||||
.chosenCommandForSpecificHero(
|
||||
actingFactionId = actingFactionId,
|
||||
gameState = gameState,
|
||||
availableCommands = acs,
|
||||
desiredHeroId = hero.id
|
||||
)
|
||||
@@ -170,7 +169,6 @@ object SeekMoreLeadersCommandChooser {
|
||||
.orElse {
|
||||
HeroGiftCommandSelector.chosenCommandForSpecificHero(
|
||||
actingFactionId = actingFactionId,
|
||||
gameState = gameState,
|
||||
availableCommands = acs,
|
||||
heroId = hero.id,
|
||||
reason = "want to make this hero a leader"
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
load("@rules_scala//scala:scala.bzl", "scala_library")
|
||||
|
||||
scala_library(
|
||||
name = "jwt_service",
|
||||
srcs = ["JwtService.scala"],
|
||||
visibility = [
|
||||
"//src/main/scala/net/eagle0/eagle:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
"@maven//:com_nimbusds_nimbus_jose_jwt",
|
||||
],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "oauth_config",
|
||||
srcs = ["OAuthConfig.scala"],
|
||||
visibility = [
|
||||
"//src/main/scala/net/eagle0/eagle:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle:__subpackages__",
|
||||
],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "oauth_service",
|
||||
srcs = ["OAuthService.scala"],
|
||||
visibility = [
|
||||
"//src/main/scala/net/eagle0/eagle:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
":oauth_config",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:auth_scala_grpc",
|
||||
"@maven//:org_json4s_json4s_ast_3",
|
||||
"@maven//:org_json4s_json4s_core_3",
|
||||
"@maven//:org_json4s_json4s_native_3",
|
||||
],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "user_service",
|
||||
srcs = ["UserService.scala"],
|
||||
visibility = [
|
||||
"//src/main/scala/net/eagle0/eagle:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:user_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle/service/persistence:persister",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,182 @@
|
||||
package net.eagle0.eagle.auth
|
||||
|
||||
import java.security.MessageDigest
|
||||
import java.time.Instant
|
||||
import java.util.{Date, UUID}
|
||||
|
||||
import scala.jdk.CollectionConverters.*
|
||||
import scala.util.Try
|
||||
|
||||
import com.nimbusds.jose.{JWSAlgorithm, JWSHeader}
|
||||
import com.nimbusds.jose.crypto.{RSASSASigner, RSASSAVerifier}
|
||||
import com.nimbusds.jose.jwk.gen.RSAKeyGenerator
|
||||
import com.nimbusds.jose.jwk.RSAKey
|
||||
import com.nimbusds.jwt.{JWTClaimsSet, SignedJWT}
|
||||
|
||||
/** JWT claims extracted from a validated token */
|
||||
case class JwtClaims(
|
||||
userId: String,
|
||||
displayName: String,
|
||||
isAdmin: Boolean,
|
||||
issuedAt: Instant,
|
||||
expiresAt: Instant
|
||||
)
|
||||
|
||||
/** Refresh token with metadata */
|
||||
case class RefreshTokenInfo(
|
||||
tokenId: String,
|
||||
userId: String,
|
||||
tokenHash: String,
|
||||
expiresAt: Instant
|
||||
)
|
||||
|
||||
/** Service for creating and validating JWT tokens */
|
||||
trait JwtService {
|
||||
|
||||
/** Create an access token for a user */
|
||||
def createAccessToken(userId: String, displayName: String, isAdmin: Boolean): String
|
||||
|
||||
/** Create a refresh token (returns token string and info for storage) */
|
||||
def createRefreshToken(userId: String): (String, RefreshTokenInfo)
|
||||
|
||||
/** Validate an access token and extract claims */
|
||||
def validateAccessToken(token: String): Option[JwtClaims]
|
||||
|
||||
/** Hash a refresh token for storage */
|
||||
def hashRefreshToken(token: String): String
|
||||
}
|
||||
|
||||
class JwtServiceImpl(rsaKey: RSAKey) extends JwtService {
|
||||
|
||||
private val accessTokenDuration = java.time.Duration.ofDays(7)
|
||||
private val refreshTokenDuration = java.time.Duration.ofDays(30)
|
||||
private val issuer = "eagle0"
|
||||
|
||||
private val signer = new RSASSASigner(rsaKey)
|
||||
private val verifier = new RSASSAVerifier(rsaKey.toPublicJWK)
|
||||
|
||||
override def createAccessToken(
|
||||
userId: String,
|
||||
displayName: String,
|
||||
isAdmin: Boolean
|
||||
): String = {
|
||||
val now = Instant.now()
|
||||
val expiresAt = now.plus(accessTokenDuration)
|
||||
|
||||
val claimsBuilder = new JWTClaimsSet.Builder()
|
||||
.subject(userId)
|
||||
.issuer(issuer)
|
||||
.issueTime(Date.from(now))
|
||||
.expirationTime(Date.from(expiresAt))
|
||||
.claim("name", displayName)
|
||||
|
||||
// Only include admin claim if true to keep tokens smaller
|
||||
if isAdmin then {
|
||||
claimsBuilder.claim("admin", true)
|
||||
}
|
||||
|
||||
val signedJWT = new SignedJWT(
|
||||
new JWSHeader.Builder(JWSAlgorithm.RS256).keyID(rsaKey.getKeyID).build(),
|
||||
claimsBuilder.build()
|
||||
)
|
||||
|
||||
signedJWT.sign(signer)
|
||||
signedJWT.serialize()
|
||||
}
|
||||
|
||||
override def createRefreshToken(userId: String): (String, RefreshTokenInfo) = {
|
||||
val tokenId = UUID.randomUUID().toString
|
||||
val tokenSecret = UUID.randomUUID().toString
|
||||
val token = s"$tokenId.$tokenSecret"
|
||||
val tokenHash = hashRefreshToken(token)
|
||||
val expiresAt = Instant.now().plus(refreshTokenDuration)
|
||||
|
||||
val info = RefreshTokenInfo(
|
||||
tokenId = tokenId,
|
||||
userId = userId,
|
||||
tokenHash = tokenHash,
|
||||
expiresAt = expiresAt
|
||||
)
|
||||
|
||||
(token, info)
|
||||
}
|
||||
|
||||
override def validateAccessToken(token: String): Option[JwtClaims] =
|
||||
Try {
|
||||
val signedJWT = SignedJWT.parse(token)
|
||||
|
||||
// Verify signature
|
||||
if !signedJWT.verify(verifier) then {
|
||||
return None
|
||||
}
|
||||
|
||||
val claims = signedJWT.getJWTClaimsSet
|
||||
|
||||
// Check issuer
|
||||
if claims.getIssuer != issuer then {
|
||||
return None
|
||||
}
|
||||
|
||||
// Check expiration
|
||||
val expirationTime = claims.getExpirationTime
|
||||
if expirationTime == null || expirationTime.before(new Date()) then {
|
||||
return None
|
||||
}
|
||||
|
||||
val userId = claims.getSubject
|
||||
val displayName = Option(claims.getStringClaim("name")).getOrElse("")
|
||||
val isAdmin = Option(claims.getBooleanClaim("admin")).map(_.booleanValue()).getOrElse(false)
|
||||
val issuedAt = claims.getIssueTime.toInstant
|
||||
val expiresAt = expirationTime.toInstant
|
||||
|
||||
Some(
|
||||
JwtClaims(
|
||||
userId = userId,
|
||||
displayName = displayName,
|
||||
isAdmin = isAdmin,
|
||||
issuedAt = issuedAt,
|
||||
expiresAt = expiresAt
|
||||
)
|
||||
)
|
||||
}.toOption.flatten
|
||||
|
||||
override def hashRefreshToken(token: String): String = {
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
val hashBytes = digest.digest(token.getBytes("UTF-8"))
|
||||
hashBytes.map("%02x".format(_)).mkString
|
||||
}
|
||||
}
|
||||
|
||||
object JwtServiceImpl {
|
||||
|
||||
/** Generate a new RSA key pair for JWT signing */
|
||||
def generateKey(): RSAKey =
|
||||
new RSAKeyGenerator(2048)
|
||||
.keyID(UUID.randomUUID().toString)
|
||||
.generate()
|
||||
|
||||
/** Load RSA key from JSON string (JWK format) */
|
||||
def loadKey(json: String): RSAKey =
|
||||
RSAKey.parse(json)
|
||||
|
||||
/** Create a JwtService with a new generated key (for testing) */
|
||||
def withGeneratedKey(): JwtServiceImpl =
|
||||
new JwtServiceImpl(generateKey())
|
||||
|
||||
/** Create a JwtService from a JWK JSON string */
|
||||
def fromKeyJson(json: String): JwtServiceImpl =
|
||||
new JwtServiceImpl(loadKey(json))
|
||||
|
||||
/**
|
||||
* Create a JwtService from environment variables.
|
||||
*
|
||||
* Looks for JWT_PRIVATE_KEY environment variable containing RSA key in JWK format. If not found, returns None (auth
|
||||
* will fall back to Basic Auth only).
|
||||
*/
|
||||
def fromEnvironment(): Option[JwtServiceImpl] =
|
||||
sys.env.get("JWT_PRIVATE_KEY").flatMap { keyJson =>
|
||||
Try {
|
||||
fromKeyJson(keyJson)
|
||||
}.toOption
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package net.eagle0.eagle.auth
|
||||
|
||||
/** Configuration for an OAuth provider */
|
||||
case class OAuthProviderConfig(
|
||||
clientId: String,
|
||||
clientSecret: String,
|
||||
authorizationEndpoint: String,
|
||||
tokenEndpoint: String,
|
||||
userInfoEndpoint: String,
|
||||
scopes: Seq[String]
|
||||
)
|
||||
|
||||
/** OAuth configuration loaded from environment variables */
|
||||
object OAuthConfig {
|
||||
|
||||
def discord: OAuthProviderConfig = OAuthProviderConfig(
|
||||
clientId = sys.env.getOrElse("DISCORD_CLIENT_ID", ""),
|
||||
clientSecret = sys.env.getOrElse("DISCORD_CLIENT_SECRET", ""),
|
||||
authorizationEndpoint = "https://discord.com/api/oauth2/authorize",
|
||||
tokenEndpoint = "https://discord.com/api/oauth2/token",
|
||||
userInfoEndpoint = "https://discord.com/api/users/@me",
|
||||
scopes = Seq("identify", "email")
|
||||
)
|
||||
|
||||
def google: OAuthProviderConfig = OAuthProviderConfig(
|
||||
clientId = sys.env.getOrElse("GOOGLE_CLIENT_ID", ""),
|
||||
clientSecret = sys.env.getOrElse("GOOGLE_CLIENT_SECRET", ""),
|
||||
authorizationEndpoint = "https://accounts.google.com/o/oauth2/v2/auth",
|
||||
tokenEndpoint = "https://oauth2.googleapis.com/token",
|
||||
userInfoEndpoint = "https://www.googleapis.com/oauth2/v2/userinfo",
|
||||
scopes = Seq("openid", "email", "profile")
|
||||
)
|
||||
|
||||
/** Check if Discord OAuth is configured */
|
||||
def isDiscordConfigured: Boolean =
|
||||
discord.clientId.nonEmpty && discord.clientSecret.nonEmpty
|
||||
|
||||
/** Check if Google OAuth is configured */
|
||||
def isGoogleConfigured: Boolean =
|
||||
google.clientId.nonEmpty && google.clientSecret.nonEmpty
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
// Copyright 2025 Dan Crosby
|
||||
package net.eagle0.eagle.auth
|
||||
|
||||
import java.net.{URI, URLEncoder}
|
||||
import java.net.http.{HttpClient, HttpRequest, HttpResponse}
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.time.Duration
|
||||
import java.util.UUID
|
||||
|
||||
import scala.collection.concurrent.TrieMap
|
||||
import scala.util.Try
|
||||
|
||||
import net.eagle0.eagle.api.auth.auth.OAuthProvider
|
||||
import org.json4s.{DefaultFormats, JObject, JString}
|
||||
import org.json4s.jvalue2extractable
|
||||
import org.json4s.jvalue2monadic
|
||||
import org.json4s.native.Json
|
||||
|
||||
/** User info from OAuth provider */
|
||||
case class ProviderUserInfo(
|
||||
id: String,
|
||||
email: String,
|
||||
avatarUrl: String,
|
||||
username: String
|
||||
)
|
||||
|
||||
/** Service for OAuth code exchange */
|
||||
trait OAuthService {
|
||||
|
||||
/** Generate OAuth authorization URL */
|
||||
def getAuthUrl(provider: OAuthProvider, redirectUri: String): (String, String) // (url, state)
|
||||
|
||||
/** Exchange authorization code for user info */
|
||||
def exchangeCode(
|
||||
provider: OAuthProvider,
|
||||
code: String,
|
||||
state: String,
|
||||
redirectUri: String
|
||||
): Either[String, ProviderUserInfo]
|
||||
}
|
||||
|
||||
class OAuthServiceImpl extends OAuthService {
|
||||
|
||||
private implicit val formats: DefaultFormats.type = DefaultFormats
|
||||
private val json = new Json(formats)
|
||||
|
||||
private val httpClient = HttpClient
|
||||
.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(10))
|
||||
.build()
|
||||
|
||||
// State parameter storage with timestamp (for CSRF protection)
|
||||
private val stateStore = TrieMap[String, Long]()
|
||||
|
||||
// State expiration (10 minutes)
|
||||
private val stateExpirationMs = 600000L
|
||||
|
||||
override def getAuthUrl(provider: OAuthProvider, redirectUri: String): (String, String) = {
|
||||
val config = getConfig(provider)
|
||||
|
||||
val state = UUID.randomUUID().toString
|
||||
stateStore.put(state, System.currentTimeMillis())
|
||||
|
||||
// Clean old states
|
||||
val cutoff = System.currentTimeMillis() - stateExpirationMs
|
||||
stateStore.filterInPlace((_, timestamp) => timestamp > cutoff)
|
||||
|
||||
val params = Map(
|
||||
"client_id" -> config.clientId,
|
||||
"redirect_uri" -> redirectUri,
|
||||
"response_type" -> "code",
|
||||
"scope" -> config.scopes.mkString(" "),
|
||||
"state" -> state
|
||||
)
|
||||
|
||||
val queryString = params.map {
|
||||
case (k, v) =>
|
||||
s"$k=${URLEncoder.encode(v, StandardCharsets.UTF_8)}"
|
||||
}
|
||||
.mkString("&")
|
||||
|
||||
val url = s"${config.authorizationEndpoint}?$queryString"
|
||||
(url, state)
|
||||
}
|
||||
|
||||
override def exchangeCode(
|
||||
provider: OAuthProvider,
|
||||
code: String,
|
||||
state: String,
|
||||
redirectUri: String
|
||||
): Either[String, ProviderUserInfo] = {
|
||||
// Verify state
|
||||
stateStore.remove(state) match {
|
||||
case Some(timestamp) if System.currentTimeMillis() - timestamp < stateExpirationMs =>
|
||||
// Valid state
|
||||
case _ =>
|
||||
return Left("Invalid or expired state parameter")
|
||||
}
|
||||
|
||||
val config = getConfig(provider)
|
||||
|
||||
// Exchange code for access token
|
||||
val tokenResult = exchangeCodeForToken(config, code, redirectUri)
|
||||
tokenResult match {
|
||||
case Left(error) => Left(error)
|
||||
case Right(accessToken) =>
|
||||
// Fetch user info
|
||||
fetchUserInfo(provider, config, accessToken)
|
||||
}
|
||||
}
|
||||
|
||||
private def getConfig(provider: OAuthProvider): OAuthProviderConfig =
|
||||
provider match {
|
||||
case OAuthProvider.OAUTH_PROVIDER_DISCORD => OAuthConfig.discord
|
||||
case OAuthProvider.OAUTH_PROVIDER_GOOGLE => OAuthConfig.google
|
||||
case _ => throw new IllegalArgumentException(s"Unsupported provider: $provider")
|
||||
}
|
||||
|
||||
private def exchangeCodeForToken(
|
||||
config: OAuthProviderConfig,
|
||||
code: String,
|
||||
redirectUri: String
|
||||
): Either[String, String] =
|
||||
Try {
|
||||
val formData = Map(
|
||||
"client_id" -> config.clientId,
|
||||
"client_secret" -> config.clientSecret,
|
||||
"grant_type" -> "authorization_code",
|
||||
"code" -> code,
|
||||
"redirect_uri" -> redirectUri
|
||||
)
|
||||
|
||||
val formBody = formData.map {
|
||||
case (k, v) =>
|
||||
s"${URLEncoder.encode(k, StandardCharsets.UTF_8)}=${URLEncoder.encode(v, StandardCharsets.UTF_8)}"
|
||||
}
|
||||
.mkString("&")
|
||||
|
||||
val request = HttpRequest
|
||||
.newBuilder()
|
||||
.uri(URI.create(config.tokenEndpoint))
|
||||
.timeout(Duration.ofSeconds(10))
|
||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||
.header("Accept", "application/json")
|
||||
.POST(HttpRequest.BodyPublishers.ofString(formBody))
|
||||
.build()
|
||||
|
||||
val response = httpClient.send(request, HttpResponse.BodyHandlers.ofString())
|
||||
val body = response.body()
|
||||
|
||||
if response.statusCode() < 200 || response.statusCode() >= 300 then {
|
||||
return Left(s"Token exchange failed: ${response.statusCode()} - $body")
|
||||
}
|
||||
|
||||
val parsed = json.parse(body)
|
||||
val accessToken = (parsed \ "access_token").extract[String]
|
||||
Right(accessToken)
|
||||
}.toEither.left.map(e => s"Token exchange error: ${e.getMessage}").flatten
|
||||
|
||||
private def fetchUserInfo(
|
||||
provider: OAuthProvider,
|
||||
config: OAuthProviderConfig,
|
||||
accessToken: String
|
||||
): Either[String, ProviderUserInfo] =
|
||||
Try {
|
||||
val request = HttpRequest
|
||||
.newBuilder()
|
||||
.uri(URI.create(config.userInfoEndpoint))
|
||||
.timeout(Duration.ofSeconds(10))
|
||||
.header("Authorization", s"Bearer $accessToken")
|
||||
.header("Accept", "application/json")
|
||||
.GET()
|
||||
.build()
|
||||
|
||||
val response = httpClient.send(request, HttpResponse.BodyHandlers.ofString())
|
||||
val body = response.body()
|
||||
|
||||
if response.statusCode() < 200 || response.statusCode() >= 300 then {
|
||||
return Left(s"User info fetch failed: ${response.statusCode()} - $body")
|
||||
}
|
||||
|
||||
val parsed = json.parse(body)
|
||||
parseUserInfo(provider, parsed)
|
||||
}.toEither.left.map(e => s"User info fetch error: ${e.getMessage}").flatten
|
||||
|
||||
private def parseUserInfo(
|
||||
provider: OAuthProvider,
|
||||
data: org.json4s.JValue
|
||||
): Either[String, ProviderUserInfo] =
|
||||
Try {
|
||||
provider match {
|
||||
case OAuthProvider.OAUTH_PROVIDER_DISCORD =>
|
||||
val id = (data \ "id").extract[String]
|
||||
val username = (data \ "username").extract[String]
|
||||
val email = (data \ "email").extractOpt[String].getOrElse("")
|
||||
val avatarHash = (data \ "avatar").extractOpt[String]
|
||||
val avatarUrl = avatarHash
|
||||
.map(hash => s"https://cdn.discordapp.com/avatars/$id/$hash.png")
|
||||
.getOrElse("")
|
||||
ProviderUserInfo(id, email, avatarUrl, username)
|
||||
|
||||
case OAuthProvider.OAUTH_PROVIDER_GOOGLE =>
|
||||
val id = (data \ "id").extract[String]
|
||||
val email = (data \ "email").extractOpt[String].getOrElse("")
|
||||
val name = (data \ "name").extractOpt[String].getOrElse("")
|
||||
val avatarUrl = (data \ "picture").extractOpt[String].getOrElse("")
|
||||
ProviderUserInfo(id, email, avatarUrl, name)
|
||||
|
||||
case _ =>
|
||||
throw new IllegalArgumentException(s"Unsupported provider: $provider")
|
||||
}
|
||||
}.toEither.left.map(e => s"Failed to parse user info: ${e.getMessage}")
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
// Copyright 2025 Dan Crosby
|
||||
package net.eagle0.eagle.auth
|
||||
|
||||
import java.time.Instant
|
||||
import java.util.UUID
|
||||
|
||||
import scala.util.{Success, Using}
|
||||
|
||||
import com.google.protobuf.timestamp.Timestamp
|
||||
import net.eagle0.eagle.internal.user.user.{OAuthIdentity, User, UserDatabase}
|
||||
import net.eagle0.eagle.service.persistence.Persister
|
||||
|
||||
/** Service for managing user records */
|
||||
trait UserService {
|
||||
|
||||
/** Find or create a user from OAuth provider info */
|
||||
def findOrCreateUser(
|
||||
provider: String,
|
||||
providerUserId: String,
|
||||
email: String,
|
||||
avatarUrl: String
|
||||
): User
|
||||
|
||||
/** Find user by internal user ID */
|
||||
def findByUserId(userId: String): Option[User]
|
||||
|
||||
/** Find user by display name (case-insensitive) */
|
||||
def findByDisplayName(displayName: String): Option[User]
|
||||
|
||||
/** Set or update display name */
|
||||
def setDisplayName(userId: String, displayName: String): Either[String, User]
|
||||
|
||||
/** Check if display name is available */
|
||||
def isDisplayNameAvailable(displayName: String): Boolean
|
||||
}
|
||||
|
||||
class UserServiceImpl(persister: Persister) extends UserService {
|
||||
|
||||
private val usersPath = "auth/users.pb"
|
||||
|
||||
// Thread-safe access to user database
|
||||
private var database: UserDatabase = loadDatabase()
|
||||
private val lock = new Object
|
||||
|
||||
private def loadDatabase(): UserDatabase =
|
||||
persister.retrieveAsStream(usersPath) match {
|
||||
case Success(stream) =>
|
||||
Using(stream) { s =>
|
||||
UserDatabase.parseFrom(s)
|
||||
}.getOrElse(UserDatabase.defaultInstance)
|
||||
case _ => UserDatabase.defaultInstance
|
||||
}
|
||||
|
||||
private def saveDatabase(): Unit = {
|
||||
persister.save(usersPath, database.toByteArray)
|
||||
()
|
||||
}
|
||||
|
||||
private def nowTimestamp: Timestamp = {
|
||||
val now = Instant.now()
|
||||
Timestamp(now.getEpochSecond, now.getNano)
|
||||
}
|
||||
|
||||
override def findOrCreateUser(
|
||||
provider: String,
|
||||
providerUserId: String,
|
||||
email: String,
|
||||
avatarUrl: String
|
||||
): User = lock.synchronized {
|
||||
val oauthKey = s"$provider:$providerUserId"
|
||||
|
||||
database.oauthIndex.get(oauthKey) match {
|
||||
case Some(existingUserId) =>
|
||||
// Update last login, avatar
|
||||
val user = findByUserIdInternal(existingUserId).get
|
||||
val updated = user.copy(
|
||||
lastLoginAt = Some(nowTimestamp),
|
||||
avatarUrl = avatarUrl
|
||||
)
|
||||
updateUser(updated)
|
||||
updated
|
||||
|
||||
case None =>
|
||||
// Create new user
|
||||
val userId = UUID.randomUUID().toString
|
||||
val now = nowTimestamp
|
||||
val identity = OAuthIdentity(
|
||||
provider = provider,
|
||||
providerUserId = providerUserId,
|
||||
email = email,
|
||||
linkedAt = Some(now)
|
||||
)
|
||||
val user = User(
|
||||
userId = userId,
|
||||
displayName = "",
|
||||
displayNameLower = "",
|
||||
oauthIdentities = Seq(identity),
|
||||
createdAt = Some(now),
|
||||
lastLoginAt = Some(now),
|
||||
avatarUrl = avatarUrl,
|
||||
isAdmin = false
|
||||
)
|
||||
|
||||
database = database.copy(
|
||||
users = database.users :+ user,
|
||||
oauthIndex = database.oauthIndex + (oauthKey -> userId)
|
||||
)
|
||||
saveDatabase()
|
||||
user
|
||||
}
|
||||
}
|
||||
|
||||
override def findByUserId(userId: String): Option[User] = lock.synchronized {
|
||||
findByUserIdInternal(userId)
|
||||
}
|
||||
|
||||
private def findByUserIdInternal(userId: String): Option[User] =
|
||||
database.users.find(_.userId == userId)
|
||||
|
||||
override def findByDisplayName(displayName: String): Option[User] = lock.synchronized {
|
||||
val lowerName = displayName.toLowerCase
|
||||
database.displayNameIndex.get(lowerName).flatMap(findByUserIdInternal)
|
||||
}
|
||||
|
||||
override def setDisplayName(userId: String, displayName: String): Either[String, User] =
|
||||
lock.synchronized {
|
||||
// Validate: 3-20 chars, alphanumeric + underscore
|
||||
val validPattern = "^[a-zA-Z0-9_]{3,20}$".r
|
||||
if !validPattern.matches(displayName) then {
|
||||
return Left("Display name must be 3-20 characters, alphanumeric and underscore only")
|
||||
}
|
||||
|
||||
val lowerName = displayName.toLowerCase
|
||||
|
||||
// Check uniqueness
|
||||
database.displayNameIndex.get(lowerName) match {
|
||||
case Some(existingId) if existingId != userId =>
|
||||
Left("Display name already taken")
|
||||
case _ =>
|
||||
findByUserIdInternal(userId) match {
|
||||
case Some(user) =>
|
||||
// Remove old name from index if exists
|
||||
val newIndex = user.displayNameLower match {
|
||||
case "" => database.displayNameIndex
|
||||
case old => database.displayNameIndex - old
|
||||
}
|
||||
|
||||
val updated = user.copy(
|
||||
displayName = displayName,
|
||||
displayNameLower = lowerName
|
||||
)
|
||||
|
||||
database = database.copy(
|
||||
users = database.users.filterNot(_.userId == userId) :+ updated,
|
||||
displayNameIndex = newIndex + (lowerName -> userId)
|
||||
)
|
||||
saveDatabase()
|
||||
Right(updated)
|
||||
|
||||
case None =>
|
||||
Left("User not found")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override def isDisplayNameAvailable(displayName: String): Boolean = lock.synchronized {
|
||||
!database.displayNameIndex.contains(displayName.toLowerCase)
|
||||
}
|
||||
|
||||
private def updateUser(user: User): Unit = {
|
||||
database = database.copy(
|
||||
users = database.users.filterNot(_.userId == user.userId) :+ user
|
||||
)
|
||||
saveDatabase()
|
||||
}
|
||||
}
|
||||
@@ -309,14 +309,16 @@ case class EngineImpl(
|
||||
initialState = this.currentState,
|
||||
actionResultApplier = ActionResultApplierImpl(Some(ScalaRuntimeValidator)),
|
||||
functionalRandom = SeededRandom(this.currentState.randomSeed)
|
||||
).withTCommand { gs =>
|
||||
commandFactory.makeTCommand(
|
||||
actingFactionId = factionId,
|
||||
gameState = gs,
|
||||
availableCommand = availableCommand,
|
||||
selectedCommand = selectedCommand
|
||||
)
|
||||
}.withProtolessSequentialResultsAction(gs => HeroBackstoryUpdateActionGenerator(gs))
|
||||
).withTCommandAndLastCommand(
|
||||
commandGen = gs =>
|
||||
commandFactory.makeTCommand(
|
||||
actingFactionId = factionId,
|
||||
gameState = gs,
|
||||
availableCommand = availableCommand,
|
||||
selectedCommand = selectedCommand
|
||||
),
|
||||
selectedCommand = selectedCommand
|
||||
).withProtolessSequentialResultsAction(gs => HeroBackstoryUpdateActionGenerator(gs))
|
||||
|
||||
// Validate that the first result has an acting faction set (required for player commands)
|
||||
val firstResult = sequencer.actionResults.newValue.headOption
|
||||
|
||||
@@ -9,8 +9,6 @@ case class ActionResultWithResultingState(
|
||||
)
|
||||
|
||||
trait ActionResultApplier {
|
||||
def xpForStatBump(stat: Int): Int
|
||||
|
||||
def applyActionResults(
|
||||
startingState: GameState,
|
||||
results: Iterable[ActionResultT]
|
||||
|
||||
@@ -56,8 +56,6 @@ class ActionResultApplierImpl(validator: Option[ScalaValidator]) extends ActionR
|
||||
def validate(province: ProvinceT, roundPhase: RoundPhase): ProvinceT =
|
||||
validator.fold(province)(_.validate(province, roundPhase))
|
||||
|
||||
override def xpForStatBump(stat: Int): Int = GameStateExtensions.xpForStatBump(stat)
|
||||
|
||||
override def applyActionResults(
|
||||
startingState: GameState,
|
||||
results: Iterable[ActionResultT]
|
||||
|
||||
@@ -4,8 +4,6 @@ import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.library.actions.impl.common.ActionWithResultingState
|
||||
|
||||
trait ActionResultProtoApplier {
|
||||
def xpForStatBump(stat: Int): Int
|
||||
|
||||
def applyActionResults(
|
||||
startingState: GameState,
|
||||
results: Iterable[ActionResult]
|
||||
|
||||
-5
@@ -47,7 +47,6 @@ import net.eagle0.eagle.internal.province.Province
|
||||
import net.eagle0.eagle.internal.run_status.RunStatus
|
||||
import net.eagle0.eagle.internal.shardok_battle.ShardokBattle
|
||||
import net.eagle0.eagle.library.actions.impl.common.ActionWithResultingState
|
||||
import net.eagle0.eagle.library.settings.{ExtraXpForStatBumpOver100, XpForStatBump}
|
||||
import net.eagle0.eagle.library.util.hero.LegacyHeroUtils
|
||||
import net.eagle0.eagle.library.util.validations.Validator
|
||||
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
|
||||
@@ -57,10 +56,6 @@ import net.eagle0.eagle.library.EagleInternalException
|
||||
class ActionResultProtoApplierImpl(validator: Validator) extends ActionResultProtoApplier {
|
||||
import validator.validate
|
||||
|
||||
override def xpForStatBump(stat: Int): Int =
|
||||
if stat <= 99 then XpForStatBump.intValue
|
||||
else XpForStatBump.intValue + ExtraXpForStatBumpOver100.intValue * (stat - 99)
|
||||
|
||||
private val trustMax: Int = 100
|
||||
|
||||
implicit class _GS(gameState: GameState) {
|
||||
|
||||
@@ -9,8 +9,6 @@ case class ActionResultTWithResultingState(
|
||||
)
|
||||
|
||||
trait ActionResultTApplier {
|
||||
def xpForStatBump(stat: Int): Int
|
||||
|
||||
def applyActionResults(
|
||||
startingState: GameState,
|
||||
results: Iterable[ActionResultT]
|
||||
|
||||
+1
-2
@@ -6,6 +6,7 @@ import net.eagle0.eagle.model.action_result.ActionResultT
|
||||
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
|
||||
|
||||
object ActionResultTApplierImpl {
|
||||
|
||||
/** Creates an ActionResultTApplierImpl with no validation (for tests) */
|
||||
def apply(): ActionResultTApplierImpl =
|
||||
new ActionResultTApplierImpl(new ActionResultApplierImpl(None))
|
||||
@@ -18,8 +19,6 @@ object ActionResultTApplierImpl {
|
||||
}
|
||||
|
||||
class ActionResultTApplierImpl(baseApplier: ActionResultApplier) extends ActionResultTApplier {
|
||||
override def xpForStatBump(stat: Int): Int = baseApplier.xpForStatBump(stat)
|
||||
|
||||
override def applyActionResults(
|
||||
startingState: GameState,
|
||||
results: Iterable[ActionResultT]
|
||||
|
||||
@@ -242,8 +242,6 @@ scala_library(
|
||||
"//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/actions/impl/common:action_with_resulting_state",
|
||||
"//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",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:eagle_require",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:province_event_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/hero:legacy_hero_utils",
|
||||
|
||||
+1
@@ -108,6 +108,7 @@ object GameStateFactionExtensions {
|
||||
)
|
||||
|
||||
val updatedFaction = existingFaction.copy(
|
||||
name = cf.newName.getOrElse(existingFaction.name),
|
||||
factionHeadId = cf.newFactionHeadHeroId.getOrElse(existingFaction.factionHeadId),
|
||||
leaderIds = existingFaction.leaderIds.diff(cf.removedLeaderHeroIds) ++ cf.newLeaderHeroIds,
|
||||
prestigeModifiers =
|
||||
|
||||
+15
-8
@@ -7,9 +7,14 @@ import net.eagle0.eagle.internal.army.Attacking
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.internal.province.Province
|
||||
import net.eagle0.eagle.library.settings.{MaxCombatUnitCountPerSide, MinVigorForDefend}
|
||||
import net.eagle0.eagle.library.util.{BattalionSuitability, LegacyBattalionUtils}
|
||||
import net.eagle0.eagle.library.util.battalion_suitability.BattalionSuitability
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.CombatUnitSelector
|
||||
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
|
||||
import net.eagle0.eagle.library.util.LegacyBattalionUtils
|
||||
import net.eagle0.eagle.model.proto_converters.hero.HeroConverter
|
||||
import net.eagle0.eagle.model.proto_converters.BattalionConverter
|
||||
import net.eagle0.eagle.model.proto_converters.BattalionTypeConverter
|
||||
import net.eagle0.eagle.model.proto_converters.SuitableBattalionsConverter
|
||||
|
||||
object AvailableDefendCommandsFactory extends AvailableCommandsFactoryForType {
|
||||
private def sortedFleeProvinces(
|
||||
@@ -71,9 +76,9 @@ object AvailableDefendCommandsFactory extends AvailableCommandsFactoryForType {
|
||||
|
||||
val recommendedUnits = CombatUnitSelector.selectedCombatBattalions(
|
||||
desiredCount = availableHeroes.size.min(MaxCombatUnitCountPerSide.intValue),
|
||||
heroes = availableHeroes.toVector,
|
||||
battalions = availableBattalions.toVector,
|
||||
battalionTypes = gameState.battalionTypes.toVector
|
||||
heroes = availableHeroes.map(HeroConverter.fromProto).toVector,
|
||||
battalions = availableBattalions.map(BattalionConverter.fromProto).toVector,
|
||||
battalionTypes = gameState.battalionTypes.map(BattalionTypeConverter.fromProto).toVector
|
||||
)
|
||||
val hostileUnits = hostileArmies
|
||||
.flatMap(_.armies)
|
||||
@@ -86,10 +91,12 @@ object AvailableDefendCommandsFactory extends AvailableCommandsFactoryForType {
|
||||
.toVector
|
||||
.map(_.id),
|
||||
actingProvinceId = province.id,
|
||||
suitableBattalionsForHeroes = BattalionSuitability.suitableBattalionsForHeroes(
|
||||
hs = availableHeroes.toVector,
|
||||
bs = availableBattalions.toVector,
|
||||
bts = gameState.battalionTypes.toVector
|
||||
suitableBattalionsForHeroes = SuitableBattalionsConverter.toProtoMap(
|
||||
BattalionSuitability.suitableBattalionsForHeroes(
|
||||
hs = availableHeroes.map(HeroConverter.fromProto).toVector,
|
||||
bs = availableBattalions.map(BattalionConverter.fromProto).toVector,
|
||||
bts = gameState.battalionTypes.map(BattalionTypeConverter.fromProto).toVector
|
||||
)
|
||||
),
|
||||
availableBattalions = availableBattalionsWithCosts,
|
||||
availableFleeProvinceIds = sortedFleeProvinces(from = province, gs = gameState),
|
||||
|
||||
+17
-11
@@ -11,10 +11,15 @@ import net.eagle0.eagle.api.command.util.battalion_with_food_cost.BattalionWithF
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.internal.province.Province
|
||||
import net.eagle0.eagle.library.settings.{MaxCombatUnitCountPerSide, MinVigorForMarch}
|
||||
import net.eagle0.eagle.library.util.{BattalionSuitability, LegacyBattalionUtils, ShardokMapInfo}
|
||||
import net.eagle0.eagle.library.util.{LegacyBattalionUtils, ShardokMapInfo}
|
||||
import net.eagle0.eagle.library.util.battalion_suitability.BattalionSuitability
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.{CombatUnitSelector, MarchSuppliesHelpers}
|
||||
import net.eagle0.eagle.library.util.hero.LegacyHeroUtils
|
||||
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
|
||||
import net.eagle0.eagle.model.proto_converters.hero.HeroConverter
|
||||
import net.eagle0.eagle.model.proto_converters.BattalionConverter
|
||||
import net.eagle0.eagle.model.proto_converters.BattalionTypeConverter
|
||||
import net.eagle0.eagle.model.proto_converters.SuitableBattalionsConverter
|
||||
|
||||
object AvailableMarchCommandFactory extends AvailableCommandsFactoryForType {
|
||||
|
||||
@@ -67,14 +72,14 @@ object AvailableMarchCommandFactory extends AvailableCommandsFactoryForType {
|
||||
Option.when(recommendedSendCount > 0) {
|
||||
val units = CombatUnitSelector.selectedCombatBattalions(
|
||||
desiredCount = recommendedSendCount,
|
||||
heroes = availableHeroes.toVector,
|
||||
battalions = availableBattalions.toVector,
|
||||
battalionTypes = gameState.battalionTypes.toVector
|
||||
heroes = availableHeroes.map(HeroConverter.fromProto).toVector,
|
||||
battalions = availableBattalions.map(BattalionConverter.fromProto).toVector,
|
||||
battalionTypes = gameState.battalionTypes.map(BattalionTypeConverter.fromProto).toVector
|
||||
)
|
||||
RecommendedBattleConfig(
|
||||
units = units,
|
||||
food = MarchSuppliesHelpers.foodToTakeOnAttack(
|
||||
units.flatMap(_.battalionId).map(gameState.battalions),
|
||||
units.flatMap(_.battalionId).map(gameState.battalions).map(BattalionConverter.fromProto),
|
||||
originProvince.food
|
||||
),
|
||||
gold = MarchSuppliesHelpers.goldToTakeOnAttack(
|
||||
@@ -97,12 +102,13 @@ object AvailableMarchCommandFactory extends AvailableCommandsFactoryForType {
|
||||
)
|
||||
}.toVector,
|
||||
availableDestinationProvinces = availableDestinations,
|
||||
suitableBattalionsForHeroes = BattalionSuitability
|
||||
.suitableBattalionsForHeroes(
|
||||
hs = availableHeroes.toVector,
|
||||
bs = availableBattalions.toVector,
|
||||
bts = gameState.battalionTypes.toVector
|
||||
),
|
||||
suitableBattalionsForHeroes = SuitableBattalionsConverter.toProtoMap(
|
||||
BattalionSuitability.suitableBattalionsForHeroes(
|
||||
hs = availableHeroes.map(HeroConverter.fromProto).toVector,
|
||||
bs = availableBattalions.map(BattalionConverter.fromProto).toVector,
|
||||
bts = gameState.battalionTypes.map(BattalionTypeConverter.fromProto).toVector
|
||||
)
|
||||
),
|
||||
originProvinceId = originProvince.id,
|
||||
goldAvailable = originProvince.gold,
|
||||
foodAvailable = originProvince.food,
|
||||
|
||||
@@ -187,10 +187,16 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:max_combat_unit_count_per_side",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:min_vigor_for_defend",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:battalion_suitability",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:legacy_battalion_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/battalion_suitability",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:combat_unit_selector",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils:legacy_faction_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:battalion_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:battalion_type_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:suitable_battalions_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/hero",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -446,13 +452,19 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:max_combat_unit_count_per_side",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:min_vigor_for_march",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:battalion_suitability",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:legacy_battalion_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:shardok_map_info",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/battalion_suitability",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:combat_unit_selector",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:march_supplies_helpers",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/hero:legacy_hero_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/province:legacy_province_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:battalion_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:battalion_type_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:suitable_battalions_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/hero",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -1,18 +1,5 @@
|
||||
load("@rules_scala//scala:scala.bzl", "scala_library")
|
||||
|
||||
scala_library(
|
||||
name = "action_pkg",
|
||||
srcs = ["package.scala"],
|
||||
visibility = [
|
||||
"//src/main/scala/net/eagle0/eagle/library:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/library:__subpackages__",
|
||||
],
|
||||
runtime_deps = [
|
||||
"@maven//:com_thesamet_scalapb_lenses_3",
|
||||
],
|
||||
deps = ["//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto"],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "check_for_faction_changes_action",
|
||||
srcs = ["CheckForFactionChangesAction.scala"],
|
||||
@@ -42,6 +29,7 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_battalion_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_faction_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_hero_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:notification_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types:faction_destroyed_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types:faction_leader_removed_result_type",
|
||||
@@ -116,7 +104,7 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:min_support_for_taxes",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:portion_of_capacity_for_upgrade_battalion_quest",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:battalion_type_finder",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/battalion_type_finder:legacy_battalion_type_finder",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/province",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/quest_fulfillment:quest_fulfillment_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
|
||||
@@ -149,14 +137,16 @@ scala_library(
|
||||
exports = [
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:action_result_notification_details_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:action_result_type_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:faction_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library:game_history",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request/chronicle_event",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/date",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/date",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -789,7 +779,6 @@ scala_library(
|
||||
],
|
||||
deps = [
|
||||
":hero_backstory_update_action",
|
||||
"//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/common:protoless_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils",
|
||||
@@ -886,7 +875,6 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request/chronicle_event",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types:new_round_action_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/llm_request/chronicle_event",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/chronicle_entry",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/date",
|
||||
|
||||
+41
-16
@@ -6,8 +6,10 @@ import net.eagle0.eagle.library.actions.impl.common.ProtolessRandomSequentialRes
|
||||
import net.eagle0.eagle.library.util.province.ProvinceUtils
|
||||
import net.eagle0.eagle.library.util.ransom_validity.RansomValidity
|
||||
import net.eagle0.eagle.library.util.ReturningHeroes
|
||||
import net.eagle0.eagle.model.action_result.{NotificationDetails, NotificationT}
|
||||
import net.eagle0.eagle.model.action_result.changed_province.concrete.ChangedProvinceC
|
||||
import net.eagle0.eagle.model.action_result.concrete.{ActionResultC, ChangedFactionC, ChangedHeroC}
|
||||
import net.eagle0.eagle.model.action_result.concrete.{ActionResultC, ChangedFactionC, ChangedHeroC, NotificationC}
|
||||
import net.eagle0.eagle.model.action_result.generated_text_request.LlmRequestT.NewFactionHeadMessage
|
||||
import net.eagle0.eagle.model.action_result.types.{
|
||||
FactionDestroyedResultType,
|
||||
FactionLeaderRemovedResultType,
|
||||
@@ -208,24 +210,47 @@ case class CheckForFactionChangesAction(
|
||||
revisedFactions match {
|
||||
case Nil => None
|
||||
case fs =>
|
||||
val changedFactionData = fs.map { revisedFaction =>
|
||||
val originalFaction = factions.find(_.id == revisedFaction.id).get
|
||||
val newHeadHero = heroes.find(_.id == revisedFaction.factionHeadId)
|
||||
val newFactionName = newHeadHero.flatMap(_.ledFactionName)
|
||||
val llmRequestId = s"new_faction_head_${revisedFaction.id}_${revisedFaction.factionHeadId}"
|
||||
(
|
||||
ChangedFactionC(
|
||||
factionId = revisedFaction.id,
|
||||
removedLeaderHeroIds = originalFaction.leaderIds.filter(killedHeroIds.contains),
|
||||
newFactionHeadHeroId = Some(revisedFaction.factionHeadId),
|
||||
newName = newFactionName
|
||||
),
|
||||
NewFactionHeadMessage(
|
||||
requestId = llmRequestId,
|
||||
eagleGameId = gameId,
|
||||
recipientFactionIds = Vector.empty,
|
||||
alwaysGenerate = false,
|
||||
newHeadHeroId = revisedFaction.factionHeadId,
|
||||
factionId = revisedFaction.id,
|
||||
previousFactionName = originalFaction.name,
|
||||
newFactionName = newFactionName,
|
||||
previousHeadHeroId = originalFaction.factionHeadId
|
||||
),
|
||||
NotificationC(
|
||||
details = NotificationDetails.NewFactionHead(
|
||||
newHeadHeroId = revisedFaction.factionHeadId,
|
||||
factionId = revisedFaction.id,
|
||||
previousHeadHeroId = originalFaction.factionHeadId
|
||||
),
|
||||
affectedHeroIds = Vector(revisedFaction.factionHeadId, originalFaction.factionHeadId),
|
||||
llm = NotificationT.Llm.Id(llmRequestId),
|
||||
deferred = true
|
||||
)
|
||||
)
|
||||
}.toVector
|
||||
Some(
|
||||
ActionResultC(
|
||||
actionResultType = FactionLeaderRemovedResultType,
|
||||
changedFactions = fs
|
||||
.map(revisedFaction =>
|
||||
ChangedFactionC(
|
||||
factionId = revisedFaction.id,
|
||||
removedLeaderHeroIds = factions
|
||||
.find(
|
||||
_.id == revisedFaction.id
|
||||
)
|
||||
.get
|
||||
.leaderIds
|
||||
.filter(killedHeroIds.contains),
|
||||
newFactionHeadHeroId = Some(revisedFaction.factionHeadId)
|
||||
)
|
||||
)
|
||||
.toVector
|
||||
changedFactions = changedFactionData.map(_._1),
|
||||
newGeneratedTextRequests = changedFactionData.map(_._2),
|
||||
newNotifications = changedFactionData.map(_._3)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
+2
-2
@@ -4,9 +4,9 @@ import net.eagle0.eagle.{BattalionId, FactionId, GameId, HeroId, ProvinceId, Rou
|
||||
import net.eagle0.eagle.common.battalion_type.BattalionType
|
||||
import net.eagle0.eagle.library.actions.impl.common.ProtolessSequentialResultsAction
|
||||
import net.eagle0.eagle.library.settings.{MinSupportForTaxes, PortionOfCapacityForUpgradeBattalionQuest}
|
||||
import net.eagle0.eagle.library.util.battalion_type_finder.LegacyBattalionTypeFinder
|
||||
import net.eagle0.eagle.library.util.province.ProvinceUtils
|
||||
import net.eagle0.eagle.library.util.quest_fulfillment.QuestFulfillmentUtils
|
||||
import net.eagle0.eagle.library.util.BattalionTypeFinder
|
||||
import net.eagle0.eagle.model.action_result.ActionResultT
|
||||
import net.eagle0.eagle.model.state.battalion.BattalionT
|
||||
import net.eagle0.eagle.model.state.date.Date
|
||||
@@ -49,7 +49,7 @@ case class CheckForFulfilledQuestsAction(
|
||||
heroBackstoryTextIdLookup: HeroId => String
|
||||
) extends ProtolessSequentialResultsAction {
|
||||
private def battalionType(id: BattalionTypeId): BattalionType =
|
||||
BattalionTypeFinder.battalionType(id, battalionTypes)
|
||||
LegacyBattalionTypeFinder.battalionType(id, battalionTypes)
|
||||
|
||||
private def battalionWithId(id: BattalionId): Option[BattalionT] =
|
||||
battalions.find(_.id == id)
|
||||
|
||||
+310
-327
@@ -28,47 +28,14 @@ import net.eagle0.eagle.common.action_result_notification_details.{
|
||||
}
|
||||
import net.eagle0.eagle.common.action_result_notification_details.ShatteredArmyDetails.Reason.{
|
||||
SHATTERED_ARMY_REASON_BLIZZARD,
|
||||
SHATTERED_ARMY_REASON_UNKNOWN_UNSPECIFIED,
|
||||
SHATTERED_ARMY_REASON_WITHDRAW,
|
||||
Unrecognized
|
||||
SHATTERED_ARMY_REASON_WITHDRAW
|
||||
}
|
||||
import net.eagle0.eagle.common.action_result_type.ActionResultType.FACTION_DESTROYED
|
||||
import net.eagle0.eagle.internal.action_result.ActionResult
|
||||
import net.eagle0.eagle.internal.event_for_chronicle.{
|
||||
AllianceAcceptedEvent,
|
||||
BreakAllianceAmbassadorImprisonedEvent,
|
||||
BreakAllianceEvent,
|
||||
CapturedHeroExecutedEvent,
|
||||
CapturedHeroExiledEvent,
|
||||
CapturedHeroImprisonedEvent,
|
||||
EventForChronicle,
|
||||
EventForChronicleDetails,
|
||||
FactionDestroyedEvent,
|
||||
HeroDepartedEvent,
|
||||
InvitationAcceptedEvent,
|
||||
InvitationAmbassadorImprisonedEvent,
|
||||
OutlawApprehendedEvent,
|
||||
PrisonerExecutedEvent,
|
||||
PrisonersExchangedEvent,
|
||||
ProvinceConqueredEvent,
|
||||
ProvinceExpansionEvent,
|
||||
ProvinceHeldEvent,
|
||||
RansomPaidEvent,
|
||||
RansomRejectedEvent,
|
||||
ShatteredArmyEvent,
|
||||
SuppressBeastsFailedEvent,
|
||||
SwearBrotherhoodEvent,
|
||||
TruceAcceptedEvent,
|
||||
TruceAmbassadorImprisonedEvent,
|
||||
VassalExiledEvent
|
||||
}
|
||||
import net.eagle0.eagle.internal.event_for_chronicle.ShatteredArmyEvent.Reason.{
|
||||
SHATTERED_ARMY_EVENT_REASON_BLIZZARD,
|
||||
SHATTERED_ARMY_EVENT_REASON_UNKNOWN_UNSPECIFIED,
|
||||
SHATTERED_ARMY_EVENT_REASON_WITHDRAW
|
||||
}
|
||||
import net.eagle0.eagle.library.actions.impl.common.ActionWithResultingState
|
||||
import net.eagle0.eagle.library.GameHistory
|
||||
import net.eagle0.eagle.model.action_result.generated_text_request.chronicle_event.*
|
||||
import net.eagle0.eagle.model.proto_converters.date.DateConverter
|
||||
import net.eagle0.eagle.model.state.date.Date
|
||||
|
||||
object ChronicleEventGenerator {
|
||||
@@ -79,325 +46,341 @@ object ChronicleEventGenerator {
|
||||
|
||||
private def notificationEntries(
|
||||
awrs: ActionWithResultingState
|
||||
): Vector[EventForChronicle] = {
|
||||
val detailList: Vector[EventForChronicleDetails] =
|
||||
relevantNotifications(awrs.actionResult)
|
||||
.map(_.details)
|
||||
.collect {
|
||||
case AllianceAcceptedDetails(
|
||||
offeringFid,
|
||||
targetFid,
|
||||
ambassadorHid,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
AllianceAcceptedEvent(
|
||||
offeringFactionId = offeringFid,
|
||||
targetFactionId = targetFid,
|
||||
ambassadorHeroId = ambassadorHid
|
||||
)
|
||||
): Vector[ChronicleEvent] = {
|
||||
val date = DateConverter.fromProto(awrs.gameState.currentDate)
|
||||
relevantNotifications(awrs.actionResult)
|
||||
.map(_.details)
|
||||
.collect {
|
||||
case AllianceAcceptedDetails(
|
||||
offeringFid,
|
||||
targetFid,
|
||||
ambassadorHid,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
AllianceAcceptedChronicleEvent(
|
||||
date = date,
|
||||
offeringFactionId = offeringFid,
|
||||
targetFactionId = targetFid,
|
||||
ambassadorHeroId = ambassadorHid
|
||||
)
|
||||
|
||||
case CapturedHeroExiledDetails(
|
||||
capturedFromFactionId,
|
||||
provinceId,
|
||||
exiledHeroId,
|
||||
exilingFactionId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
CapturedHeroExiledEvent(
|
||||
capturedFromFactionId = capturedFromFactionId,
|
||||
provinceId = provinceId,
|
||||
exiledHeroId = exiledHeroId,
|
||||
exilingFactionId = exilingFactionId
|
||||
)
|
||||
case CapturedHeroExiledDetails(
|
||||
capturedFromFactionId,
|
||||
provinceId,
|
||||
exiledHeroId,
|
||||
exilingFactionId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
CapturedHeroExiledChronicleEvent(
|
||||
date = date,
|
||||
capturedFromFactionId = capturedFromFactionId,
|
||||
provinceId = provinceId,
|
||||
exiledHeroId = exiledHeroId,
|
||||
exilingFactionId = exilingFactionId
|
||||
)
|
||||
|
||||
case CapturedHeroExecutedDetails(
|
||||
capturedFromFactionId,
|
||||
provinceId,
|
||||
executedHeroId,
|
||||
executingFactionId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
CapturedHeroExecutedEvent(
|
||||
capturedFromFactionId = capturedFromFactionId,
|
||||
provinceId = provinceId,
|
||||
executedHeroId = executedHeroId,
|
||||
executingFactionId = executingFactionId
|
||||
)
|
||||
case CapturedHeroExecutedDetails(
|
||||
capturedFromFactionId,
|
||||
provinceId,
|
||||
executedHeroId,
|
||||
executingFactionId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
CapturedHeroExecutedChronicleEvent(
|
||||
date = date,
|
||||
capturedFromFactionId = capturedFromFactionId,
|
||||
provinceId = provinceId,
|
||||
executedHeroId = executedHeroId,
|
||||
executingFactionId = executingFactionId
|
||||
)
|
||||
|
||||
case CapturedHeroImprisonedDetails(
|
||||
capturedFromFactionId,
|
||||
provinceId,
|
||||
imprisonedHeroId,
|
||||
imprisoningFactionId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
CapturedHeroImprisonedEvent(
|
||||
capturedFromFactionId = capturedFromFactionId,
|
||||
provinceId = provinceId,
|
||||
imprisonedHeroId = imprisonedHeroId,
|
||||
imprisoningFactionId = imprisoningFactionId
|
||||
)
|
||||
case CapturedHeroImprisonedDetails(
|
||||
capturedFromFactionId,
|
||||
provinceId,
|
||||
imprisonedHeroId,
|
||||
imprisoningFactionId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
CapturedHeroImprisonedChronicleEvent(
|
||||
date = date,
|
||||
capturedFromFactionId = capturedFromFactionId,
|
||||
provinceId = provinceId,
|
||||
imprisonedHeroId = imprisonedHeroId,
|
||||
imprisoningFactionId = imprisoningFactionId
|
||||
)
|
||||
|
||||
case HeroDepartureDetails(
|
||||
departingHeroId,
|
||||
fromFactionId,
|
||||
provinceId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
HeroDepartedEvent(
|
||||
departingHeroId = departingHeroId,
|
||||
fromFactionId = fromFactionId,
|
||||
provinceId = provinceId
|
||||
)
|
||||
case HeroDepartureDetails(
|
||||
departingHeroId,
|
||||
fromFactionId,
|
||||
provinceId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
HeroDepartedChronicleEvent(
|
||||
date = date,
|
||||
departingHeroId = departingHeroId,
|
||||
fromFactionId = fromFactionId,
|
||||
provinceId = provinceId
|
||||
)
|
||||
|
||||
case SuppressBeastsFailedDetails(
|
||||
heroId,
|
||||
provinceId,
|
||||
_ /* singularName */,
|
||||
pluralName,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
SuppressBeastsFailedEvent(
|
||||
heroId = heroId,
|
||||
provinceId = provinceId,
|
||||
pluralName = pluralName
|
||||
)
|
||||
case SuppressBeastsFailedDetails(
|
||||
heroId,
|
||||
provinceId,
|
||||
singularName,
|
||||
pluralName,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
SuppressBeastsFailedChronicleEvent(
|
||||
date = date,
|
||||
heroId = heroId,
|
||||
provinceId = provinceId,
|
||||
singularName = singularName,
|
||||
pluralName = pluralName
|
||||
)
|
||||
|
||||
case PrisonerExecutedDetails(
|
||||
lastFactionId,
|
||||
provinceId,
|
||||
executedHeroId,
|
||||
executingFactionId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
PrisonerExecutedEvent(
|
||||
lastFactionId = lastFactionId,
|
||||
provinceId = provinceId,
|
||||
executedHeroId = executedHeroId,
|
||||
executingFactionId = executingFactionId
|
||||
)
|
||||
case PrisonerExecutedDetails(
|
||||
lastFactionId,
|
||||
provinceId,
|
||||
executedHeroId,
|
||||
executingFactionId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
PrisonerExecutedChronicleEvent(
|
||||
date = date,
|
||||
lastFactionId = lastFactionId,
|
||||
provinceId = provinceId,
|
||||
executedHeroId = executedHeroId,
|
||||
executingFactionId = executingFactionId
|
||||
)
|
||||
|
||||
case PrisonerExchangeDetails(
|
||||
hero1Id,
|
||||
hero1FactionId,
|
||||
hero1ProvinceId,
|
||||
hero2Id,
|
||||
hero2FactionId,
|
||||
hero2ProvinceId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
PrisonersExchangedEvent(
|
||||
hero1Id = hero1Id,
|
||||
hero1FactionId = hero1FactionId,
|
||||
hero1ProvinceId = hero1ProvinceId,
|
||||
hero2Id = hero2Id,
|
||||
hero2FactionId = hero2FactionId,
|
||||
hero2ProvinceId = hero2ProvinceId
|
||||
)
|
||||
case PrisonerExchangeDetails(
|
||||
hero1Id,
|
||||
hero1FactionId,
|
||||
hero1ProvinceId,
|
||||
hero2Id,
|
||||
hero2FactionId,
|
||||
hero2ProvinceId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
PrisonersExchangedChronicleEvent(
|
||||
date = date,
|
||||
hero1Id = hero1Id,
|
||||
hero1FactionId = hero1FactionId,
|
||||
hero1ProvinceId = hero1ProvinceId,
|
||||
hero2Id = hero2Id,
|
||||
hero2FactionId = hero2FactionId,
|
||||
hero2ProvinceId = hero2ProvinceId
|
||||
)
|
||||
|
||||
case ProvinceConqueredDetails(
|
||||
provinceId,
|
||||
conqueringFactionId,
|
||||
otherAttackingFactionIds,
|
||||
defendingFactionId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
ProvinceConqueredEvent(
|
||||
provinceId = provinceId,
|
||||
conqueringFactionId = conqueringFactionId,
|
||||
otherAttackingFactionIds = otherAttackingFactionIds,
|
||||
defendingFactionId = defendingFactionId
|
||||
)
|
||||
case ProvinceConqueredDetails(
|
||||
provinceId,
|
||||
conqueringFactionId,
|
||||
otherAttackingFactionIds,
|
||||
defendingFactionId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
ProvinceConqueredChronicleEvent(
|
||||
date = date,
|
||||
provinceId = provinceId,
|
||||
conqueringFactionId = conqueringFactionId,
|
||||
otherAttackingFactionIds = otherAttackingFactionIds.toVector,
|
||||
defendingFactionId = defendingFactionId
|
||||
)
|
||||
|
||||
case ProvinceExpansionDetails(
|
||||
provinceId,
|
||||
expandingFactionId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
ProvinceExpansionEvent(
|
||||
provinceId = provinceId,
|
||||
expandingFactionId = expandingFactionId
|
||||
)
|
||||
case ProvinceExpansionDetails(
|
||||
provinceId,
|
||||
expandingFactionId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
ProvinceExpansionChronicleEvent(
|
||||
date = date,
|
||||
provinceId = provinceId,
|
||||
expandingFactionId = expandingFactionId
|
||||
)
|
||||
|
||||
case ProvinceHeldDetails(
|
||||
provinceId,
|
||||
attackingFids,
|
||||
defendingFid,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
ProvinceHeldEvent(
|
||||
provinceId = provinceId,
|
||||
attackingFactionIds = attackingFids,
|
||||
defendingFactionId = defendingFid
|
||||
)
|
||||
case ProvinceHeldDetails(
|
||||
provinceId,
|
||||
attackingFids,
|
||||
defendingFid,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
ProvinceHeldChronicleEvent(
|
||||
date = date,
|
||||
provinceId = provinceId,
|
||||
attackingFactionIds = attackingFids.toVector,
|
||||
defendingFactionId = defendingFid
|
||||
)
|
||||
|
||||
case SwearBrotherhoodDetails(
|
||||
factionId,
|
||||
newBrotherHeroId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
SwearBrotherhoodEvent(
|
||||
factionId = factionId,
|
||||
newBrotherHeroId = newBrotherHeroId
|
||||
)
|
||||
case SwearBrotherhoodDetails(
|
||||
factionId,
|
||||
newBrotherHeroId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
SwearBrotherhoodChronicleEvent(
|
||||
date = date,
|
||||
factionId = factionId,
|
||||
newBrotherHeroId = newBrotherHeroId
|
||||
)
|
||||
|
||||
case InvitationAcceptedDetails(
|
||||
offeringFactionId,
|
||||
invitedFactionId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
InvitationAcceptedEvent(
|
||||
offeringFactionId = offeringFactionId,
|
||||
invitedFactionId = invitedFactionId
|
||||
)
|
||||
case InvitationAcceptedDetails(
|
||||
offeringFactionId,
|
||||
invitedFactionId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
InvitationAcceptedChronicleEvent(
|
||||
date = date,
|
||||
offeringFactionId = offeringFactionId,
|
||||
invitedFactionId = invitedFactionId
|
||||
)
|
||||
|
||||
case InvitationAmbassadorImprisonedDetails(
|
||||
offeringFactionId,
|
||||
invitedFactionId,
|
||||
ambassadorHeroId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
InvitationAmbassadorImprisonedEvent(
|
||||
offeringFactionId = offeringFactionId,
|
||||
invitedFactionId = invitedFactionId,
|
||||
ambassadorHeroId = ambassadorHeroId
|
||||
)
|
||||
case InvitationAmbassadorImprisonedDetails(
|
||||
offeringFactionId,
|
||||
invitedFactionId,
|
||||
ambassadorHeroId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
InvitationAmbassadorImprisonedChronicleEvent(
|
||||
date = date,
|
||||
offeringFactionId = offeringFactionId,
|
||||
invitedFactionId = invitedFactionId,
|
||||
ambassadorHeroId = ambassadorHeroId
|
||||
)
|
||||
|
||||
case TruceAcceptedDetails(
|
||||
offeringFactionId,
|
||||
targetFactionId,
|
||||
ambassadorHeroId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
TruceAcceptedEvent(
|
||||
offeringFactionId = offeringFactionId,
|
||||
targetFactionId = targetFactionId,
|
||||
ambassadorHeroId = ambassadorHeroId
|
||||
)
|
||||
case TruceAcceptedDetails(
|
||||
offeringFactionId,
|
||||
targetFactionId,
|
||||
ambassadorHeroId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
TruceAcceptedChronicleEvent(
|
||||
date = date,
|
||||
offeringFactionId = offeringFactionId,
|
||||
targetFactionId = targetFactionId,
|
||||
ambassadorHeroId = ambassadorHeroId
|
||||
)
|
||||
|
||||
case TruceAmbassadorImprisonedDetails(
|
||||
offeringFactionId,
|
||||
targetFactionId,
|
||||
ambassadorHeroId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
TruceAmbassadorImprisonedEvent(
|
||||
offeringFactionId = offeringFactionId,
|
||||
targetFactionId = targetFactionId,
|
||||
ambassadorHeroId = ambassadorHeroId
|
||||
)
|
||||
case TruceAmbassadorImprisonedDetails(
|
||||
offeringFactionId,
|
||||
targetFactionId,
|
||||
ambassadorHeroId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
TruceAmbassadorImprisonedChronicleEvent(
|
||||
date = date,
|
||||
offeringFactionId = offeringFactionId,
|
||||
targetFactionId = targetFactionId,
|
||||
ambassadorHeroId = ambassadorHeroId
|
||||
)
|
||||
|
||||
case BreakAllianceAcceptedDetails(
|
||||
offeringFactionId,
|
||||
targetFactionId,
|
||||
ambassadorHeroId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
BreakAllianceEvent(
|
||||
offeringFactionId = offeringFactionId,
|
||||
targetFactionId = targetFactionId,
|
||||
ambassadorHeroId = ambassadorHeroId
|
||||
)
|
||||
case BreakAllianceAcceptedDetails(
|
||||
offeringFactionId,
|
||||
targetFactionId,
|
||||
ambassadorHeroId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
BreakAllianceChronicleEvent(
|
||||
date = date,
|
||||
offeringFactionId = offeringFactionId,
|
||||
targetFactionId = targetFactionId,
|
||||
ambassadorHeroId = ambassadorHeroId
|
||||
)
|
||||
|
||||
case BreakAllianceAmbassadorImprisonedDetails(
|
||||
offeringFactionId,
|
||||
targetFactionId,
|
||||
ambassadorHeroId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
BreakAllianceAmbassadorImprisonedEvent(
|
||||
offeringFactionId = offeringFactionId,
|
||||
targetFactionId = targetFactionId,
|
||||
ambassadorHeroId = ambassadorHeroId
|
||||
)
|
||||
case BreakAllianceAmbassadorImprisonedDetails(
|
||||
offeringFactionId,
|
||||
targetFactionId,
|
||||
ambassadorHeroId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
BreakAllianceAmbassadorImprisonedChronicleEvent(
|
||||
date = date,
|
||||
offeringFactionId = offeringFactionId,
|
||||
targetFactionId = targetFactionId,
|
||||
ambassadorHeroId = ambassadorHeroId
|
||||
)
|
||||
|
||||
case OutlawApprehendedDetails(
|
||||
factionId,
|
||||
provinceId,
|
||||
apprehendedHeroId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
OutlawApprehendedEvent(
|
||||
factionId = factionId,
|
||||
provinceId = provinceId,
|
||||
apprehendedHeroId = apprehendedHeroId
|
||||
)
|
||||
case OutlawApprehendedDetails(
|
||||
factionId,
|
||||
provinceId,
|
||||
apprehendedHeroId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
OutlawApprehendedChronicleEvent(
|
||||
date = date,
|
||||
factionId = factionId,
|
||||
provinceId = provinceId,
|
||||
apprehendedHeroId = apprehendedHeroId
|
||||
)
|
||||
|
||||
case RansomPaidDetails(
|
||||
ransomedHeroId,
|
||||
ransomPaidByFactionId,
|
||||
ransomPaidToFactionId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
RansomPaidEvent(
|
||||
ransomedHeroId = ransomedHeroId,
|
||||
ransomPaidByFactionId = ransomPaidByFactionId,
|
||||
ransomPaidToFactionId = ransomPaidToFactionId
|
||||
)
|
||||
case RansomPaidDetails(
|
||||
ransomedHeroId,
|
||||
ransomPaidByFactionId,
|
||||
ransomPaidToFactionId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
RansomPaidChronicleEvent(
|
||||
date = date,
|
||||
ransomedHeroId = ransomedHeroId,
|
||||
ransomPaidByFactionId = ransomPaidByFactionId,
|
||||
ransomPaidToFactionId = ransomPaidToFactionId
|
||||
)
|
||||
|
||||
case RansomRejectedDetails(
|
||||
ransomedHeroId,
|
||||
ransomRefusedByFactionId,
|
||||
ransomOfferingFactionId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
RansomRejectedEvent(
|
||||
ransomedHeroId = ransomedHeroId,
|
||||
ransomRejectedByFactionId = ransomRefusedByFactionId,
|
||||
ransomOfferingFactionId = ransomOfferingFactionId
|
||||
)
|
||||
case RansomRejectedDetails(
|
||||
ransomedHeroId,
|
||||
ransomRefusedByFactionId,
|
||||
ransomOfferingFactionId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
RansomRejectedChronicleEvent(
|
||||
date = date,
|
||||
ransomedHeroId = ransomedHeroId,
|
||||
ransomRejectedByFactionId = ransomRefusedByFactionId,
|
||||
ransomOfferingFactionId = ransomOfferingFactionId
|
||||
)
|
||||
|
||||
case ShatteredArmyDetails(
|
||||
factionId,
|
||||
provinceId,
|
||||
reason,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
ShatteredArmyEvent(
|
||||
factionId = factionId,
|
||||
provinceId = provinceId,
|
||||
reason = reason match {
|
||||
case SHATTERED_ARMY_REASON_BLIZZARD =>
|
||||
SHATTERED_ARMY_EVENT_REASON_BLIZZARD
|
||||
case SHATTERED_ARMY_REASON_UNKNOWN_UNSPECIFIED =>
|
||||
SHATTERED_ARMY_EVENT_REASON_UNKNOWN_UNSPECIFIED
|
||||
case SHATTERED_ARMY_REASON_WITHDRAW =>
|
||||
SHATTERED_ARMY_EVENT_REASON_WITHDRAW
|
||||
case Unrecognized(value) =>
|
||||
ShatteredArmyEvent.Reason.Unrecognized(value)
|
||||
}
|
||||
)
|
||||
case ShatteredArmyDetails(
|
||||
factionId,
|
||||
provinceId,
|
||||
reason,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
ShatteredArmyChronicleEvent(
|
||||
date = date,
|
||||
factionId = factionId,
|
||||
provinceId = provinceId,
|
||||
reason = reason match {
|
||||
case SHATTERED_ARMY_REASON_BLIZZARD =>
|
||||
ShatteredArmyChronicleEvent.Blizzard
|
||||
case SHATTERED_ARMY_REASON_WITHDRAW =>
|
||||
ShatteredArmyChronicleEvent.Withdraw
|
||||
case _ =>
|
||||
ShatteredArmyChronicleEvent.Withdraw // Default fallback
|
||||
}
|
||||
)
|
||||
|
||||
case VassalExiledDetails(
|
||||
factionId,
|
||||
provinceId,
|
||||
exiledHeroId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
VassalExiledEvent(
|
||||
factionId = factionId,
|
||||
provinceId = provinceId,
|
||||
exiledHeroId = exiledHeroId
|
||||
)
|
||||
}
|
||||
|
||||
detailList.map { details =>
|
||||
EventForChronicle(
|
||||
awrs.gameState.currentDate,
|
||||
details = details
|
||||
)
|
||||
}
|
||||
case VassalExiledDetails(
|
||||
factionId,
|
||||
provinceId,
|
||||
exiledHeroId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
VassalExiledChronicleEvent(
|
||||
date = date,
|
||||
factionId = factionId,
|
||||
provinceId = provinceId,
|
||||
exiledHeroId = exiledHeroId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private def basicActionResultEntries(
|
||||
awrs: ActionWithResultingState
|
||||
): Vector[EventForChronicle] =
|
||||
): Vector[ChronicleEvent] =
|
||||
awrs.actionResult.`type` match {
|
||||
case FACTION_DESTROYED =>
|
||||
val date = DateConverter.fromProto(awrs.gameState.currentDate)
|
||||
awrs.actionResult.removedFactionIds.map { factionId =>
|
||||
EventForChronicle(
|
||||
date = awrs.gameState.currentDate,
|
||||
details = FactionDestroyedEvent(factionId = factionId)
|
||||
FactionDestroyedChronicleEvent(
|
||||
date = date,
|
||||
factionId = factionId
|
||||
)
|
||||
}.toVector
|
||||
|
||||
@@ -407,7 +390,7 @@ object ChronicleEventGenerator {
|
||||
def eventTextEntries(
|
||||
gameHistory: GameHistory,
|
||||
since: Date
|
||||
): Vector[EventForChronicle] =
|
||||
): Vector[ChronicleEvent] =
|
||||
gameHistory.sinceDate(since).flatMap { (awrs: ActionWithResultingState) =>
|
||||
notificationEntries(awrs) ++ basicActionResultEntries(awrs)
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ import net.eagle0.eagle.model.action_result.concrete.{
|
||||
import net.eagle0.eagle.model.action_result.generated_text_request.{ChronicleUpdatePreviousEntry, LlmRequestT}
|
||||
import net.eagle0.eagle.model.action_result.types.NewRoundActionResultType
|
||||
import net.eagle0.eagle.model.action_result.ActionResultT
|
||||
import net.eagle0.eagle.model.proto_converters.generated_text_request.chronicle_event.ChronicleEventConverter
|
||||
import net.eagle0.eagle.model.state.chronicle_entry.ChronicleEntry
|
||||
import net.eagle0.eagle.model.state.date.Date
|
||||
import net.eagle0.eagle.model.state.faction.FactionRelationship
|
||||
@@ -117,14 +116,12 @@ case class NewRoundAction(
|
||||
generatedTextId = entry.generatedTextId
|
||||
)
|
||||
},
|
||||
new_entries = ChronicleEventGenerator
|
||||
.eventTextEntries(
|
||||
gameHistory = gameHistory,
|
||||
since = gs.chronicleEntries.lastOption
|
||||
.map(_.date)
|
||||
.getOrElse(Date(year = 0, month = Date.Month.January))
|
||||
)
|
||||
.map(ChronicleEventConverter.fromProto)
|
||||
new_entries = ChronicleEventGenerator.eventTextEntries(
|
||||
gameHistory = gameHistory,
|
||||
since = gs.chronicleEntries.lastOption
|
||||
.map(_.date)
|
||||
.getOrElse(Date(year = 0, month = Date.Month.January))
|
||||
)
|
||||
)
|
||||
)
|
||||
else Vector()
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
package net.eagle0.eagle.library.actions.impl
|
||||
|
||||
import net.eagle0.eagle.internal.action_result.ActionResult
|
||||
import scalapb.lenses
|
||||
import scalapb.lenses.Lens
|
||||
|
||||
package object action {
|
||||
type ActionResultProtoUpdater =
|
||||
Lens[ActionResult, ActionResult] => lenses.Mutation[ActionResult]
|
||||
}
|
||||
@@ -1410,7 +1410,6 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:train_charisma_xp",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:train_stat_divisor",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:train_strength_xp",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:battalion_type_finder",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:eagle_require",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:notification_trait",
|
||||
|
||||
@@ -189,7 +189,6 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:vigor_to_constitution_xp_multiplier",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
|
||||
|
||||
@@ -1,22 +1,10 @@
|
||||
package net.eagle0.eagle.library.actions.impl.common
|
||||
|
||||
import net.eagle0.eagle.internal.action_result.ActionResult
|
||||
import net.eagle0.eagle.library.settings.VigorToConstitutionXpMultiplier
|
||||
import net.eagle0.eagle.model.action_result.concrete.{ChangedHeroC, StatAbsolute, StatDelta, StatNoChange}
|
||||
import net.eagle0.eagle.model.action_result.ActionResultT
|
||||
|
||||
object VigorXPApplier {
|
||||
def withVigorXp(actionResult: ActionResult): ActionResult =
|
||||
actionResult.update(
|
||||
_.changedHeroes.modify(_.map { ch =>
|
||||
ch.update(
|
||||
_.optionalConstitutionXpDelta := ch.vigor.vigorDelta
|
||||
.filter(_ < 0)
|
||||
.map(vigor => (vigor * -VigorToConstitutionXpMultiplier.doubleValue).ceil.toInt)
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
def withVigorXp(actionResult: ActionResultT): ActionResultT =
|
||||
actionResult.copy(
|
||||
changedHeroes = actionResult.changedHeroes.map {
|
||||
|
||||
@@ -347,6 +347,24 @@ scala_library(
|
||||
deps = [],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "new_faction_head_prompt_generator",
|
||||
srcs = ["NewFactionHeadPromptGenerator.scala"],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
":faction_info_utilities",
|
||||
":generator_utilities",
|
||||
":hero_description_generator",
|
||||
":hero_info_utilities",
|
||||
":llm_prompt_generator",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:client_text_store",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:text_generation_result",
|
||||
],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "please_recruit_me_prompt_generator",
|
||||
srcs = ["PleaseRecruitMePromptGenerator.scala"],
|
||||
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package net.eagle0.eagle.library.actions.llm_prompt_generators
|
||||
|
||||
import net.eagle0.eagle.client_text.{ClientTextStore, TextGenerationResult}
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.internal.generated_text_request.NewFactionHeadMessage
|
||||
|
||||
case class NewFactionHeadPromptGenerator(
|
||||
newFactionHeadMessage: NewFactionHeadMessage,
|
||||
gameState: GameState,
|
||||
clientTextStore: ClientTextStore
|
||||
) extends LLMPromptGenerator {
|
||||
private val newHeadHero = HeroInfoUtilities.getHero(
|
||||
newFactionHeadMessage.newHeadHeroId,
|
||||
gameState
|
||||
)
|
||||
private val previousHeadHero = HeroInfoUtilities.getHero(
|
||||
newFactionHeadMessage.previousHeadHeroId,
|
||||
gameState
|
||||
)
|
||||
private val faction = FactionInfoUtilities.getFaction(
|
||||
newFactionHeadMessage.factionId,
|
||||
gameState
|
||||
)
|
||||
|
||||
override def generate: TextGenerationResult = for {
|
||||
newHeadHeroDescription <- HeroDescriptionGenerator.description(
|
||||
heroId = newFactionHeadMessage.newHeadHeroId,
|
||||
gameState = gameState,
|
||||
clientTextStore = clientTextStore
|
||||
)
|
||||
previousHeadHeroDescription <- HeroDescriptionGenerator.description(
|
||||
heroId = newFactionHeadMessage.previousHeadHeroId,
|
||||
gameState = gameState,
|
||||
clientTextStore = clientTextStore
|
||||
)
|
||||
newHeadHeroName <- clientTextStore.getText(newHeadHero.nameTextId)
|
||||
previousHeadHeroName <- clientTextStore.getText(previousHeadHero.nameTextId)
|
||||
setup <- GeneratorUtilities.basicSetup(gameState, clientTextStore)
|
||||
} yield {
|
||||
val previousFactionName = newFactionHeadMessage.previousFactionName
|
||||
val newFactionName = Option(newFactionHeadMessage.newFactionName).filter(_.nonEmpty)
|
||||
|
||||
val renameClause = newFactionName match {
|
||||
case Some(newName) =>
|
||||
s" $newHeadHeroName is also renaming the faction from \"$previousFactionName\" to \"$newName\"."
|
||||
case None => ""
|
||||
}
|
||||
|
||||
val possP = HeroDescriptionGenerator.possessivePronoun(newHeadHero.pronounGender)
|
||||
|
||||
val inputText =
|
||||
s"""$setup
|
||||
|
|
||||
|NEW FACTION HEAD:
|
||||
|$newHeadHeroDescription
|
||||
|
|
||||
|PREVIOUS FACTION HEAD (EXECUTED):
|
||||
|$previousHeadHeroDescription
|
||||
|
|
||||
|$previousHeadHeroName, the previous head of the faction "$previousFactionName", has been executed.
|
||||
|$newHeadHeroName has now become the new head of the faction.$renameClause
|
||||
|
|
||||
|Write a message from $newHeadHeroName to be delivered to the world, declaring $possP new leadership
|
||||
|in the wake of $previousHeadHeroName's execution. The message should reflect $possP personality and
|
||||
|ambitions, and may reference the circumstances of $possP rise to power or their relationship with
|
||||
|the previous leader. If the faction is being renamed, the declaration should explain why the new
|
||||
|name represents $possP vision for the faction.
|
||||
|
|
||||
|${GeneratorUtilities.limitations}""".stripMargin
|
||||
|
||||
inputText
|
||||
}
|
||||
}
|
||||
+35
@@ -1,6 +1,7 @@
|
||||
package net.eagle0.eagle.library.actions.random_state_sequencer
|
||||
|
||||
import net.eagle0.common.{FunctionalRandom, RandomState}
|
||||
import net.eagle0.eagle.api.selected_command.SelectedCommand
|
||||
import net.eagle0.eagle.library.actions.applier.{ActionResultApplier, ActionResultWithResultingState}
|
||||
import net.eagle0.eagle.library.actions.impl.common.{
|
||||
ProtolessRandomSimpleAction,
|
||||
@@ -79,6 +80,15 @@ trait RandomStateSequencer {
|
||||
case TCommand.RandomSimple(action) => withProtolessRandomSimpleAction(_ => action)
|
||||
case TCommand.Sequential(action) => withProtolessSequentialResultsAction(_ => action)
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a TCommand and wraps the first result with lastCommandTypeForActingProvince. This mimics the old
|
||||
* Command.resultWithLastCommand behavior.
|
||||
*/
|
||||
def withTCommandAndLastCommand(
|
||||
commandGen: GameState => TCommand,
|
||||
selectedCommand: SelectedCommand
|
||||
): RandomStateSequencer
|
||||
}
|
||||
|
||||
object RandomStateSequencer {
|
||||
@@ -176,4 +186,29 @@ private case class RandomStateSequencerImpl(
|
||||
case (sequencer, t) =>
|
||||
sequencer.withRandomActionResult((gs, fr) => f(t, gs, fr))
|
||||
}
|
||||
|
||||
override def withTCommandAndLastCommand(
|
||||
commandGen: GameState => TCommand,
|
||||
selectedCommand: SelectedCommand
|
||||
): RandomStateSequencer = {
|
||||
def wrapWithLastCommand(result: ActionResultT): ActionResultT =
|
||||
result.withLastCommandTypeForActingProvince(selectedCommand)
|
||||
|
||||
commandGen(lastState) match {
|
||||
case TCommand.Simple(action) =>
|
||||
withActionResult(_ => wrapWithLastCommand(action.immediateExecute))
|
||||
|
||||
case TCommand.RandomSimple(action) =>
|
||||
withRandomActionResult((gs, fr) => action.immediateExecute(fr).map(wrapWithLastCommand))
|
||||
|
||||
case TCommand.Sequential(action) =>
|
||||
action.results.toVector match {
|
||||
case head +: tail =>
|
||||
withActionResult(_ => wrapWithLastCommand(head))
|
||||
.withActionResults(_ => tail)
|
||||
case empty =>
|
||||
this
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,43 +35,6 @@ scala_library(
|
||||
],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "battalion_suitability",
|
||||
srcs = ["BattalionSuitability.scala"],
|
||||
visibility = [
|
||||
"__subpackages__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/util:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/api/command/util:appropriate_battalions_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:battalion_type_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:profession_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:battalion_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:hero_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "battalion_type_finder",
|
||||
srcs = ["BattalionTypeFinder.scala"],
|
||||
visibility = [
|
||||
"//src/main/scala/net/eagle0/eagle/ai:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/library:__subpackages__",
|
||||
"//src/main/scala/net/eagle0/util:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/library:__subpackages__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:battalion_type_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type_id",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:battalion_type_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type_id",
|
||||
],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "battalion_power",
|
||||
srcs = ["BattalionPower.scala"],
|
||||
|
||||
@@ -194,7 +194,8 @@ object GameStateViewDiffer {
|
||||
changedFactionRelationships = after.factionRelationships.diff(before.factionRelationships),
|
||||
addedIncomingDiplomacyOffers = after.incomingDiplomacyOffers.diff(before.incomingDiplomacyOffers),
|
||||
removedIncomingDiplomacyOffers = before.incomingDiplomacyOffers.diff(after.incomingDiplomacyOffers),
|
||||
newFocusProvinceId = after.focusProvinceId
|
||||
newFocusProvinceId = after.focusProvinceId,
|
||||
newName = optionalDiff(before.name, after.name)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
load("@rules_scala//scala:scala.bzl", "scala_library")
|
||||
|
||||
scala_library(
|
||||
name = "battalion_suitability",
|
||||
srcs = ["BattalionSuitability.scala"],
|
||||
visibility = [
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/util:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/util/battalion_suitability:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero",
|
||||
],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "legacy_battalion_suitability",
|
||||
srcs = ["LegacyBattalionSuitability.scala"],
|
||||
visibility = [
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/util:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/util/battalion_suitability:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/api/command/util:appropriate_battalions_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero",
|
||||
],
|
||||
)
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package net.eagle0.eagle.library.util.battalion_suitability
|
||||
|
||||
import net.eagle0.eagle.{BattalionId, HeroId}
|
||||
import net.eagle0.eagle.model.state.battalion.BattalionT
|
||||
import net.eagle0.eagle.model.state.hero.HeroT
|
||||
import net.eagle0.eagle.model.state.hero.Profession.{Engineer, Mage, Necromancer, Ranger}
|
||||
import net.eagle0.eagle.model.state.BattalionType
|
||||
|
||||
enum SuitabilityLevel:
|
||||
case Optimal, Suboptimal, Restrictive
|
||||
|
||||
case class BattalionIdWithSuitability(
|
||||
battalionId: BattalionId,
|
||||
level: SuitabilityLevel
|
||||
)
|
||||
|
||||
case class SuitableBattalions(
|
||||
battalionIdsWithSuitability: Vector[BattalionIdWithSuitability]
|
||||
)
|
||||
|
||||
object BattalionSuitability {
|
||||
def suitabilityLevel(h: HeroT, bt: BattalionType): SuitabilityLevel =
|
||||
h.profession match {
|
||||
case Mage | Necromancer =>
|
||||
if !bt.allowsCasting then SuitabilityLevel.Restrictive
|
||||
else if bt.alwaysArcheryCapable then SuitabilityLevel.Suboptimal
|
||||
else SuitabilityLevel.Optimal
|
||||
|
||||
case Engineer =>
|
||||
if bt.alwaysArcheryCapable then SuitabilityLevel.Suboptimal
|
||||
else SuitabilityLevel.Optimal
|
||||
|
||||
case Ranger =>
|
||||
if !bt.allowsStealth then SuitabilityLevel.Restrictive
|
||||
else SuitabilityLevel.Optimal
|
||||
|
||||
case _ => SuitabilityLevel.Optimal
|
||||
}
|
||||
|
||||
def suitableBattalions(
|
||||
h: HeroT,
|
||||
bs: Vector[BattalionT],
|
||||
bts: Vector[BattalionType]
|
||||
): SuitableBattalions =
|
||||
SuitableBattalions(
|
||||
battalionIdsWithSuitability = bs.map(b =>
|
||||
BattalionIdWithSuitability(
|
||||
battalionId = b.id,
|
||||
level = BattalionSuitability
|
||||
.suitabilityLevel(h, bts.find(_.typeId == b.typeId).get)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
def suitableBattalionsForHeroes(
|
||||
hs: Vector[HeroT],
|
||||
bs: Vector[BattalionT],
|
||||
bts: Vector[BattalionType]
|
||||
): Map[HeroId, SuitableBattalions] =
|
||||
hs.map(h => h.id -> suitableBattalions(h, bs, bts)).toMap
|
||||
}
|
||||
+16
-16
@@ -1,4 +1,4 @@
|
||||
package net.eagle0.eagle.library.util
|
||||
package net.eagle0.eagle.library.util.battalion_suitability
|
||||
|
||||
import net.eagle0.eagle.api.command.util.appropriate_battalions.SuitableBattalions
|
||||
import net.eagle0.eagle.api.command.util.appropriate_battalions.SuitableBattalions.{
|
||||
@@ -10,25 +10,25 @@ import net.eagle0.eagle.api.command.util.appropriate_battalions.SuitableBattalio
|
||||
RESTRICTIVE,
|
||||
SUBOPTIMAL
|
||||
}
|
||||
import net.eagle0.eagle.common.battalion_type.BattalionType
|
||||
import net.eagle0.eagle.common.profession.Profession.{ENGINEER, MAGE, NECROMANCER, RANGER}
|
||||
import net.eagle0.eagle.internal.battalion.Battalion
|
||||
import net.eagle0.eagle.internal.hero.Hero
|
||||
import net.eagle0.eagle.model.state.battalion.BattalionT
|
||||
import net.eagle0.eagle.model.state.hero.HeroT
|
||||
import net.eagle0.eagle.model.state.hero.Profession.{Engineer, Mage, Necromancer, Ranger}
|
||||
import net.eagle0.eagle.model.state.BattalionType
|
||||
import net.eagle0.eagle.HeroId
|
||||
|
||||
object BattalionSuitability {
|
||||
def suitabilityLevel(h: Hero, bt: BattalionType): SuitabilityLevel =
|
||||
object LegacyBattalionSuitability {
|
||||
def suitabilityLevel(h: HeroT, bt: BattalionType): SuitabilityLevel =
|
||||
h.profession match {
|
||||
case MAGE | NECROMANCER =>
|
||||
case Mage | Necromancer =>
|
||||
if !bt.allowsCasting then RESTRICTIVE
|
||||
else if bt.alwaysArcheryCapable then SUBOPTIMAL
|
||||
else OPTIMAL
|
||||
|
||||
case ENGINEER =>
|
||||
case Engineer =>
|
||||
if bt.alwaysArcheryCapable then SUBOPTIMAL
|
||||
else OPTIMAL
|
||||
|
||||
case RANGER =>
|
||||
case Ranger =>
|
||||
if !bt.allowsStealth then RESTRICTIVE
|
||||
else OPTIMAL
|
||||
|
||||
@@ -36,23 +36,23 @@ object BattalionSuitability {
|
||||
}
|
||||
|
||||
def suitableBattalions(
|
||||
h: Hero,
|
||||
bs: Vector[Battalion],
|
||||
h: HeroT,
|
||||
bs: Vector[BattalionT],
|
||||
bts: Vector[BattalionType]
|
||||
): SuitableBattalions =
|
||||
SuitableBattalions(
|
||||
battalionIdsWithSuitability = bs.map(b =>
|
||||
BattalionIdWithSuitability(
|
||||
battalionId = b.id,
|
||||
level = BattalionSuitability
|
||||
.suitabilityLevel(h, bts.find(_.typeId == b.`type`).get)
|
||||
level = LegacyBattalionSuitability
|
||||
.suitabilityLevel(h, bts.find(_.typeId == b.typeId).get)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
def suitableBattalionsForHeroes(
|
||||
hs: Vector[Hero],
|
||||
bs: Vector[Battalion],
|
||||
hs: Vector[HeroT],
|
||||
bs: Vector[BattalionT],
|
||||
bts: Vector[BattalionType]
|
||||
): Map[HeroId, SuitableBattalions] =
|
||||
hs.map(h => h.id -> suitableBattalions(h, bs, bts)).toMap
|
||||
@@ -0,0 +1,39 @@
|
||||
load("@rules_scala//scala:scala.bzl", "scala_library")
|
||||
|
||||
scala_library(
|
||||
name = "battalion_type_finder",
|
||||
srcs = ["BattalionTypeFinder.scala"],
|
||||
visibility = [
|
||||
"//src/main/scala/net/eagle0/eagle/ai:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/library:__subpackages__",
|
||||
"//src/main/scala/net/eagle0/util:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/library:__subpackages__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type_id",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type_id",
|
||||
],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "legacy_battalion_type_finder",
|
||||
srcs = ["LegacyBattalionTypeFinder.scala"],
|
||||
visibility = [
|
||||
"//src/main/scala/net/eagle0/eagle/ai:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/library:__subpackages__",
|
||||
"//src/main/scala/net/eagle0/util:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/library:__subpackages__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:battalion_type_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type_id",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:battalion_type_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type_id",
|
||||
],
|
||||
)
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package net.eagle0.eagle.library.util.battalion_type_finder
|
||||
|
||||
import net.eagle0.eagle.model.state.{BattalionType, BattalionTypeId}
|
||||
|
||||
object BattalionTypeFinder {
|
||||
def battalionType(
|
||||
battalionTypeId: BattalionTypeId,
|
||||
battalionTypes: Vector[BattalionType]
|
||||
): BattalionType =
|
||||
battalionTypes.find(_.typeId == battalionTypeId).get
|
||||
}
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
package net.eagle0.eagle.library.util
|
||||
package net.eagle0.eagle.library.util.battalion_type_finder
|
||||
|
||||
import net.eagle0.eagle.common.battalion_type.{
|
||||
BattalionType as BattalionTypeProto,
|
||||
@@ -6,7 +6,7 @@ import net.eagle0.eagle.common.battalion_type.{
|
||||
}
|
||||
import net.eagle0.eagle.model.state.BattalionTypeId
|
||||
|
||||
object BattalionTypeFinder {
|
||||
object LegacyBattalionTypeFinder {
|
||||
def battalionType(
|
||||
battalionTypeId: BattalionTypeId,
|
||||
battalionTypes: Vector[BattalionTypeProto]
|
||||
+3
-3
@@ -4,17 +4,17 @@ import net.eagle0.eagle.{FactionId, HeroId}
|
||||
import net.eagle0.eagle.api.available_command.{AvailableCommand, DiplomacyAvailableCommand}
|
||||
import net.eagle0.eagle.api.command.util.diplomacy_option.{AllianceOption, DiplomacyOption}
|
||||
import net.eagle0.eagle.api.selected_command.DiplomacySelectedCommand
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.AvailableCommandSelector.ofType
|
||||
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
|
||||
import net.eagle0.eagle.library.util.faction_utils.FactionUtils
|
||||
import net.eagle0.eagle.library.util.CommandSelection
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
|
||||
object AllianceOfferCommandSelector {
|
||||
private def usableHeroIds(
|
||||
hids: Vector[HeroId],
|
||||
gameState: GameState
|
||||
): Vector[HeroId] =
|
||||
hids.filterNot(LegacyFactionUtils.isFactionLeader(_, gameState))
|
||||
hids.filterNot(FactionUtils.isFactionLeader(_, gameState.factions.values.toVector))
|
||||
|
||||
private def allianceOptionWithTarget(
|
||||
options: Vector[DiplomacyOption],
|
||||
|
||||
+20
-20
@@ -3,41 +3,40 @@ package net.eagle0.eagle.library.util.command_choice_helpers
|
||||
import net.eagle0.eagle.{FactionId, ProvinceId}
|
||||
import net.eagle0.eagle.api.available_command.{AlmsAvailableCommand, AvailableCommand}
|
||||
import net.eagle0.eagle.api.selected_command.AlmsSelectedCommand
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.internal.hero.Hero
|
||||
import net.eagle0.eagle.internal.province.Province
|
||||
import net.eagle0.eagle.library.actions.impl.command.AlmsCommand
|
||||
import net.eagle0.eagle.library.settings.MinSupportForTaxes
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.AvailableCommandSelector.flatSelectionForType
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.FoodConsumptionUtils.foodConsumptionMonthsToHold
|
||||
import net.eagle0.eagle.library.util.hero.LegacyHeroUtils
|
||||
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
|
||||
import net.eagle0.eagle.library.util.food_consumption.FoodConsumptionUtils.foodConsumptionMonthsToHold
|
||||
import net.eagle0.eagle.library.util.hero.HeroUtils
|
||||
import net.eagle0.eagle.library.util.province.ProvinceUtils
|
||||
import net.eagle0.eagle.library.util.CommandSelection
|
||||
import net.eagle0.eagle.model.proto_converters.hero.HeroConverter
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.hero.HeroT
|
||||
import net.eagle0.eagle.model.state.province.ProvinceT
|
||||
|
||||
object AlmsCommandSelector {
|
||||
|
||||
private def bestHeroWithSupport(
|
||||
availableHeroes: Vector[Hero]
|
||||
): (Hero, Double) =
|
||||
availableHeroes: Vector[HeroT]
|
||||
): (HeroT, Double) =
|
||||
availableHeroes
|
||||
.map(h => (h, AlmsCommand.supportIncreasePerFood(HeroConverter.fromProto(h))))
|
||||
.map(h => (h, AlmsCommand.supportIncreasePerFood(h)))
|
||||
.maxBy {
|
||||
case (hero, inc) =>
|
||||
(inc, -LegacyHeroUtils.fatigue(hero))
|
||||
(inc, -HeroUtils.fatigue(hero))
|
||||
}
|
||||
|
||||
private def neededSupport(province: Province): Double =
|
||||
private def neededSupport(province: ProvinceT): Double =
|
||||
MinSupportForTaxes.doubleValue - province.support
|
||||
|
||||
def surplusFood(province: Province, gameState: GameState): Int =
|
||||
def surplusFood(province: ProvinceT, gameState: GameState): Int =
|
||||
Math.max(
|
||||
0,
|
||||
province.food - foodConsumptionMonthsToHold(
|
||||
provinceId = province.id,
|
||||
province = province,
|
||||
gameState = gameState
|
||||
) * LegacyProvinceUtils
|
||||
.monthlyFoodConsumption(province.id, gameState)
|
||||
) * ProvinceUtils
|
||||
.monthlyFoodConsumption(province, gameState)
|
||||
)
|
||||
|
||||
def chosenAlmsForSupportCommand(
|
||||
@@ -92,12 +91,12 @@ object AlmsCommandSelector {
|
||||
)
|
||||
}
|
||||
|
||||
def maybeAlmsCommandForProvince(
|
||||
private def maybeAlmsCommandForProvince(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
availableCommand: AlmsAvailableCommand,
|
||||
minimumFood: Int,
|
||||
province: Province
|
||||
province: ProvinceT
|
||||
): Option[CommandSelection] = {
|
||||
val foodAfterFeedingTroops = surplusFood(province, gameState)
|
||||
val effectiveMinimumFood = minimumFood.min(foodAfterFeedingTroops)
|
||||
@@ -109,7 +108,7 @@ object AlmsCommandSelector {
|
||||
)
|
||||
|
||||
val neededSupportPerMonth =
|
||||
neededSupport(province) / (13 - gameState.currentDate.get.month)
|
||||
neededSupport(province) / (13 - gameState.currentDate.get.month.value)
|
||||
val neededFoodForSupport =
|
||||
neededSupport(province) / supportPerFood
|
||||
val neededFoodPerMonth =
|
||||
@@ -123,7 +122,7 @@ object AlmsCommandSelector {
|
||||
|
||||
val isPartialGift =
|
||||
neededFoodForSupport > availableCommand.foodAvailable
|
||||
val isDecember = gameState.currentDate.get.month == 12
|
||||
val isDecember = gameState.currentDate.get.month.value == 12
|
||||
val haveExcessToQuellRiot =
|
||||
(foodAfterFeedingTroops - neededFoodForSupport > 1000) || province.gold > 700
|
||||
|
||||
@@ -152,4 +151,5 @@ object AlmsCommandSelector {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+22
-19
@@ -4,22 +4,21 @@ import net.eagle0.eagle.{FactionId, ProvinceId}
|
||||
import net.eagle0.eagle.api.available_command.*
|
||||
import net.eagle0.eagle.api.selected_command.*
|
||||
import net.eagle0.eagle.common.combat_unit.CombatUnit
|
||||
import net.eagle0.eagle.common.profession.Profession.{ENGINEER, MAGE}
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.internal.hero.Hero
|
||||
import net.eagle0.eagle.internal.province.Province
|
||||
import net.eagle0.eagle.library.settings.{
|
||||
MinVigorForMarch,
|
||||
RequiredRatioToAttack,
|
||||
RequiredRatioToAttackWithoutTakingCastles
|
||||
}
|
||||
import net.eagle0.eagle.library.util.{CommandSelection, LegacyBattalionUtils, ShardokMapInfo}
|
||||
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
|
||||
import net.eagle0.eagle.library.util.{BattalionPower, CommandSelection, LegacyBattalionUtils, ShardokMapInfo}
|
||||
import net.eagle0.eagle.library.util.faction_utils.FactionUtils
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.hero.HeroT
|
||||
import net.eagle0.eagle.model.state.hero.Profession.{Engineer, Mage}
|
||||
import net.eagle0.eagle.model.state.province.ProvinceT
|
||||
import net.eagle0.eagle.views.battalion_view.BattalionView
|
||||
|
||||
object AttackCommandChooser {
|
||||
|
||||
import net.eagle0.eagle.library.util.DateProtoUtils._Date
|
||||
import AvailableCommandSelector.flatSelectionForType
|
||||
|
||||
private val winterMonths = Set(12, 1, 2)
|
||||
@@ -27,7 +26,7 @@ object AttackCommandChooser {
|
||||
private def heroesWithSufficientVigorForAttack(
|
||||
provinceId: ProvinceId,
|
||||
gs: GameState
|
||||
): Vector[Hero] =
|
||||
): Vector[HeroT] =
|
||||
gs.provinces(provinceId)
|
||||
.rulingFactionHeroIds
|
||||
.map(gs.heroes)
|
||||
@@ -41,10 +40,13 @@ object AttackCommandChooser {
|
||||
gameState: GameState
|
||||
): Boolean =
|
||||
destinationFactionId.exists(dfid =>
|
||||
dfid != actingFactionId && !LegacyFactionUtils
|
||||
.hasAlliance(actingFactionId, dfid, gameState) && !LegacyFactionUtils
|
||||
.truceExpirationDate(dfid, actingFactionId, gameState)
|
||||
.exists(_ > gameState.currentDate.get)
|
||||
dfid != actingFactionId && !FactionUtils
|
||||
.hasAlliance(actingFactionId, dfid, gameState.factions.values.toVector) && !FactionUtils
|
||||
.factionRelationship(dfid, actingFactionId, gameState.factions.values)
|
||||
.resetDate
|
||||
.exists(d =>
|
||||
d.year > gameState.currentDate.get.year || (d.year == gameState.currentDate.get.year && d.month.value > gameState.currentDate.get.month.value)
|
||||
)
|
||||
)
|
||||
|
||||
// Calculates the best attack for a particular destination, not taking into account whether it's a good idea --
|
||||
@@ -56,7 +58,7 @@ object AttackCommandChooser {
|
||||
goldAvailable: Int,
|
||||
foodAvailable: Int,
|
||||
originProvinceId: ProvinceId,
|
||||
destinationProvince: Province,
|
||||
destinationProvince: ProvinceT,
|
||||
gameState: GameState
|
||||
): MarchSelectedCommand = MarchSelectedCommand(
|
||||
marchingUnits = selectedUnits,
|
||||
@@ -81,7 +83,7 @@ object AttackCommandChooser {
|
||||
goldAvailable: Int,
|
||||
foodAvailable: Int,
|
||||
originProvinceId: ProvinceId,
|
||||
destinationProvince: Province,
|
||||
destinationProvince: ProvinceT,
|
||||
requiresRiverCrossing: Boolean,
|
||||
totalBattalionPower: Double
|
||||
)
|
||||
@@ -124,7 +126,7 @@ object AttackCommandChooser {
|
||||
desiredCount = unitCount,
|
||||
heroes = heroes,
|
||||
battalions = battalions.toVector,
|
||||
battalionTypes = gameState.battalionTypes.toVector
|
||||
battalionTypes = gameState.battalionTypes
|
||||
)
|
||||
|
||||
// Get river crossing requirement from the available destination
|
||||
@@ -140,7 +142,7 @@ object AttackCommandChooser {
|
||||
totalBattalionPower = selectedUnits
|
||||
.flatMap(_.battalionId)
|
||||
.map(gameState.battalions)
|
||||
.map(LegacyBattalionUtils.power)
|
||||
.map(BattalionPower.power)
|
||||
.sum
|
||||
)
|
||||
}).toVector
|
||||
@@ -162,7 +164,7 @@ object AttackCommandChooser {
|
||||
.filter {
|
||||
_.destinationProvince.incomingArmies
|
||||
.exists(incomingArmy =>
|
||||
incomingArmy.getArmy.factionId == actingFactionId && incomingArmy.arrivalRound > gameState.currentRoundId
|
||||
incomingArmy.army.factionId == actingFactionId && incomingArmy.arrivalRound > gameState.currentRoundId
|
||||
)
|
||||
}
|
||||
.map {
|
||||
@@ -258,12 +260,12 @@ object AttackCommandChooser {
|
||||
.map(u => u.heroId)
|
||||
.map(gameState.heroes)
|
||||
.map(_.profession)
|
||||
.exists(prof => prof == ENGINEER || prof == MAGE)
|
||||
.exists(prof => prof == Engineer || prof == Mage)
|
||||
|
||||
// If it's winter or we have a river crossing profession, take everything.
|
||||
// Otherwise, only take those that don't require a river crossing.
|
||||
val isWinter =
|
||||
winterMonths.contains(gameState.currentDate.get.month)
|
||||
winterMonths.contains(gameState.currentDate.get.month.value)
|
||||
|
||||
val allMarchingCIs =
|
||||
if isWinter || hasRiverCrossingProfession then commandInfos
|
||||
@@ -316,4 +318,5 @@ object AttackCommandChooser {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -11,10 +11,10 @@ scala_library(
|
||||
":available_command_selector",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:command_selection",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils:legacy_faction_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -26,19 +26,24 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers/quest_command_selectors:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/util/command_choice_helpers:__pkg__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
],
|
||||
deps = [
|
||||
":available_command_selector",
|
||||
":food_consumption_utils",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//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/actions/impl/command:alms_command",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:min_support_for_taxes",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:command_selection",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/hero:legacy_hero_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/province:legacy_province_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/hero",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/food_consumption",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/hero:hero_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -49,25 +54,29 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/ai:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/util/command_choice_helpers:__pkg__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
deps = [
|
||||
":available_command_selector",
|
||||
":combat_unit_selector",
|
||||
":march_supplies_helpers",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:battalion_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:hero_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:province_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/views:battalion_view_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:min_vigor_for_march",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:required_ratio_to_attack",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:required_ratio_to_attack_without_taking_castles",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:battalion_type_finder",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:battalion_power",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:battalion_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:command_selection",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:date_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:legacy_battalion_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:shardok_map_info",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils:legacy_faction_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -116,14 +125,15 @@ scala_library(
|
||||
"//src/test/scala/net/eagle0/eagle/library/util/command_choice_helpers:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/api/command/util:appropriate_battalions_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:combat_unit_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:battalion_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:hero_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:battalion_suitability",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:battalion_power",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:eagle_require",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:legacy_battalion_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/hero:legacy_hero_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/battalion_suitability",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/hero:hero_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero:gender",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -168,7 +178,6 @@ scala_library(
|
||||
":combat_unit_selector",
|
||||
":command_chooser",
|
||||
":expand_command_selector",
|
||||
":food_consumption_utils",
|
||||
":hero_gift_command_selector",
|
||||
":hero_selector",
|
||||
":improve_command_selector",
|
||||
@@ -197,14 +206,19 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:vassal_executes_on_capture_chance",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:vassal_minimum_food_to_send_supplies",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:vassal_minimum_gold_to_send_supplies",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:battalion_type_finder",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:beast_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:command_selection",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:incoming_army_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:province_distances",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/battalion_type_finder:legacy_battalion_type_finder",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils:legacy_faction_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/food_consumption:legacy_food_consumption_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/hero:legacy_hero_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/province:legacy_province_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:battalion_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:battalion_type_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/hero",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -219,7 +233,6 @@ scala_library(
|
||||
":available_command_selector",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:command_selection",
|
||||
],
|
||||
@@ -233,35 +246,26 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/ai:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/util/command_choice_helpers:__pkg__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
deps = [
|
||||
":available_command_selector",
|
||||
":food_consumption_utils",
|
||||
":province_gold_surplus_calculator",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/scala/net/eagle0/common:functional_random",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:max_combat_unit_count_per_side",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:battalion_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:command_selection",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:eagle_require",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:legacy_battalion_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:province_gold_surplus_calculator",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils:legacy_faction_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/province:legacy_province_utils",
|
||||
],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "food_consumption_utils",
|
||||
srcs = ["FoodConsumptionUtils.scala"],
|
||||
visibility = [
|
||||
"//src/main/scala/net/eagle0/eagle/ai:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/util/command_choice_helpers:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:min_support_for_taxes",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/food_consumption",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -294,6 +298,8 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers/quest_command_selectors:truce_with_faction_quest_command_chooser",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers/quest_command_selectors:unaffiliated_hero_with_quest",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/province:legacy_province_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -309,9 +315,9 @@ scala_library(
|
||||
":available_command_selector",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:command_selection",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -325,7 +331,9 @@ scala_library(
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:hero_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/hero:hero_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/hero:legacy_hero_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -337,16 +345,19 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/ai:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/util/command_choice_helpers:__pkg__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
deps = [
|
||||
":available_command_selector",
|
||||
":hero_selector",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:hero_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:command_selection",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/hero:legacy_hero_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/hero:hero_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -359,7 +370,8 @@ scala_library(
|
||||
"//src/test/scala/net/eagle0/eagle/library/util/command_choice_helpers:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:battalion_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -371,17 +383,22 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/ai:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/util/command_choice_helpers:__pkg__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
deps = [
|
||||
":available_command_selector",
|
||||
":province_gold_surplus_calculator",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:battalion_type_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:battalion_type_finder",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:command_selection",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:province_gold_surplus_calculator",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/hero:legacy_hero_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/province:legacy_province_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/battalion_type_finder",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -392,10 +409,15 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/ai:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/util/command_choice_helpers:__pkg__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:gold_per_hero_held_back",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -408,7 +430,6 @@ scala_library(
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:diplomacy_offer_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:minimum_months_before_repeat_ransom_offer",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:minimum_trust_to_offer_ransom",
|
||||
@@ -416,7 +437,9 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:ransom_offer_score_per_gold",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:ransom_offer_score_per_hostage",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:ransom_offer_score_per_prisoner",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils:legacy_faction_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/faction",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -429,7 +452,7 @@ scala_library(
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//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/util:command_selection",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:available_command_selector",
|
||||
@@ -448,7 +471,9 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:ai_minimum_charisma_for_sworn_brother",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:ai_minimum_constitution_for_sworn_brother",
|
||||
"//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/model/state/hero",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -463,10 +488,10 @@ scala_library(
|
||||
":available_command_selector",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:command_selection",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils:legacy_faction_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -479,13 +504,14 @@ scala_library(
|
||||
"//src/test/scala/net/eagle0/eagle/library/util/command_choice_helpers:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:minimum_months_before_repeat_invitation",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:minimum_months_before_repeat_truce_offer",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:minimum_trust_to_invite",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:minimum_trust_to_offer_alliance",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:minimum_trust_to_offer_truce",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils:legacy_faction_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/faction",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
+17
-18
@@ -2,21 +2,20 @@ package net.eagle0.eagle.library.util.command_choice_helpers
|
||||
|
||||
import scala.annotation.tailrec
|
||||
|
||||
import net.eagle0.eagle.api.command.util.appropriate_battalions.SuitableBattalions.SuitabilityLevel
|
||||
import net.eagle0.eagle.api.command.util.appropriate_battalions.SuitableBattalions.SuitabilityLevel.RESTRICTIVE
|
||||
import net.eagle0.eagle.common.battalion_type.BattalionType
|
||||
import net.eagle0.eagle.common.combat_unit.CombatUnit
|
||||
import net.eagle0.eagle.internal.battalion.Battalion
|
||||
import net.eagle0.eagle.internal.hero.Hero
|
||||
import net.eagle0.eagle.library.util.{BattalionSuitability, LegacyBattalionUtils}
|
||||
import net.eagle0.eagle.library.util.hero.LegacyHeroUtils
|
||||
import net.eagle0.eagle.library.util.battalion_suitability.{BattalionSuitability, SuitabilityLevel}
|
||||
import net.eagle0.eagle.library.util.hero.HeroUtils
|
||||
import net.eagle0.eagle.library.util.BattalionPower
|
||||
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
|
||||
import net.eagle0.eagle.model.state.battalion.BattalionT
|
||||
import net.eagle0.eagle.model.state.hero.HeroT
|
||||
import net.eagle0.eagle.model.state.BattalionType
|
||||
|
||||
object CombatUnitSelector {
|
||||
def selectedCombatBattalions(
|
||||
desiredCount: Int,
|
||||
heroes: Vector[Hero],
|
||||
battalions: Vector[Battalion],
|
||||
heroes: Vector[HeroT],
|
||||
battalions: Vector[BattalionT],
|
||||
battalionTypes: Vector[BattalionType]
|
||||
): Vector[CombatUnit] = {
|
||||
val suitableBattalionsMap =
|
||||
@@ -26,18 +25,18 @@ object CombatUnitSelector {
|
||||
bts = battalionTypes
|
||||
)
|
||||
|
||||
def suitability(h: Hero, b: Battalion): SuitabilityLevel =
|
||||
def suitability(h: HeroT, b: BattalionT): SuitabilityLevel =
|
||||
suitableBattalionsMap(h.id).battalionIdsWithSuitability
|
||||
.find(_.battalionId == b.id)
|
||||
.get
|
||||
.level
|
||||
|
||||
def chooseOne(hs: Vector[Hero], bs: Vector[Battalion]): CombatUnit = {
|
||||
def chooseOne(hs: Vector[HeroT], bs: Vector[BattalionT]): CombatUnit = {
|
||||
val bestBattalionOpt = bs
|
||||
.filter(b => hs.exists(h => suitability(h, b) != RESTRICTIVE))
|
||||
.maxByOption(LegacyBattalionUtils.power)
|
||||
.filter(b => hs.exists(h => suitability(h, b) != SuitabilityLevel.Restrictive))
|
||||
.maxByOption(BattalionPower.power)
|
||||
bestBattalionOpt.map { bestBattalion =>
|
||||
val bestHero = hs.minBy(h => (suitability(h, bestBattalion).index, -LegacyHeroUtils.power(h)))
|
||||
val bestHero = hs.minBy(h => (suitability(h, bestBattalion).ordinal, -HeroUtils.power(h)))
|
||||
CombatUnit(
|
||||
heroId = bestHero.id,
|
||||
battalionId = Some(bestBattalion.id),
|
||||
@@ -46,8 +45,8 @@ object CombatUnitSelector {
|
||||
}
|
||||
.getOrElse(
|
||||
CombatUnit(
|
||||
heroId = hs.maxBy(LegacyHeroUtils.power).id,
|
||||
battalionId = bs.maxByOption(LegacyBattalionUtils.power).map(_.id),
|
||||
heroId = hs.maxBy(HeroUtils.power).id,
|
||||
battalionId = bs.maxByOption(BattalionPower.power).map(_.id),
|
||||
factionId = hs.head.factionId.get
|
||||
)
|
||||
)
|
||||
@@ -56,8 +55,8 @@ object CombatUnitSelector {
|
||||
@tailrec
|
||||
def go(
|
||||
c: Int,
|
||||
hs: Vector[Hero],
|
||||
bs: Vector[Battalion],
|
||||
hs: Vector[HeroT],
|
||||
bs: Vector[BattalionT],
|
||||
acc: Vector[CombatUnit]
|
||||
): Vector[CombatUnit] =
|
||||
if c == 0 then acc
|
||||
|
||||
+37
-21
@@ -41,17 +41,22 @@ import net.eagle0.eagle.library.settings.{
|
||||
VassalMinimumFoodToSendSupplies,
|
||||
VassalMinimumGoldToSendSupplies
|
||||
}
|
||||
import net.eagle0.eagle.library.util.{BattalionTypeFinder, CommandSelection, IncomingArmyUtils, ProvinceDistances}
|
||||
import net.eagle0.eagle.library.util.{CommandSelection, IncomingArmyUtils, ProvinceDistances}
|
||||
import net.eagle0.eagle.library.util.battalion_type_finder.LegacyBattalionTypeFinder
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.AvailableCommandSelector.randomSelectionForType
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.CommandChooserImplicits.*
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.FoodConsumptionUtils.foodConsumptionMonthsToHold
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.ProvinceGoldSurplusCalculator.{
|
||||
goldToHoldBack,
|
||||
provinceGoldSurplus
|
||||
}
|
||||
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
|
||||
import net.eagle0.eagle.library.util.food_consumption.LegacyFoodConsumptionUtils.foodConsumptionMonthsToHold
|
||||
import net.eagle0.eagle.library.util.hero.LegacyHeroUtils
|
||||
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
|
||||
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
|
||||
import net.eagle0.eagle.model.proto_converters.hero.HeroConverter
|
||||
import net.eagle0.eagle.model.proto_converters.BattalionConverter
|
||||
import net.eagle0.eagle.model.proto_converters.BattalionTypeConverter
|
||||
|
||||
object CommandChoiceHelpers {
|
||||
import AvailableCommandSelector.{flatSelectionForType, selectionForType}
|
||||
@@ -71,7 +76,7 @@ object CommandChoiceHelpers {
|
||||
OrganizeCommandSelector.selectedOrganizeCommand(
|
||||
actingFactionId = actingFactionId,
|
||||
availableCommand = organizeCommand,
|
||||
gameState = gameState,
|
||||
gameState = GameStateConverter.fromProto(gameState),
|
||||
extraFoodSurplus = gameState.provinces(organizeCommand.actingProvinceId).food,
|
||||
reason = "organizing because we're under attack"
|
||||
)
|
||||
@@ -83,7 +88,7 @@ object CommandChoiceHelpers {
|
||||
gameState: GameState
|
||||
): Option[Supplies] = {
|
||||
def battalionType(battalionTypeId: BattalionTypeId): BattalionType =
|
||||
BattalionTypeFinder.battalionType(
|
||||
LegacyBattalionTypeFinder.battalionType(
|
||||
battalionTypeId,
|
||||
gameState.battalionTypes.toVector
|
||||
)
|
||||
@@ -209,9 +214,9 @@ object CommandChoiceHelpers {
|
||||
(actingFactionId, gameState, availableCommands, functionalRandom) =>
|
||||
RandomState(
|
||||
AlmsCommandSelector.chosenAlmsForSupportCommand(
|
||||
actingFactionId,
|
||||
gameState,
|
||||
availableCommands
|
||||
actingFactionId = actingFactionId,
|
||||
gameState = GameStateConverter.fromProto(gameState),
|
||||
availableCommands = availableCommands
|
||||
),
|
||||
functionalRandom
|
||||
),
|
||||
@@ -254,7 +259,7 @@ object CommandChoiceHelpers {
|
||||
RandomState(
|
||||
ImproveCommandSelector.chosenImproveCommand(
|
||||
actingFactionId,
|
||||
gameState,
|
||||
GameStateConverter.fromProto(gameState),
|
||||
availableCommands
|
||||
),
|
||||
functionalRandom
|
||||
@@ -298,7 +303,7 @@ object CommandChoiceHelpers {
|
||||
RandomState(
|
||||
OrganizeCommandSelector.chosenOrganizeCommand(
|
||||
actingFactionId,
|
||||
gameState,
|
||||
GameStateConverter.fromProto(gameState),
|
||||
availableCommands,
|
||||
"mobilizing"
|
||||
),
|
||||
@@ -331,7 +336,7 @@ object CommandChoiceHelpers {
|
||||
RandomState(
|
||||
ImproveCommandSelector.chosenImproveCommand(
|
||||
actingFactionId,
|
||||
gameState,
|
||||
GameStateConverter.fromProto(gameState),
|
||||
availableCommands
|
||||
),
|
||||
functionalRandom
|
||||
@@ -406,7 +411,7 @@ object CommandChoiceHelpers {
|
||||
.exists(b => gameState.battalions(b).size >= beastPower)
|
||||
then {
|
||||
val leaderId = HeroSelector
|
||||
.minimallyFatiguedHeroes(
|
||||
.minimallyFatiguedHeroesProto(
|
||||
suppressAc.availableHeroIds
|
||||
.map(gameState.heroes)
|
||||
.toVector
|
||||
@@ -470,7 +475,18 @@ object CommandChoiceHelpers {
|
||||
availableCommands = availableCommands,
|
||||
rankedChoosers = Vector[CommandChooser](
|
||||
chosenUniversalCommand,
|
||||
ExpandCommandSelector.maybeChosenExpandCommand,
|
||||
(
|
||||
fid: FactionId,
|
||||
gs: GameState,
|
||||
acs: Vector[AvailableCommand],
|
||||
fr: FunctionalRandom
|
||||
) =>
|
||||
ExpandCommandSelector.maybeChosenExpandCommand(
|
||||
fid,
|
||||
GameStateConverter.fromProto(gs),
|
||||
acs,
|
||||
fr
|
||||
),
|
||||
(
|
||||
fid: FactionId,
|
||||
gs: GameState,
|
||||
@@ -480,7 +496,7 @@ object CommandChoiceHelpers {
|
||||
RandomState(
|
||||
AttackCommandChooser.chosenAttackCommand(
|
||||
actingFactionId = fid,
|
||||
gameState = gs,
|
||||
gameState = GameStateConverter.fromProto(gs),
|
||||
acs = acs,
|
||||
knownEnemyHeroCount = _ => 0,
|
||||
knownEnemyBattalions = _ => Vector()
|
||||
@@ -491,7 +507,7 @@ object CommandChoiceHelpers {
|
||||
RandomState(
|
||||
ImproveCommandSelector.chosenImproveCommand(
|
||||
actingFactionId,
|
||||
gameState,
|
||||
GameStateConverter.fromProto(gameState),
|
||||
availableCommands
|
||||
),
|
||||
functionalRandom
|
||||
@@ -879,12 +895,13 @@ object CommandChoiceHelpers {
|
||||
CombatUnitSelector.selectedCombatBattalions(
|
||||
desiredCount = availableCommand.availableHeroIds.size
|
||||
.min(MaxCombatUnitCountPerSide.intValue),
|
||||
heroes = availableCommand.availableHeroIds.map(gs.heroes).toVector,
|
||||
heroes = availableCommand.availableHeroIds.map(gs.heroes).map(HeroConverter.fromProto).toVector,
|
||||
battalions = availableCommand.availableBattalions
|
||||
.map(_.battalionId)
|
||||
.map(gs.battalions)
|
||||
.map(BattalionConverter.fromProto)
|
||||
.toVector,
|
||||
battalionTypes = gs.battalionTypes.toVector
|
||||
battalionTypes = gs.battalionTypes.map(BattalionTypeConverter.fromProto).toVector
|
||||
)
|
||||
|
||||
def foodAmountToBuy(
|
||||
@@ -980,7 +997,7 @@ object CommandChoiceHelpers {
|
||||
) {
|
||||
ImproveCommandSelector.chosenSpecificImprovementCommand(
|
||||
actingFactionId,
|
||||
gameState,
|
||||
GameStateConverter.fromProto(gameState),
|
||||
availableCommands,
|
||||
INFRASTRUCTURE
|
||||
)
|
||||
@@ -1177,7 +1194,6 @@ object CommandChoiceHelpers {
|
||||
).headOption.flatMap { hero =>
|
||||
HeroGiftCommandSelector.chosenCommandForSpecificHero(
|
||||
actingFactionId = actingFactionId,
|
||||
gameState = gameState,
|
||||
availableCommands = availableCommands,
|
||||
heroId = hero.id,
|
||||
reason = reason
|
||||
@@ -1392,7 +1408,7 @@ object CommandChoiceHelpers {
|
||||
originatingFactionId = actingFactionId,
|
||||
targetFactionId = targetFactionId,
|
||||
availableOffer = ransomOffer,
|
||||
gameState = gameState
|
||||
gameState = GameStateConverter.fromProto(gameState)
|
||||
)
|
||||
.map { chosenOffer =>
|
||||
CommandSelection(
|
||||
@@ -1970,7 +1986,7 @@ object CommandChoiceHelpers {
|
||||
// Hire up to at least beastsCount, but if there's surplus gold, hire more.
|
||||
val batt = lowestCost._1
|
||||
val capacity =
|
||||
BattalionTypeFinder
|
||||
LegacyBattalionTypeFinder
|
||||
.battalionType(batt.`type`, gameState.battalionTypes.toVector)
|
||||
.capacity
|
||||
val maxDesired = batt.size +
|
||||
@@ -2012,7 +2028,7 @@ object CommandChoiceHelpers {
|
||||
Option.when(costForMinimalBattalion <= province.gold) {
|
||||
val goldSurplus = provinceGoldSurplus(province.id, gameState)
|
||||
val battalionType =
|
||||
BattalionTypeFinder.battalionType(
|
||||
LegacyBattalionTypeFinder.battalionType(
|
||||
lowestCost._1,
|
||||
gameState.battalionTypes.toVector
|
||||
)
|
||||
|
||||
-2
@@ -3,7 +3,6 @@ package net.eagle0.eagle.library.util.command_choice_helpers
|
||||
import net.eagle0.eagle.{FactionId, HeroId, ProvinceId}
|
||||
import net.eagle0.eagle.api.available_command.{AvailableCommand, ExileVassalAvailableCommand}
|
||||
import net.eagle0.eagle.api.selected_command.ExileVassalSelectedCommand
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.AvailableCommandSelector.ofType
|
||||
import net.eagle0.eagle.library.util.CommandSelection
|
||||
|
||||
@@ -11,7 +10,6 @@ object ExileVassalCommandSelector {
|
||||
def chosenSpecificDismissVassalCommandForProvince(
|
||||
actingFactionId: FactionId,
|
||||
actingProvinceId: ProvinceId,
|
||||
gameState: GameState,
|
||||
availableCommands: Vector[AvailableCommand],
|
||||
dismissedHeroId: HeroId,
|
||||
reason: String
|
||||
|
||||
+40
-48
@@ -7,15 +7,15 @@ import net.eagle0.eagle.{FactionId, HeroId, ProvinceId}
|
||||
import net.eagle0.eagle.api.available_command.{AvailableCommand, MarchAvailableCommand, MarchCommandFromOneProvince}
|
||||
import net.eagle0.eagle.api.selected_command.MarchSelectedCommand
|
||||
import net.eagle0.eagle.common.combat_unit.CombatUnit
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.internal.province.Province
|
||||
import net.eagle0.eagle.library.settings.MaxCombatUnitCountPerSide
|
||||
import net.eagle0.eagle.library.util.{CommandSelection, LegacyBattalionUtils}
|
||||
import net.eagle0.eagle.library.util.{BattalionUtils, CommandSelection}
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.AvailableCommandSelector.randomSelectionForType
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.ProvinceGoldSurplusCalculator.provinceGoldSurplus
|
||||
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
|
||||
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
|
||||
import net.eagle0.eagle.library.util.faction_utils.FactionUtils
|
||||
import net.eagle0.eagle.library.util.food_consumption.FoodConsumptionUtils.foodConsumptionMonthsToHold
|
||||
import net.eagle0.eagle.library.util.province.ProvinceUtils
|
||||
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.province.ProvinceT
|
||||
|
||||
object ExpandCommandSelector {
|
||||
private case class MarchUnitsWithFood(units: Vector[CombatUnit], food: Int)
|
||||
@@ -37,7 +37,7 @@ object ExpandCommandSelector {
|
||||
|
||||
if (rulingFactionHeroIds.size < 2) // Don't abandon this province
|
||||
|| (rulingFactionHeroIds.size - rulingFactionHeroIds.count(
|
||||
LegacyFactionUtils.isFactionLeader(_, gameState)
|
||||
FactionUtils.isFactionLeader(_, gameState.factions.values.toVector)
|
||||
) < 2) // Don't leave a faction leader alone
|
||||
then randomNone
|
||||
else
|
||||
@@ -52,8 +52,8 @@ object ExpandCommandSelector {
|
||||
.map(_.provinceId)
|
||||
.toVector
|
||||
).partition(p =>
|
||||
LegacyProvinceUtils
|
||||
.isAdjacentEnemy(p.id, actingFactionId, gameState)
|
||||
ProvinceUtils
|
||||
.isAdjacentEnemy(p, actingFactionId, gameState)
|
||||
)
|
||||
|
||||
// First try to expand into a "safe" province
|
||||
@@ -90,20 +90,16 @@ object ExpandCommandSelector {
|
||||
gameState: GameState,
|
||||
fromProvince: ProvinceId,
|
||||
eligibleDestinations: Vector[ProvinceId]
|
||||
): Vector[Province] =
|
||||
): Vector[ProvinceT] =
|
||||
eligibleDestinations
|
||||
.map(gameState.provinces)
|
||||
.filterNot(_.rulingFactionId.isDefined)
|
||||
.filterNot(
|
||||
_.incomingArmies.exists(ma =>
|
||||
ma.army
|
||||
.map(_.factionId)
|
||||
.contains(
|
||||
gameState
|
||||
.provinces(fromProvince)
|
||||
.rulingFactionId
|
||||
.get
|
||||
)
|
||||
gameState
|
||||
.provinces(fromProvince)
|
||||
.rulingFactionId
|
||||
.contains(ma.army.factionId)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -113,7 +109,7 @@ object ExpandCommandSelector {
|
||||
gameState: GameState,
|
||||
availableCommand: MarchAvailableCommand,
|
||||
oneProvinceCommand: MarchCommandFromOneProvince,
|
||||
safeExpansionCandidateProvinces: Vector[Province],
|
||||
safeExpansionCandidateProvinces: Vector[ProvinceT],
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Option[CommandSelection]] = {
|
||||
val province = gameState.provinces(availableCommand.actingProvinceId)
|
||||
@@ -157,7 +153,7 @@ object ExpandCommandSelector {
|
||||
gameState: GameState,
|
||||
availableCommand: MarchAvailableCommand,
|
||||
oneProvinceCommand: MarchCommandFromOneProvince,
|
||||
adjacentEnemyCandidateProvinces: Vector[Province],
|
||||
adjacentEnemyCandidateProvinces: Vector[ProvinceT],
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Option[CommandSelection]] = {
|
||||
val province = gameState.provinces(availableCommand.actingProvinceId)
|
||||
@@ -175,8 +171,8 @@ object ExpandCommandSelector {
|
||||
else {
|
||||
// If the originating province is safe, only leave two heroes behind
|
||||
val heroCountToSend = (
|
||||
if LegacyProvinceUtils.isAdjacentEnemy(
|
||||
oneProvinceCommand.originProvinceId,
|
||||
if ProvinceUtils.isAdjacentEnemy(
|
||||
province,
|
||||
actingFactionId,
|
||||
gameState
|
||||
)
|
||||
@@ -251,8 +247,8 @@ object ExpandCommandSelector {
|
||||
actingFactionId: FactionId,
|
||||
heroCountToSend: Int,
|
||||
goldToSend: Int,
|
||||
originatingProvince: Province,
|
||||
candidateDestinationProvinces: Vector[Province],
|
||||
originatingProvince: ProvinceT,
|
||||
candidateDestinationProvinces: Vector[ProvinceT],
|
||||
oneProvinceCommand: MarchCommandFromOneProvince,
|
||||
availableCommand: MarchAvailableCommand,
|
||||
gameState: GameState,
|
||||
@@ -268,8 +264,8 @@ object ExpandCommandSelector {
|
||||
rulingFactionHeroIds.diff(oneProvinceCommand.availableHeroIds)
|
||||
|
||||
// Don't leave a faction leader alone in the origin province
|
||||
if heroCountLeftBehind == 1 && notAvailableHeroes.size == 1 && LegacyFactionUtils
|
||||
.isFactionLeader(notAvailableHeroes.head, gameState)
|
||||
if heroCountLeftBehind == 1 && notAvailableHeroes.size == 1 && FactionUtils
|
||||
.isFactionLeader(notAvailableHeroes.head, gameState.factions.values.toVector)
|
||||
then {
|
||||
return randomNone
|
||||
}
|
||||
@@ -297,7 +293,7 @@ object ExpandCommandSelector {
|
||||
fid = actingFactionId,
|
||||
factionLeaders = gameState
|
||||
.factions(actingFactionId)
|
||||
.leaders
|
||||
.leaderIds
|
||||
.intersect(originatingProvince.rulingFactionHeroIds)
|
||||
.toVector,
|
||||
otherCountLeftBehind =
|
||||
@@ -310,19 +306,19 @@ object ExpandCommandSelector {
|
||||
rulingFactionHeroIds.filterNot(takenHeroes.contains)
|
||||
|
||||
internalRequire(
|
||||
takenHeroes.length > 1 || !LegacyFactionUtils
|
||||
takenHeroes.length > 1 || !FactionUtils
|
||||
.isFactionLeader(
|
||||
heroId = takenHeroes.head,
|
||||
gameState = gameState
|
||||
takenHeroes.head,
|
||||
gameState.factions.values.toVector
|
||||
),
|
||||
"trying to take only a faction leader!"
|
||||
)
|
||||
|
||||
internalRequire(
|
||||
leftHeroes.length > 1 || !LegacyFactionUtils
|
||||
leftHeroes.length > 1 || !FactionUtils
|
||||
.isFactionLeader(
|
||||
heroId = leftHeroes.head,
|
||||
gameState = gameState
|
||||
leftHeroes.head,
|
||||
gameState.factions.values.toVector
|
||||
),
|
||||
"trying to leave a faction leader alone!"
|
||||
)
|
||||
@@ -335,13 +331,13 @@ object ExpandCommandSelector {
|
||||
)
|
||||
.map(gameState.battalions)
|
||||
val monthlyFoodConsumptionInOrigin =
|
||||
LegacyBattalionUtils.monthlyConsumedFood(
|
||||
BattalionUtils.monthlyConsumedFood(
|
||||
battalionsHeldBack,
|
||||
gameState.battalionTypes.toVector
|
||||
gameState.battalionTypes
|
||||
)
|
||||
val foodToHoldBack = monthlyFoodConsumptionInOrigin *
|
||||
FoodConsumptionUtils.foodConsumptionMonthsToHold(
|
||||
originatingProvince.id,
|
||||
foodConsumptionMonthsToHold(
|
||||
originatingProvince,
|
||||
gameState
|
||||
)
|
||||
|
||||
@@ -351,11 +347,10 @@ object ExpandCommandSelector {
|
||||
val monthlyFoodConsumptionInDestination =
|
||||
marchingUnitsWithFood.food
|
||||
val minFoodToSend =
|
||||
monthlyFoodConsumptionInDestination * FoodConsumptionUtils
|
||||
.foodConsumptionMonthsToHold(
|
||||
chosenCandidateProvince.id,
|
||||
gameState
|
||||
)
|
||||
monthlyFoodConsumptionInDestination * foodConsumptionMonthsToHold(
|
||||
chosenCandidateProvince,
|
||||
gameState
|
||||
)
|
||||
|
||||
Option.when(minFoodToSend <= foodAvailableToSend) {
|
||||
CommandSelection(
|
||||
@@ -407,10 +402,7 @@ object ExpandCommandSelector {
|
||||
0,
|
||||
Math.min(
|
||||
opmc.goldAvailable,
|
||||
(provinceGoldSurplus(
|
||||
originProvince.id,
|
||||
gameState: GameState
|
||||
) - destinationProvince.gold) / 2
|
||||
(ProvinceGoldSurplusCalculator.provinceGoldSurplus(originProvince) - destinationProvince.gold) / 2
|
||||
)
|
||||
)
|
||||
|
||||
@@ -424,7 +416,7 @@ object ExpandCommandSelector {
|
||||
heroCount = heroCountToSend,
|
||||
battalionCount = battalionCountToSend,
|
||||
fid = actingFactionId,
|
||||
factionLeaders = gameState.factions(actingFactionId).leaders.toVector,
|
||||
factionLeaders = gameState.factions(actingFactionId).leaderIds,
|
||||
otherCountLeftBehind = originProvince.rulingFactionHeroIds.length - opmc.availableHeroIds.length,
|
||||
functionalRandom = functionalRandom
|
||||
).map { marchingUnitsWithFood =>
|
||||
@@ -451,7 +443,7 @@ object ExpandCommandSelector {
|
||||
gameState: GameState,
|
||||
fromProvince: ProvinceId,
|
||||
eligibleDestinations: Vector[ProvinceId]
|
||||
): Vector[Province] = {
|
||||
): Vector[ProvinceT] = {
|
||||
val p = gameState.provinces(fromProvince)
|
||||
eligibleDestinations
|
||||
.map(gameState.provinces)
|
||||
|
||||
+7
-3
@@ -2,7 +2,7 @@ package net.eagle0.eagle.library.util.command_choice_helpers
|
||||
|
||||
import net.eagle0.common.{FunctionalRandom, RandomState}
|
||||
import net.eagle0.eagle.api.available_command.AvailableCommand
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.internal.game_state.GameState as GameStateProto
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.quest_command_selectors.{
|
||||
AllianceQuestCommandChooser,
|
||||
AlmsAcrossRealmQuestCommandChooser,
|
||||
@@ -18,6 +18,8 @@ import net.eagle0.eagle.library.util.command_choice_helpers.quest_command_select
|
||||
}
|
||||
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
|
||||
import net.eagle0.eagle.library.util.CommandSelection
|
||||
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.FactionId
|
||||
|
||||
object FulfillQuestsCommandSelector {
|
||||
@@ -43,7 +45,7 @@ object FulfillQuestsCommandSelector {
|
||||
|
||||
def chosenFulfillEasyQuestsCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
availableCommands: Vector[AvailableCommand],
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Option[CommandSelection]] = {
|
||||
@@ -58,6 +60,8 @@ object FulfillQuestsCommandSelector {
|
||||
quest <- recruitmentInfo.quest
|
||||
} yield UnaffiliatedHeroWithQuest(province.id, uh, quest)).toVector
|
||||
|
||||
val nativeGameState = GameStateConverter.fromProto(gameState)
|
||||
|
||||
choiceFrom(
|
||||
choosers = Vector[QuestCommandChooser](
|
||||
AllianceQuestCommandChooser,
|
||||
@@ -71,7 +75,7 @@ object FulfillQuestsCommandSelector {
|
||||
DismissSpecificVassalCommandChooser
|
||||
),
|
||||
actingFactionId = actingFactionId,
|
||||
gameState = gameState,
|
||||
gameState = nativeGameState,
|
||||
availableCommands = availableCommands,
|
||||
uhsWithQuests = uhsWithQuests,
|
||||
functionalRandom = functionalRandom
|
||||
|
||||
+1
-2
@@ -3,9 +3,9 @@ package net.eagle0.eagle.library.util.command_choice_helpers
|
||||
import net.eagle0.eagle.{FactionId, HeroId, ProvinceId}
|
||||
import net.eagle0.eagle.api.available_command.{AvailableCommand, HeroGiftAvailableCommand}
|
||||
import net.eagle0.eagle.api.selected_command.HeroGiftSelectedCommand
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.AvailableCommandSelector.flatSelectionForType
|
||||
import net.eagle0.eagle.library.util.CommandSelection
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
|
||||
object HeroGiftCommandSelector {
|
||||
def chosenCommandForLowestLoyaltyHero(
|
||||
@@ -35,7 +35,6 @@ object HeroGiftCommandSelector {
|
||||
|
||||
def chosenCommandForSpecificHero(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
availableCommands: Vector[AvailableCommand],
|
||||
heroId: HeroId,
|
||||
reason: String
|
||||
|
||||
+12
-2
@@ -1,10 +1,20 @@
|
||||
package net.eagle0.eagle.library.util.command_choice_helpers
|
||||
|
||||
import net.eagle0.eagle.internal.hero.Hero
|
||||
import net.eagle0.eagle.library.util.hero.LegacyHeroUtils
|
||||
import net.eagle0.eagle.library.util.hero.{HeroUtils, LegacyHeroUtils}
|
||||
import net.eagle0.eagle.model.state.hero.HeroT
|
||||
|
||||
object HeroSelector {
|
||||
def minimallyFatiguedHeroes(heroes: Vector[Hero]): Vector[Hero] = {
|
||||
|
||||
/** Protoless version */
|
||||
def minimallyFatiguedHeroes(heroes: Vector[HeroT]): Vector[HeroT] = {
|
||||
val minimumFatigue = heroes.map(HeroUtils.fatigue).min
|
||||
heroes
|
||||
.filter(HeroUtils.fatigue(_) == minimumFatigue)
|
||||
}
|
||||
|
||||
/** Legacy proto version */
|
||||
def minimallyFatiguedHeroesProto(heroes: Vector[Hero]): Vector[Hero] = {
|
||||
val minimumFatigue = heroes.map(LegacyHeroUtils.fatigue).min
|
||||
heroes
|
||||
.filter(LegacyHeroUtils.fatigue(_) == minimumFatigue)
|
||||
|
||||
+7
-7
@@ -5,21 +5,21 @@ import net.eagle0.eagle.api.available_command.{AvailableCommand, ImproveAvailabl
|
||||
import net.eagle0.eagle.api.selected_command.ImproveSelectedCommand
|
||||
import net.eagle0.eagle.common.improvement_type.ImprovementType
|
||||
import net.eagle0.eagle.common.improvement_type.ImprovementType.DEVASTATION
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.internal.province.Province
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.AvailableCommandSelector.{
|
||||
flatSelectionForType,
|
||||
selectionForType
|
||||
}
|
||||
import net.eagle0.eagle.library.util.hero.LegacyHeroUtils
|
||||
import net.eagle0.eagle.library.util.hero.HeroUtils
|
||||
import net.eagle0.eagle.library.util.CommandSelection
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.province.ProvinceT
|
||||
|
||||
object ImproveCommandSelector {
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.HeroSelector.minimallyFatiguedHeroes
|
||||
|
||||
private def provinceValueForType(
|
||||
improvementType: ImprovementType,
|
||||
province: Province
|
||||
province: ProvinceT
|
||||
): Double = improvementType match {
|
||||
case ImprovementType.ECONOMY => province.economy
|
||||
case ImprovementType.AGRICULTURE => province.agriculture
|
||||
@@ -31,7 +31,7 @@ object ImproveCommandSelector {
|
||||
|
||||
private def chosenType(
|
||||
availableTypes: Vector[ImprovementType],
|
||||
province: Province
|
||||
province: ProvinceT
|
||||
): ImprovementType =
|
||||
if availableTypes.contains(DEVASTATION) then DEVASTATION
|
||||
else availableTypes.minBy(provinceValueForType(_, province))
|
||||
@@ -80,7 +80,7 @@ object ImproveCommandSelector {
|
||||
improveCommand.availableHeroIds.toVector.map(
|
||||
gameState.heroes(_)
|
||||
)
|
||||
).maxBy(LegacyHeroUtils.power(_)).id
|
||||
).maxBy(HeroUtils.power(_)).id
|
||||
),
|
||||
reason = "chosenSpecificImprovementCommand"
|
||||
)
|
||||
@@ -105,7 +105,7 @@ object ImproveCommandSelector {
|
||||
improveAvailableCommand.availableHeroIds.toVector.map(
|
||||
gameState.heroes(_)
|
||||
)
|
||||
).maxBy(LegacyHeroUtils.power(_)).id
|
||||
).maxBy(HeroUtils.power(_)).id
|
||||
),
|
||||
reason = "ImproveCommandSelector.commandSelection"
|
||||
)
|
||||
|
||||
+2
-2
@@ -1,13 +1,13 @@
|
||||
package net.eagle0.eagle.library.util.command_choice_helpers
|
||||
|
||||
import net.eagle0.eagle.internal.battalion.Battalion
|
||||
import net.eagle0.eagle.model.state.battalion.BattalionT
|
||||
|
||||
object MarchSuppliesHelpers {
|
||||
def goldToTakeOnAttack(unitCount: Int, goldAvailable: Int): Int =
|
||||
goldAvailable.min(200 * unitCount)
|
||||
|
||||
def foodToTakeOnAttack(
|
||||
battalions: Vector[Battalion],
|
||||
battalions: Vector[BattalionT],
|
||||
foodAvailable: Int
|
||||
): Int =
|
||||
foodAvailable.min(300 * battalions.size)
|
||||
|
||||
+40
-53
@@ -5,17 +5,23 @@ import scala.annotation.tailrec
|
||||
import net.eagle0.eagle.api.available_command.{AvailableCommand, OrganizeTroopsAvailableCommand}
|
||||
import net.eagle0.eagle.api.selected_command.OrganizeTroopsSelectedCommand
|
||||
import net.eagle0.eagle.api.selected_command.OrganizeTroopsSelectedCommand.{ChangedBattalion, NewBattalion}
|
||||
import net.eagle0.eagle.common.battalion_type.{BattalionType, BattalionTypeId}
|
||||
import net.eagle0.eagle.internal.battalion.Battalion
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.library.util.{BattalionTypeFinder, CommandSelection}
|
||||
import net.eagle0.eagle.common.battalion_type.BattalionTypeId as ProtoBattalionTypeId
|
||||
import net.eagle0.eagle.library.util.battalion_type_finder.BattalionTypeFinder
|
||||
import net.eagle0.eagle.library.util.battalion_type_finder.BattalionTypeFinder.battalionType
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.AvailableCommandSelector.flatSelectionForType
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.ProvinceGoldSurplusCalculator.provinceGoldSurplus
|
||||
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
|
||||
import net.eagle0.eagle.library.util.BattalionTypeFinder.battalionType
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.ProvinceGoldSurplusCalculator
|
||||
import net.eagle0.eagle.library.util.province.ProvinceUtils
|
||||
import net.eagle0.eagle.library.util.CommandSelection
|
||||
import net.eagle0.eagle.model.state.{BattalionType, BattalionTypeId}
|
||||
import net.eagle0.eagle.model.state.battalion.BattalionT
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.FactionId
|
||||
|
||||
object OrganizeCommandSelector {
|
||||
// Helper to convert Scala BattalionTypeId to proto BattalionTypeId
|
||||
private def toProtoBattalionTypeId(typeId: BattalionTypeId): ProtoBattalionTypeId =
|
||||
ProtoBattalionTypeId.fromValue(typeId.value)
|
||||
|
||||
case class ChangedBattalionsResult(
|
||||
changedBattalions: Vector[ChangedBattalion],
|
||||
remainingGold: Int,
|
||||
@@ -32,7 +38,7 @@ object OrganizeCommandSelector {
|
||||
gold: Int,
|
||||
foodSurplus: Int,
|
||||
costs: Map[BattalionTypeId, Double],
|
||||
existingBattalions: Vector[Battalion],
|
||||
existingBattalions: Vector[BattalionT],
|
||||
battalionTypes: Vector[BattalionType],
|
||||
eligibleBattalionTypeIds: Vector[BattalionTypeId]
|
||||
): ChangedBattalionsResult = {
|
||||
@@ -40,15 +46,15 @@ object OrganizeCommandSelector {
|
||||
def go(
|
||||
gold: Int,
|
||||
foodSurplus: Int,
|
||||
battalionsInNeed: Vector[Battalion],
|
||||
battalionsInNeed: Vector[BattalionT],
|
||||
acc: Vector[ChangedBattalion]
|
||||
): ChangedBattalionsResult = battalionsInNeed match {
|
||||
case b +: bs =>
|
||||
val missing = battalionType(b.`type`, battalionTypes).capacity - b.size
|
||||
val goldCost = (missing * costs(b.`type`)).ceil.toInt
|
||||
val missing = battalionType(b.typeId, battalionTypes).capacity - b.size
|
||||
val goldCost = (missing * costs(b.typeId)).ceil.toInt
|
||||
val foodCost =
|
||||
(missing * battalionType(
|
||||
b.`type`,
|
||||
b.typeId,
|
||||
battalionTypes
|
||||
).monthlyFoodCost).ceil.toInt
|
||||
if goldCost <= gold && foodCost <= foodSurplus then
|
||||
@@ -77,9 +83,9 @@ object OrganizeCommandSelector {
|
||||
gold = gold,
|
||||
foodSurplus = foodSurplus,
|
||||
battalionsInNeed = existingBattalions
|
||||
.filter(b => eligibleBattalionTypeIds.contains(b.`type`))
|
||||
.filter(b => costs.contains(b.`type`))
|
||||
.filter(b => b.size < battalionType(b.`type`, battalionTypes).capacity),
|
||||
.filter(b => eligibleBattalionTypeIds.contains(b.typeId))
|
||||
.filter(b => costs.contains(b.typeId))
|
||||
.filter(b => b.size < battalionType(b.typeId, battalionTypes).capacity),
|
||||
acc = Vector()
|
||||
)
|
||||
}
|
||||
@@ -90,7 +96,7 @@ object OrganizeCommandSelector {
|
||||
costs: Map[BattalionTypeId, Double],
|
||||
allBattalionTypes: Vector[BattalionType],
|
||||
eligibleBattalionTypeIds: Vector[BattalionTypeId],
|
||||
existingBattalions: Vector[Battalion],
|
||||
existingBattalions: Vector[BattalionT],
|
||||
maxCount: Int = 100
|
||||
): NewBattalionsResult = {
|
||||
val eligibleCosts = costs.filter {
|
||||
@@ -142,7 +148,7 @@ object OrganizeCommandSelector {
|
||||
.headOption match {
|
||||
case Some(x) =>
|
||||
val newBattalion = NewBattalion(
|
||||
`type` = x.battalionTypeId,
|
||||
`type` = toProtoBattalionTypeId(x.battalionTypeId),
|
||||
newTroops = battalionType(x.battalionTypeId, allBattalionTypes).capacity
|
||||
)
|
||||
go(
|
||||
@@ -166,7 +172,7 @@ object OrganizeCommandSelector {
|
||||
gold = gold,
|
||||
foodSurplus = foodSurplus,
|
||||
acc = Vector(),
|
||||
existingBattalionTypeCounts = existingBattalions.groupBy(_.`type`).map { case (k, v) => k -> v.size }
|
||||
existingBattalionTypeCounts = existingBattalions.groupBy(_.typeId).map { case (k, v) => k -> v.size }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -177,58 +183,39 @@ object OrganizeCommandSelector {
|
||||
extraFoodSurplus: Int,
|
||||
reason: String
|
||||
): Option[CommandSelection] = {
|
||||
val province = gameState.provinces(availableCommand.actingProvinceId)
|
||||
val foodSurplus =
|
||||
LegacyProvinceUtils.monthlyFoodSurplus(
|
||||
gameState = gameState,
|
||||
provinceId = availableCommand.actingProvinceId
|
||||
) + extraFoodSurplus
|
||||
val goldSurplus =
|
||||
provinceGoldSurplus(availableCommand.actingProvinceId, gameState)
|
||||
ProvinceUtils.monthlyFoodSurplus(province, gameState) + extraFoodSurplus
|
||||
val goldSurplus = ProvinceGoldSurplusCalculator.provinceGoldSurplus(province)
|
||||
if foodSurplus <= 0 || goldSurplus <= 0 then None
|
||||
else {
|
||||
val costs = availableCommand.troopCosts
|
||||
.map(tc => tc.`type` -> tc.costPerTroop)
|
||||
// Convert proto BattalionTypeId to Scala BattalionTypeId
|
||||
val costs = availableCommand.troopCosts
|
||||
.map(tc => BattalionTypeId.fromInt(tc.`type`.value) -> tc.costPerTroop)
|
||||
.toMap
|
||||
val eligibleTypeIds = availableCommand.availableBattalionTypes
|
||||
.filter(_.meetsRequirements)
|
||||
.toVector
|
||||
.map(abt => BattalionTypeId.fromInt(abt.typeId.value))
|
||||
|
||||
val toppedBattalionResults = changedBattalions(
|
||||
gold = goldSurplus,
|
||||
foodSurplus = foodSurplus,
|
||||
costs = costs,
|
||||
existingBattalions = gameState
|
||||
.provinces(availableCommand.actingProvinceId)
|
||||
.battalionIds
|
||||
.toVector
|
||||
existingBattalions = province.battalionIds.toVector
|
||||
.map(gameState.battalions(_)),
|
||||
battalionTypes = gameState.battalionTypes.toVector,
|
||||
eligibleBattalionTypeIds = availableCommand.availableBattalionTypes
|
||||
.filter(_.meetsRequirements)
|
||||
.toVector
|
||||
.map(_.typeId)
|
||||
eligibleBattalionTypeIds = eligibleTypeIds
|
||||
)
|
||||
|
||||
val newBattalionResults = newBattalions(
|
||||
gold = toppedBattalionResults.remainingGold,
|
||||
foodSurplus = toppedBattalionResults.remainingFoodSurplus,
|
||||
costs = costs,
|
||||
maxCount = gameState
|
||||
.provinces(availableCommand.actingProvinceId)
|
||||
.rulingFactionHeroIds
|
||||
.size - gameState
|
||||
.provinces(availableCommand.actingProvinceId)
|
||||
.battalionIds
|
||||
.size,
|
||||
allBattalionTypes = gameState.battalionTypes.toVector.filter(bt =>
|
||||
availableCommand.availableBattalionTypes
|
||||
.exists(tp => tp.typeId == bt.typeId && tp.meetsRequirements)
|
||||
),
|
||||
eligibleBattalionTypeIds = availableCommand.availableBattalionTypes
|
||||
.filter(_.meetsRequirements)
|
||||
.toVector
|
||||
.map(_.typeId),
|
||||
existingBattalions = gameState
|
||||
.provinces(availableCommand.actingProvinceId)
|
||||
.battalionIds
|
||||
.toVector
|
||||
maxCount = province.rulingFactionHeroIds.size - province.battalionIds.size,
|
||||
allBattalionTypes = gameState.battalionTypes.toVector.filter(bt => eligibleTypeIds.contains(bt.typeId)),
|
||||
eligibleBattalionTypeIds = eligibleTypeIds,
|
||||
existingBattalions = province.battalionIds.toVector
|
||||
.map(gameState.battalions(_))
|
||||
)
|
||||
|
||||
|
||||
+13
-3
@@ -1,14 +1,24 @@
|
||||
package net.eagle0.eagle.library.util.command_choice_helpers
|
||||
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.internal.game_state.GameState as GameStateProto
|
||||
import net.eagle0.eagle.library.settings.GoldPerHeroHeldBack
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.province.ProvinceT
|
||||
import net.eagle0.eagle.ProvinceId
|
||||
|
||||
object ProvinceGoldSurplusCalculator {
|
||||
|
||||
// Protoless version
|
||||
def goldToHoldBack(province: ProvinceT): Int =
|
||||
(GoldPerHeroHeldBack.doubleValue * province.rulingFactionHeroIds.length).ceil.toInt
|
||||
|
||||
def provinceGoldSurplus(province: ProvinceT): Int =
|
||||
(province.gold - goldToHoldBack(province)).max(0)
|
||||
|
||||
// Legacy proto version for backward compatibility
|
||||
def goldToHoldBack(
|
||||
provinceId: ProvinceId,
|
||||
gameState: GameState
|
||||
gameState: GameStateProto
|
||||
): Int =
|
||||
(GoldPerHeroHeldBack.doubleValue * gameState
|
||||
.provinces(provinceId)
|
||||
@@ -17,7 +27,7 @@ object ProvinceGoldSurplusCalculator {
|
||||
|
||||
def provinceGoldSurplus(
|
||||
provinceId: ProvinceId,
|
||||
gameState: GameState
|
||||
gameState: GameStateProto
|
||||
): Int =
|
||||
(gameState.provinces(provinceId).gold - goldToHoldBack(
|
||||
provinceId,
|
||||
|
||||
+7
-11
@@ -6,8 +6,6 @@ import net.eagle0.eagle.common.diplomacy_offer_status.DiplomacyOfferStatus.{
|
||||
DIPLOMACY_OFFER_STATUS_ACCEPTED,
|
||||
DIPLOMACY_OFFER_STATUS_REJECTED
|
||||
}
|
||||
import net.eagle0.eagle.internal.faction.Faction.OutgoingOfferRound
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.library.settings.{
|
||||
MinimumMonthsBeforeRepeatRansomOffer,
|
||||
MinimumTrustToOfferRansom,
|
||||
@@ -16,7 +14,9 @@ import net.eagle0.eagle.library.settings.{
|
||||
RansomOfferScorePerHostage,
|
||||
RansomOfferScorePerPrisoner
|
||||
}
|
||||
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
|
||||
import net.eagle0.eagle.library.util.faction_utils.FactionUtils
|
||||
import net.eagle0.eagle.model.state.faction.FactionT.OutgoingOfferRound
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.FactionId
|
||||
|
||||
object RansomOfferHelpers {
|
||||
@@ -36,10 +36,10 @@ object RansomOfferHelpers {
|
||||
targetFactionId: FactionId,
|
||||
gameState: GameState
|
||||
): Boolean =
|
||||
LegacyFactionUtils.trust(
|
||||
FactionUtils.trust(
|
||||
by = offeringFactionId,
|
||||
of = targetFactionId,
|
||||
gameState = gameState
|
||||
factions = gameState.factions.values.toVector
|
||||
) >= MinimumTrustToOfferRansom.intValue
|
||||
|
||||
def sufficientTime(
|
||||
@@ -51,11 +51,7 @@ object RansomOfferHelpers {
|
||||
.factions(offeringFactionId)
|
||||
.lastOutgoingRansomOfferRounds
|
||||
.exists {
|
||||
case OutgoingOfferRound(
|
||||
toFactionId,
|
||||
roundId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
case OutgoingOfferRound(toFactionId, roundId) =>
|
||||
toFactionId == targetFactionId && roundId + MinimumMonthsBeforeRepeatRansomOffer.intValue > gameState.currentRoundId
|
||||
}
|
||||
|
||||
@@ -73,7 +69,7 @@ object RansomOfferHelpers {
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
val nonFactionLeaderHostages =
|
||||
hostagesOffered.filterNot(hoie => LegacyFactionUtils.isFactionLeader(hoie.heroId, gameState))
|
||||
hostagesOffered.filterNot(hoie => FactionUtils.isFactionLeader(hoie.heroId, gameState.factions.values.toVector))
|
||||
|
||||
Option.when(
|
||||
sufficientTrust(
|
||||
|
||||
-2
@@ -3,14 +3,12 @@ package net.eagle0.eagle.library.util.command_choice_helpers
|
||||
import net.eagle0.eagle.{FactionId, HeroId}
|
||||
import net.eagle0.eagle.api.available_command.{AvailableCommand, SwearBrotherhoodAvailableCommand}
|
||||
import net.eagle0.eagle.api.selected_command.SwearBrotherhoodSelectedCommand
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.AvailableCommandSelector.flatSelectionForType
|
||||
import net.eagle0.eagle.library.util.CommandSelection
|
||||
|
||||
object SwearBrotherhoodCommandSelector {
|
||||
def chosenCommandForSpecificHero(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
availableCommands: Vector[AvailableCommand],
|
||||
desiredHeroId: HeroId
|
||||
): Option[CommandSelection] =
|
||||
|
||||
+14
-1
@@ -2,12 +2,25 @@ package net.eagle0.eagle.library.util.command_choice_helpers
|
||||
|
||||
import net.eagle0.eagle.internal.hero.Hero
|
||||
import net.eagle0.eagle.library.settings.{AiMinimumCharismaForSwornBrother, AiMinimumConstitutionForSwornBrother}
|
||||
import net.eagle0.eagle.library.util.hero.LegacyHeroUtils
|
||||
import net.eagle0.eagle.library.util.hero.{HeroUtils, LegacyHeroUtils}
|
||||
import net.eagle0.eagle.model.state.hero.HeroT
|
||||
import net.eagle0.eagle.HeroId
|
||||
|
||||
object SwornBrotherChooser {
|
||||
|
||||
/** Protoless version */
|
||||
def bestChoice(
|
||||
heroes: Iterable[HeroT],
|
||||
leaders: Vector[HeroId]
|
||||
): Option[HeroT] =
|
||||
heroes
|
||||
.filter(_.charisma >= AiMinimumCharismaForSwornBrother.doubleValue)
|
||||
.filter(_.constitution >= AiMinimumConstitutionForSwornBrother.doubleValue)
|
||||
.filterNot(h => leaders.contains(h.id))
|
||||
.maxByOption(HeroUtils.power)
|
||||
|
||||
/** Legacy proto version for backward compatibility */
|
||||
def bestChoiceProto(
|
||||
heroes: Iterable[Hero],
|
||||
leaders: Vector[HeroId]
|
||||
): Option[Hero] =
|
||||
|
||||
+3
-3
@@ -4,17 +4,17 @@ import net.eagle0.eagle.{FactionId, HeroId}
|
||||
import net.eagle0.eagle.api.available_command.{AvailableCommand, DiplomacyAvailableCommand}
|
||||
import net.eagle0.eagle.api.command.util.diplomacy_option.{DiplomacyOption, TruceOption}
|
||||
import net.eagle0.eagle.api.selected_command.DiplomacySelectedCommand
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.AvailableCommandSelector.ofType
|
||||
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
|
||||
import net.eagle0.eagle.library.util.faction_utils.FactionUtils
|
||||
import net.eagle0.eagle.library.util.CommandSelection
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
|
||||
object TruceOfferCommandSelector {
|
||||
private def usableHeroIds(
|
||||
hids: Vector[HeroId],
|
||||
gameState: GameState
|
||||
): Vector[HeroId] =
|
||||
hids.filterNot(LegacyFactionUtils.isFactionLeader(_, gameState))
|
||||
hids.filterNot(FactionUtils.isFactionLeader(_, gameState.factions.values.toVector))
|
||||
|
||||
private def truceOptionWithTarget(
|
||||
options: Vector[DiplomacyOption],
|
||||
|
||||
+12
-24
@@ -1,7 +1,5 @@
|
||||
package net.eagle0.eagle.library.util.command_choice_helpers
|
||||
|
||||
import net.eagle0.eagle.internal.faction.Faction.OutgoingOfferRound
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.library.settings.{
|
||||
MinimumMonthsBeforeRepeatInvitation,
|
||||
MinimumMonthsBeforeRepeatTruceOffer,
|
||||
@@ -9,7 +7,9 @@ import net.eagle0.eagle.library.settings.{
|
||||
MinimumTrustToOfferAlliance,
|
||||
MinimumTrustToOfferTruce
|
||||
}
|
||||
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
|
||||
import net.eagle0.eagle.library.util.faction_utils.FactionUtils
|
||||
import net.eagle0.eagle.model.state.faction.FactionT.OutgoingOfferRound
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.FactionId
|
||||
|
||||
object TrustForDiplomacy {
|
||||
@@ -48,10 +48,10 @@ object TrustForDiplomacy {
|
||||
targetFactionId: FactionId,
|
||||
gameState: GameState
|
||||
): Boolean =
|
||||
LegacyFactionUtils.trust(
|
||||
FactionUtils.trust(
|
||||
by = actingFactionId,
|
||||
of = targetFactionId,
|
||||
gameState
|
||||
factions = gameState.factions.values.toVector
|
||||
) >= MinimumTrustToOfferAlliance.intValue
|
||||
|
||||
private def hasSufficientTrustForTruce(
|
||||
@@ -59,10 +59,10 @@ object TrustForDiplomacy {
|
||||
targetFactionId: FactionId,
|
||||
gameState: GameState
|
||||
): Boolean =
|
||||
LegacyFactionUtils.trust(
|
||||
FactionUtils.trust(
|
||||
by = actingFactionId,
|
||||
of = targetFactionId,
|
||||
gameState
|
||||
factions = gameState.factions.values.toVector
|
||||
) >= MinimumTrustToOfferTruce.intValue
|
||||
|
||||
private def hasBeenLongEnoughForAlliance(
|
||||
@@ -74,11 +74,7 @@ object TrustForDiplomacy {
|
||||
.factions(actingFactionId)
|
||||
.lastOutgoingTruceOfferRounds
|
||||
.exists {
|
||||
case OutgoingOfferRound(
|
||||
toFactionId,
|
||||
roundId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
case OutgoingOfferRound(toFactionId, roundId) =>
|
||||
toFactionId == targetFactionId && roundId + MinimumMonthsBeforeRepeatTruceOffer.intValue > gameState.currentRoundId
|
||||
}
|
||||
|
||||
@@ -91,11 +87,7 @@ object TrustForDiplomacy {
|
||||
.factions(actingFactionId)
|
||||
.lastOutgoingTruceOfferRounds
|
||||
.exists {
|
||||
case OutgoingOfferRound(
|
||||
toFactionId,
|
||||
roundId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
case OutgoingOfferRound(toFactionId, roundId) =>
|
||||
toFactionId == targetFactionId && roundId + MinimumMonthsBeforeRepeatTruceOffer.intValue > gameState.currentRoundId
|
||||
}
|
||||
|
||||
@@ -119,10 +111,10 @@ object TrustForDiplomacy {
|
||||
targetFactionId: FactionId,
|
||||
gameState: GameState
|
||||
): Boolean =
|
||||
LegacyFactionUtils.trust(
|
||||
FactionUtils.trust(
|
||||
by = actingFactionId,
|
||||
of = targetFactionId,
|
||||
gameState
|
||||
factions = gameState.factions.values.toVector
|
||||
) >= MinimumTrustToInvite.intValue
|
||||
|
||||
private def hasBeenLongEnoughForInvitation(
|
||||
@@ -134,11 +126,7 @@ object TrustForDiplomacy {
|
||||
.factions(actingFactionId)
|
||||
.lastOutgoingInvitationRounds
|
||||
.exists {
|
||||
case OutgoingOfferRound(
|
||||
toFactionId,
|
||||
roundId,
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
case OutgoingOfferRound(toFactionId, roundId) =>
|
||||
toFactionId == targetFactionId && roundId + MinimumMonthsBeforeRepeatInvitation.intValue > gameState.currentRoundId
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user