mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-29 10:35:42 +00:00
Compare commits
47
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5cb23464e5 | ||
|
|
3e1919a715 | ||
|
|
699bcc684a | ||
|
|
42fd9c5853 | ||
|
|
01aa3dbace | ||
|
|
a7595e81a2 | ||
|
|
6a61634f37 | ||
|
|
cfa552270a | ||
|
|
09d560208e | ||
|
|
2188834ddf | ||
|
|
eafe5d73ee | ||
|
|
f16257591b | ||
|
|
d259282213 | ||
|
|
de5e6cc65e | ||
|
|
7da2fb7913 | ||
|
|
a485772e66 | ||
|
|
e0212fa7a2 | ||
|
|
6e76ff0fbc | ||
|
|
a37e0af028 | ||
|
|
d6704413d3 | ||
|
|
be59f9775f | ||
|
|
bebe050352 | ||
|
|
77d6fc571b | ||
|
|
65e0ad93cc | ||
|
|
275cb0c563 | ||
|
|
d64f35b18a | ||
|
|
ac0c03af9b | ||
|
|
6b0d58a9e7 | ||
|
|
fb56cf0029 | ||
|
|
70a5a517af | ||
|
|
6de07a70dc | ||
|
|
8e64af7233 | ||
|
|
ca91d3193e | ||
|
|
1de12597d7 | ||
|
|
36c09cae77 | ||
|
|
83d159823a | ||
|
|
5ce88f04d2 | ||
|
|
1bfcb343d9 | ||
|
|
88b628b73f | ||
|
|
9004e6887a | ||
|
|
c62b3d3166 | ||
|
|
6d54312aec | ||
|
|
c886ddc769 | ||
|
|
f6a7545533 | ||
|
|
8d3bc914ed | ||
|
|
844d7d8e50 | ||
|
|
cae2d47af0 |
@@ -11,6 +11,8 @@ on:
|
||||
- 'src/main/protobuf/net/eagle0/eagle/internal/auth_internal.proto'
|
||||
- 'ci/BUILD.bazel'
|
||||
- '.github/workflows/auth_build.yml'
|
||||
- 'nginx/nginx.conf'
|
||||
- 'docker-compose.prod.yml'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
push_images:
|
||||
@@ -110,6 +112,8 @@ jobs:
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
APPLE_SIGNIN_KEY_ID: ${{ secrets.APPLE_SIGNIN_KEY_ID }}
|
||||
APPLE_SIGNIN_PRIVATE_KEY: ${{ secrets.APPLE_SIGNIN_PRIVATE_KEY }}
|
||||
TWITCH_CLIENT_ID: ${{ secrets.TWITCH_CLIENT_ID }}
|
||||
TWITCH_CLIENT_SECRET: ${{ secrets.TWITCH_CLIENT_SECRET }}
|
||||
JWT_PRIVATE_KEY: ${{ secrets.JWT_PRIVATE_KEY }}
|
||||
FASTMAIL_API_TOKEN: ${{ secrets.FASTMAIL_API_TOKEN }}
|
||||
FASTMAIL_FROM_EMAIL: ${{ secrets.FASTMAIL_FROM_EMAIL }}
|
||||
@@ -118,6 +122,16 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Copy config files to server
|
||||
uses: appleboy/scp-action@v0.1.7
|
||||
with:
|
||||
host: ${{ secrets.DO_DROPLET_IP }}
|
||||
username: deploy
|
||||
key: ${{ secrets.DO_SSH_KEY }}
|
||||
source: "nginx/nginx.conf,docker-compose.prod.yml"
|
||||
target: "/opt/eagle0"
|
||||
strip_components: 0
|
||||
|
||||
- name: Deploy auth service to production
|
||||
uses: appleboy/ssh-action@v1.0.3
|
||||
with:
|
||||
@@ -125,7 +139,7 @@ jobs:
|
||||
username: deploy
|
||||
key: ${{ secrets.DO_SSH_KEY }}
|
||||
script_stop: true
|
||||
envs: AUTH_IMAGE,DISCORD_CLIENT_ID,DISCORD_CLIENT_SECRET,GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET,GH_OAUTH_CLIENT_ID,GH_OAUTH_CLIENT_SECRET,APPLE_SIGNIN_CLIENT_ID,APPLE_TEAM_ID,APPLE_SIGNIN_KEY_ID,APPLE_SIGNIN_PRIVATE_KEY,JWT_PRIVATE_KEY,FASTMAIL_API_TOKEN,FASTMAIL_FROM_EMAIL,FASTMAIL_FROM_NAME
|
||||
envs: AUTH_IMAGE,DISCORD_CLIENT_ID,DISCORD_CLIENT_SECRET,GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET,GH_OAUTH_CLIENT_ID,GH_OAUTH_CLIENT_SECRET,APPLE_SIGNIN_CLIENT_ID,APPLE_TEAM_ID,APPLE_SIGNIN_KEY_ID,APPLE_SIGNIN_PRIVATE_KEY,TWITCH_CLIENT_ID,TWITCH_CLIENT_SECRET,JWT_PRIVATE_KEY,FASTMAIL_API_TOKEN,FASTMAIL_FROM_EMAIL,FASTMAIL_FROM_NAME
|
||||
script: |
|
||||
set -x
|
||||
cd /opt/eagle0
|
||||
@@ -142,6 +156,8 @@ jobs:
|
||||
export APPLE_TEAM_ID="${APPLE_TEAM_ID}"
|
||||
export APPLE_SIGNIN_KEY_ID="${APPLE_SIGNIN_KEY_ID}"
|
||||
export APPLE_SIGNIN_PRIVATE_KEY="${APPLE_SIGNIN_PRIVATE_KEY}"
|
||||
export TWITCH_CLIENT_ID="${TWITCH_CLIENT_ID}"
|
||||
export TWITCH_CLIENT_SECRET="${TWITCH_CLIENT_SECRET}"
|
||||
export JWT_PRIVATE_KEY="${JWT_PRIVATE_KEY}"
|
||||
export FASTMAIL_API_TOKEN="${FASTMAIL_API_TOKEN}"
|
||||
export FASTMAIL_FROM_EMAIL="${FASTMAIL_FROM_EMAIL}"
|
||||
@@ -156,27 +172,47 @@ jobs:
|
||||
echo "Pulling Auth image..."
|
||||
docker pull "${AUTH_IMAGE}" || { echo "ERROR: Failed to pull auth image"; exit 1; }
|
||||
|
||||
# Only recreate the auth container (not eagle, shardok, etc.)
|
||||
docker compose -f docker-compose.prod.yml up -d --no-deps --force-recreate auth
|
||||
# Tag as :latest locally so any fallback uses correct image
|
||||
docker tag "${AUTH_IMAGE}" registry.digitalocean.com/eagle0/auth-server:latest
|
||||
|
||||
# Debug: check environment and .env file
|
||||
echo "DEBUG: AUTH_IMAGE=$AUTH_IMAGE"
|
||||
env | grep AUTH || echo "AUTH_IMAGE not in env output"
|
||||
if [ -f .env ]; then
|
||||
echo "DEBUG: .env file contents related to AUTH:"
|
||||
grep AUTH .env || echo "No AUTH in .env"
|
||||
fi
|
||||
|
||||
# Recreate auth container - pass AUTH_IMAGE explicitly on command line
|
||||
AUTH_IMAGE="${AUTH_IMAGE}" docker compose -f docker-compose.prod.yml up -d --no-deps --force-recreate auth
|
||||
|
||||
# Wait for health check
|
||||
sleep 5
|
||||
|
||||
# Verify container is using the correct image
|
||||
# Note: docker-compose may use :latest tag (which we tagged to the correct image)
|
||||
echo "=== Verifying auth container image ==="
|
||||
RUNNING_IMAGE=$(docker inspect auth-server --format '{{.Config.Image}}')
|
||||
RUNNING_DIGEST=$(docker inspect auth-server --format '{{.Image}}')
|
||||
EXPECTED_DIGEST=$(docker inspect "${AUTH_IMAGE}" --format '{{.Id}}')
|
||||
echo "Expected image: ${AUTH_IMAGE}"
|
||||
echo "Running image: ${RUNNING_IMAGE}"
|
||||
echo "Expected digest: ${EXPECTED_DIGEST}"
|
||||
echo "Running digest: ${RUNNING_DIGEST}"
|
||||
|
||||
if [ "$RUNNING_IMAGE" != "$AUTH_IMAGE" ]; then
|
||||
if [ "$RUNNING_DIGEST" != "$EXPECTED_DIGEST" ]; then
|
||||
echo "ERROR: Container is running wrong image!"
|
||||
echo "Container details:"
|
||||
docker inspect auth-server --format '{{.Id}} {{.Config.Image}}'
|
||||
exit 1
|
||||
fi
|
||||
echo "Image digests match - correct image is running"
|
||||
|
||||
# Show container status
|
||||
docker compose -f docker-compose.prod.yml ps auth
|
||||
|
||||
# Restart nginx with updated config (copied via SCP step)
|
||||
echo "=== Restarting nginx ==="
|
||||
docker compose -f docker-compose.prod.yml up -d --no-deps --force-recreate nginx
|
||||
echo "Nginx restarted"
|
||||
|
||||
# Cleanup old images
|
||||
docker image prune -f
|
||||
|
||||
@@ -5,9 +5,15 @@ on:
|
||||
branches: [ "main" ]
|
||||
paths:
|
||||
# Note: C++ changes trigger shardok_arm64_build.yml instead
|
||||
# Note: Auth changes trigger auth_build.yml instead
|
||||
- 'src/main/go/**'
|
||||
- '!src/main/go/net/eagle0/authservice/**'
|
||||
- '!src/main/go/net/eagle0/authcli/**'
|
||||
- 'src/main/scala/**'
|
||||
- 'src/main/protobuf/**'
|
||||
- '!src/main/protobuf/net/eagle0/eagle/api/auth.proto'
|
||||
- '!src/main/protobuf/net/eagle0/eagle/api/admin/**'
|
||||
- '!src/main/protobuf/net/eagle0/eagle/internal/auth_internal.proto'
|
||||
- 'src/main/resources/**'
|
||||
- 'ci/BUILD.bazel'
|
||||
- 'MODULE.bazel'
|
||||
@@ -198,7 +204,7 @@ jobs:
|
||||
# Create directory structure on remote
|
||||
ssh -i ~/.ssh/deploy_key deploy@"$DO_DROPLET_IP" bash -s << 'SETUP_DIRS'
|
||||
set -e
|
||||
mkdir -p /opt/eagle0/scripts/bin /opt/eagle0/nginx /opt/eagle0/deploy
|
||||
mkdir -p /opt/eagle0/scripts/bin /opt/eagle0/nginx
|
||||
rm -f /opt/eagle0/scripts/bin/warmup
|
||||
SETUP_DIRS
|
||||
|
||||
@@ -207,7 +213,6 @@ jobs:
|
||||
scp -i ~/.ssh/deploy_key nginx/nginx.conf deploy@"$DO_DROPLET_IP":/opt/eagle0/nginx/
|
||||
scp -i ~/.ssh/deploy_key scripts/deploy-blue-green.sh scripts/warmup-eagle.sh deploy@"$DO_DROPLET_IP":/opt/eagle0/scripts/
|
||||
scp -i ~/.ssh/deploy_key scripts/bin/warmup deploy@"$DO_DROPLET_IP":/opt/eagle0/scripts/bin/
|
||||
scp -i ~/.ssh/deploy_key deploy/env.template deploy/update-env.sh deploy@"$DO_DROPLET_IP":/opt/eagle0/deploy/
|
||||
|
||||
- name: Deploy to production droplet
|
||||
run: |
|
||||
@@ -215,6 +220,55 @@ jobs:
|
||||
set -ex
|
||||
cd /opt/eagle0
|
||||
|
||||
# =================================================================
|
||||
# CRITICAL: Validate environment variables before proceeding
|
||||
# This catches GitHub Actions secret store hiccups early
|
||||
# =================================================================
|
||||
validate_env() {
|
||||
local var_name="\$1"
|
||||
local var_value="\$2"
|
||||
local default_value="\${3:-}"
|
||||
|
||||
if [ -z "\${var_value}" ]; then
|
||||
echo "ERROR: \${var_name} is empty. GitHub Actions secrets may have failed to load."
|
||||
echo "Please retry the workflow."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check if we got a default value instead of the real secret
|
||||
if [ -n "\${default_value}" ] && [ "\${var_value}" = "\${default_value}" ]; then
|
||||
echo "ERROR: \${var_name} has default value '\${default_value}' instead of the actual secret."
|
||||
echo "This indicates GitHub Actions secrets failed to load. Please retry the workflow."
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
echo "Validating critical environment variables..."
|
||||
|
||||
# These are the raw values from GitHub Actions (before export)
|
||||
# We check them before exporting to catch issues early
|
||||
VALIDATION_FAILED=0
|
||||
|
||||
validate_env "SHARDOK_ADDRESS" "${SHARDOK_ADDRESS}" "" || VALIDATION_FAILED=1
|
||||
validate_env "EAGLE_IMAGE" "${EAGLE_IMAGE}" "" || VALIDATION_FAILED=1
|
||||
validate_env "JWT_PRIVATE_KEY" "${JWT_PRIVATE_KEY}" "" || VALIDATION_FAILED=1
|
||||
validate_env "DO_REGISTRY_TOKEN" "${DO_REGISTRY_TOKEN}" "" || VALIDATION_FAILED=1
|
||||
|
||||
if [ "\${VALIDATION_FAILED}" -eq 1 ]; then
|
||||
echo ""
|
||||
echo "========================================="
|
||||
echo "DEPLOYMENT ABORTED: Missing critical secrets"
|
||||
echo "This is likely a transient GitHub Actions issue."
|
||||
echo "Please retry the workflow."
|
||||
echo "========================================="
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "All critical environment variables validated successfully."
|
||||
|
||||
# =================================================================
|
||||
# Export environment variables for docker compose
|
||||
# These are passed via heredoc and exported so child processes (docker compose) can access them
|
||||
export EAGLE_IMAGE="${EAGLE_IMAGE}"
|
||||
@@ -236,7 +290,7 @@ jobs:
|
||||
export APPLE_TEAM_ID="${APPLE_TEAM_ID}"
|
||||
export APPLE_SIGNIN_KEY_ID="${APPLE_SIGNIN_KEY_ID}"
|
||||
export APPLE_SIGNIN_PRIVATE_KEY="${APPLE_SIGNIN_PRIVATE_KEY}"
|
||||
export SHARDOK_ADDRESS="${SHARDOK_ADDRESS:-shardok:40042}"
|
||||
export SHARDOK_ADDRESS="${SHARDOK_ADDRESS}"
|
||||
export SHARDOK_AUTH_TOKEN="${SHARDOK_AUTH_TOKEN}"
|
||||
export SENTRY_DSN="${SENTRY_DSN}"
|
||||
export FASTMAIL_API_TOKEN="${FASTMAIL_API_TOKEN}"
|
||||
@@ -276,20 +330,43 @@ jobs:
|
||||
|
||||
echo "All images pulled successfully"
|
||||
|
||||
# =================================================================
|
||||
# Verify Shardok connectivity before proceeding with deployment
|
||||
# This catches network/firewall issues early
|
||||
# =================================================================
|
||||
echo "Verifying Shardok connectivity..."
|
||||
SHARDOK_HOST=\$(echo "\${SHARDOK_ADDRESS}" | cut -d: -f1)
|
||||
SHARDOK_PORT=\$(echo "\${SHARDOK_ADDRESS}" | cut -d: -f2)
|
||||
|
||||
# Try to connect to Shardok (timeout after 10 seconds)
|
||||
if nc -z -w 10 "\${SHARDOK_HOST}" "\${SHARDOK_PORT}" 2>/dev/null; then
|
||||
echo "Shardok connectivity verified: \${SHARDOK_ADDRESS} is reachable"
|
||||
else
|
||||
echo ""
|
||||
echo "========================================="
|
||||
echo "ERROR: Cannot reach Shardok at \${SHARDOK_ADDRESS}"
|
||||
echo "This may indicate:"
|
||||
echo " - Shardok server is not running on Hetzner"
|
||||
echo " - Network/firewall issues between DigitalOcean and Hetzner"
|
||||
echo " - Incorrect SHARDOK_ADDRESS configuration"
|
||||
echo ""
|
||||
echo "DEPLOYMENT ABORTED: Shardok must be reachable for battles to work."
|
||||
echo "========================================="
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Stop local shardok container if running (now runs on Hetzner)
|
||||
docker stop shardok-server 2>/dev/null || true
|
||||
docker rm shardok-server 2>/dev/null || true
|
||||
|
||||
# Deploy Eagle with blue-green (handles eagle, nginx, admin, jfr-sidecar)
|
||||
# Note: Shardok runs on Hetzner, deployed separately via shardok_arm64_build.yml
|
||||
# Note: Auth is deployed separately via auth_build.yml - do NOT touch auth here
|
||||
chmod +x /opt/eagle0/scripts/*.sh
|
||||
[ -f "/opt/eagle0/scripts/bin/warmup" ] && chmod +x /opt/eagle0/scripts/bin/warmup
|
||||
GIT_SHA=\$(echo "\${EAGLE_IMAGE}" | sed 's/.*://')
|
||||
/opt/eagle0/scripts/deploy-blue-green.sh "\${GIT_SHA}"
|
||||
|
||||
# Ensure auth is running
|
||||
docker compose -f docker-compose.prod.yml up -d auth
|
||||
|
||||
# Verify
|
||||
sleep 10
|
||||
docker compose -f docker-compose.prod.yml ps
|
||||
|
||||
@@ -261,8 +261,9 @@ jobs:
|
||||
echo "$SPARKLE_EDDSA_PRIVATE_KEY" > "$SPARKLE_PRIVATE_KEY_PATH"
|
||||
chmod 600 "$SPARKLE_PRIVATE_KEY_PATH"
|
||||
|
||||
VERSION=$(git describe --tags --always)
|
||||
# Use commit count for automatic incrementing versions (e.g., 1.0.9548)
|
||||
BUILD_NUMBER=$(git rev-list --count HEAD)
|
||||
VERSION="1.0.${BUILD_NUMBER}"
|
||||
|
||||
bazel run //src/main/go/net/eagle0/build/mac_build_handler:mac_build_handler -- \
|
||||
"/tmp/eagle0/eagle0MAC/eagle0.app" \
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
# Environment template for production deployment
|
||||
# This file defines all env vars used by docker-compose.prod.yml
|
||||
# Workflows should update their specific vars without overwriting others
|
||||
|
||||
# Container images (managed by respective build workflows)
|
||||
# Note: Shardok runs on Hetzner, deployed via shardok_arm64_build.yml
|
||||
EAGLE_IMAGE=registry.digitalocean.com/eagle0/eagle-server:latest
|
||||
ADMIN_IMAGE=registry.digitalocean.com/eagle0/admin-server:latest
|
||||
JFR_SIDECAR_IMAGE=registry.digitalocean.com/eagle0/jfr-sidecar:latest
|
||||
AUTH_IMAGE=registry.digitalocean.com/eagle0/auth-server:latest
|
||||
|
||||
# OpenAI / LLM
|
||||
OPENAI_API_KEY=
|
||||
GPT_MODEL_NAME=gpt-4o
|
||||
|
||||
# DigitalOcean Spaces (S3-compatible storage)
|
||||
EAGLE_ENABLE_S3=false
|
||||
DO_SPACES_ACCESS_KEY=
|
||||
DO_SPACES_SECRET_KEY=
|
||||
|
||||
# JWT authentication
|
||||
JWT_PRIVATE_KEY=
|
||||
|
||||
# OAuth providers
|
||||
DISCORD_CLIENT_ID=
|
||||
DISCORD_CLIENT_SECRET=
|
||||
GOOGLE_CLIENT_ID=
|
||||
GOOGLE_CLIENT_SECRET=
|
||||
|
||||
# Shardok connection (Hetzner ARM64 server)
|
||||
SHARDOK_ADDRESS=
|
||||
SHARDOK_AUTH_TOKEN=
|
||||
|
||||
# Monitoring
|
||||
SENTRY_DSN=
|
||||
|
||||
# Email (Fastmail JMAP)
|
||||
FASTMAIL_API_TOKEN=
|
||||
FASTMAIL_FROM_EMAIL=
|
||||
FASTMAIL_FROM_NAME=
|
||||
@@ -1,65 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Update .env file without losing other variables
|
||||
# Usage: ./update-env.sh KEY1=value1 KEY2=value2 ...
|
||||
#
|
||||
# This script:
|
||||
# 1. Creates .env from template if it doesn't exist
|
||||
# 2. Updates only the specified KEY=value pairs
|
||||
# 3. Preserves all other existing values
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
ENV_FILE="${ENV_FILE:-/opt/eagle0/.env}"
|
||||
TEMPLATE_FILE="${TEMPLATE_FILE:-$SCRIPT_DIR/env.template}"
|
||||
|
||||
# Create .env from template if it doesn't exist
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
if [ -f "$TEMPLATE_FILE" ]; then
|
||||
echo "Creating .env from template..."
|
||||
grep -v '^#' "$TEMPLATE_FILE" | grep -v '^$' > "$ENV_FILE"
|
||||
else
|
||||
echo "Creating empty .env..."
|
||||
touch "$ENV_FILE"
|
||||
fi
|
||||
chmod 600 "$ENV_FILE"
|
||||
fi
|
||||
|
||||
# Process each KEY=VALUE argument
|
||||
for arg in "$@"; do
|
||||
# Skip empty args
|
||||
[ -z "$arg" ] && continue
|
||||
|
||||
# Parse KEY=VALUE
|
||||
KEY="${arg%%=*}"
|
||||
VALUE="${arg#*=}"
|
||||
|
||||
# Skip if no key
|
||||
[ -z "$KEY" ] && continue
|
||||
|
||||
# Skip setting empty values (keeps existing value)
|
||||
if [ -z "$VALUE" ]; then
|
||||
echo "Skipping $KEY (empty value)"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Remove existing line for this key and add new one
|
||||
# Use single quotes to avoid issues with special characters in values
|
||||
# (JSON strings, base64 with /, etc.)
|
||||
# Escape any single quotes in the value by replacing ' with '\''
|
||||
ESCAPED_VALUE="${VALUE//\'/\'\\\'\'}"
|
||||
|
||||
if grep -q "^${KEY}=" "$ENV_FILE" 2>/dev/null; then
|
||||
# Key exists, remove old line and add new one (sed with special chars is fragile)
|
||||
grep -v "^${KEY}=" "$ENV_FILE" > "$ENV_FILE.tmp" && mv "$ENV_FILE.tmp" "$ENV_FILE"
|
||||
echo "${KEY}='${ESCAPED_VALUE}'" >> "$ENV_FILE"
|
||||
echo "Updated $KEY"
|
||||
else
|
||||
# Key doesn't exist, add it
|
||||
echo "${KEY}='${ESCAPED_VALUE}'" >> "$ENV_FILE"
|
||||
echo "Added $KEY"
|
||||
fi
|
||||
done
|
||||
|
||||
chmod 600 "$ENV_FILE"
|
||||
echo "Done updating $ENV_FILE"
|
||||
+11
-10
@@ -21,7 +21,7 @@ services:
|
||||
- "--gpt-model-name"
|
||||
- "${GPT_MODEL_NAME:-gpt-5.1}"
|
||||
- "--shardok-interface-remote-address"
|
||||
- "${SHARDOK_ADDRESS:-shardok:40042}"
|
||||
- "${SHARDOK_ADDRESS}"
|
||||
- "--auth-service-url"
|
||||
- "auth:40033"
|
||||
ports:
|
||||
@@ -32,9 +32,8 @@ services:
|
||||
DO_SPACES_ENDPOINT: "${DO_SPACES_ENDPOINT:-https://sfo3.digitaloceanspaces.com}"
|
||||
DO_SPACES_ACCESS_KEY: "${DO_SPACES_ACCESS_KEY:-}"
|
||||
DO_SPACES_SECRET_KEY: "${DO_SPACES_SECRET_KEY:-}"
|
||||
JWT_PRIVATE_KEY: "${JWT_PRIVATE_KEY:-}"
|
||||
DISCORD_CLIENT_ID: "${DISCORD_CLIENT_ID:-}"
|
||||
DISCORD_CLIENT_SECRET: "${DISCORD_CLIENT_SECRET:-}"
|
||||
# JWT public key for token validation (auth service handles signing)
|
||||
# Reads from /etc/eagle0/keys/public.pem via shared volume
|
||||
# Auth token for Shardok on Hetzner (required)
|
||||
SHARDOK_AUTH_TOKEN: "${SHARDOK_AUTH_TOKEN:-}"
|
||||
# Use persistent volume for save data (users, games, etc.)
|
||||
@@ -47,7 +46,7 @@ services:
|
||||
- ./archived:/app/archived # Archived completed games
|
||||
- ./jfr:/app/jfr # JFR recordings - dump with: docker exec eagle-blue jcmd 1 JFR.dump filename=/app/jfr/profile.jfr
|
||||
- jvm-tmp:/tmp # Shared with jfr-sidecar for JVM attach socket files
|
||||
- jwt-keys:/etc/eagle0/keys # Shared JWT keys with auth service
|
||||
- jwt-keys:/etc/eagle0/keys:ro # JWT public key from auth service (read-only)
|
||||
depends_on:
|
||||
- auth
|
||||
restart: unless-stopped
|
||||
@@ -71,7 +70,7 @@ services:
|
||||
- "--gpt-model-name"
|
||||
- "${GPT_MODEL_NAME:-gpt-5.1}"
|
||||
- "--shardok-interface-remote-address"
|
||||
- "${SHARDOK_ADDRESS:-shardok:40042}"
|
||||
- "${SHARDOK_ADDRESS}"
|
||||
- "--auth-service-url"
|
||||
- "auth:40033"
|
||||
ports:
|
||||
@@ -82,9 +81,8 @@ services:
|
||||
DO_SPACES_ENDPOINT: "${DO_SPACES_ENDPOINT:-https://sfo3.digitaloceanspaces.com}"
|
||||
DO_SPACES_ACCESS_KEY: "${DO_SPACES_ACCESS_KEY:-}"
|
||||
DO_SPACES_SECRET_KEY: "${DO_SPACES_SECRET_KEY:-}"
|
||||
JWT_PRIVATE_KEY: "${JWT_PRIVATE_KEY:-}"
|
||||
DISCORD_CLIENT_ID: "${DISCORD_CLIENT_ID:-}"
|
||||
DISCORD_CLIENT_SECRET: "${DISCORD_CLIENT_SECRET:-}"
|
||||
# JWT public key for token validation (auth service handles signing)
|
||||
# Reads from /etc/eagle0/keys/public.pem via shared volume
|
||||
SHARDOK_AUTH_TOKEN: "${SHARDOK_AUTH_TOKEN:-}"
|
||||
EAGLE_SAVE_DIR: "/app/saves"
|
||||
EAGLE_ARCHIVE_DIR: "/app/archived"
|
||||
@@ -95,7 +93,7 @@ services:
|
||||
- ./archived:/app/archived # Same archive directory as blue
|
||||
- ./jfr:/app/jfr # JFR recordings (same as blue)
|
||||
- jvm-tmp:/tmp # Shared with jfr-sidecar-green for JVM attach socket files
|
||||
- jwt-keys:/etc/eagle0/keys # Shared JWT keys with auth service
|
||||
- jwt-keys:/etc/eagle0/keys:ro # JWT public key from auth service (read-only)
|
||||
depends_on:
|
||||
- auth
|
||||
restart: "no" # Don't auto-restart during deployment
|
||||
@@ -140,6 +138,9 @@ services:
|
||||
APPLE_TEAM_ID: "${APPLE_TEAM_ID:-}"
|
||||
APPLE_SIGNIN_KEY_ID: "${APPLE_SIGNIN_KEY_ID:-}"
|
||||
APPLE_SIGNIN_PRIVATE_KEY: "${APPLE_SIGNIN_PRIVATE_KEY:-}"
|
||||
# Twitch OAuth credentials
|
||||
TWITCH_CLIENT_ID: "${TWITCH_CLIENT_ID:-}"
|
||||
TWITCH_CLIENT_SECRET: "${TWITCH_CLIENT_SECRET:-}"
|
||||
# Server base URL for OAuth callbacks
|
||||
SERVER_BASE_URL: "${SERVER_BASE_URL:-https://prod.eagle0.net}"
|
||||
# JWT keys - PEM files in volume, bootstrapped from JWK on first run
|
||||
|
||||
+47
-28
@@ -45,9 +45,11 @@
|
||||
| Availability (`/library/actions/availability/`) | **0** | ✅ **Complete** |
|
||||
| Command Choice Helpers (`/library/util/command_choice_helpers/`) | **0** | ✅ **Complete** |
|
||||
| View Filters (`/library/util/view_filters/`) | **0** | ✅ **Complete** (except boundary code) |
|
||||
| Other Utilities (`/library/util/`) | **0** | ✅ **Complete** |
|
||||
| NewGameCreation (`/service/new_game_creation/`) | **0** | ✅ **Complete** (except input GameParameters proto) |
|
||||
| LLM Prompt Generators (`/library/actions/llm_prompt_generators/`) | **56** | ⏳ Remaining work (34 files) |
|
||||
| Other Utilities (`/library/util/`) | **7** | ⏳ Remaining work (2 files) |
|
||||
| Root Library (`/library/`) | **8** | Boundary code (3 files) |
|
||||
| LLM Request Generators (`/library/actions/llm_request_generators/`) | **2** | ⏳ Remaining work (1 file) |
|
||||
| Root Library (`/library/`) | **7** | Boundary code (4 files) |
|
||||
|
||||
**Total: ~65 proto imports remaining** (down from 149)
|
||||
|
||||
@@ -66,7 +68,7 @@
|
||||
| Phase 6c: AI Clients | **Complete** | AIClient, command choosers all protoless |
|
||||
| Phase 6d: CommandSelection | **Complete** | Renamed ScalaCommandSelection → CommandSelection |
|
||||
|
||||
### Recent Completions (PRs #5326, #5330, #5332, #5333, #5334, #5336, #5341, #5342)
|
||||
### Recent Completions (PRs #5326-#5390)
|
||||
|
||||
1. **CommandFactory protoless** - Now accepts/returns Scala `SelectedCommand` types
|
||||
2. **AI clients protoless** - `AIClient`, `MidGameAIClient`, all command choosers use Scala types
|
||||
@@ -81,6 +83,10 @@
|
||||
11. **StatWithConditionUtils & ProvinceEventUtils protoless** (PR #5342) - Deleted unused proto overloads
|
||||
12. **IncomingArmyUtils protoless** (PR #5348) - Deleted unused `stats()` method
|
||||
13. **RansomOfferHelpers deleted** (PR #5351) - Deleted unused dead code file
|
||||
14. **MapGenerator protoless** (PR #5373) - Returns Scala types instead of proto
|
||||
15. **Remove proto ActionResult from Engine layer** (PR #5378) - Engine uses Scala `ActionResultT`
|
||||
16. **NewGameCreation protoless** (PR #5381) - Uses Scala types throughout, no proto conversion
|
||||
17. **PersistedHistory Scala API** (PR #5390) - New `apply` overload accepts `Vector[ActionResultT]` directly
|
||||
|
||||
---
|
||||
|
||||
@@ -114,29 +120,26 @@ The LLM prompt generators still use proto types for hero/faction/province data:
|
||||
|
||||
**Priority**: Medium - These don't block other migrations and are isolated.
|
||||
|
||||
### Phase 9: Remaining Utilities (~1 proto import in 1 file)
|
||||
### Phase 9: Remaining Utilities ✅ Complete
|
||||
|
||||
| File | Proto Usage | Migration Path |
|
||||
|------|-------------|----------------|
|
||||
| `ProvinceEventUtils.scala` | ✅ **0** | Deleted unused proto overloads (PR #5342) |
|
||||
| `StatWithConditionUtils.scala` | ✅ **0** | Deleted unused proto overloads (PR #5342) |
|
||||
| `MapGenerator.scala` | 1 import | Convert to Scala types |
|
||||
| `MapGenerator.scala` | ✅ **0** | Returns Scala types (PR #5373) |
|
||||
| `IncomingArmyUtils.scala` | ✅ **0** | Deleted unused `stats()` method (PR #5348) |
|
||||
| `GameStateViewDiffer.scala` | ✅ **0** | Uses Scala diff types, converts at boundary |
|
||||
|
||||
### Phase 10: History APIs 🔄 In Progress
|
||||
### Phase 10: History APIs ✅ Complete
|
||||
|
||||
**Completed:**
|
||||
- `ActionWithResultingState` now has `scalaActionResult` property (lazy converted from proto)
|
||||
- `ActionResultFilter.includeForPlayer` uses Scala types (`ActionResultT`, `NotificationT`, `ActionResultType`)
|
||||
- `GameHistory.withNewResultsScala` preserves both Scala `GameState` and `ActionResultT` to avoid re-conversion
|
||||
- `PersistedHistory.apply` now accepts `Vector[ActionResultT]` directly (PR #5390)
|
||||
- `NewGameCreation` is fully protoless (PR #5381)
|
||||
|
||||
**Remaining:**
|
||||
```
|
||||
src/main/scala/net/eagle0/eagle/service/InMemoryHistory.scala
|
||||
src/main/scala/net/eagle0/eagle/service/PersistedHistory.scala
|
||||
```
|
||||
Change APIs to vend Scala `GameState` and `ActionResultT` instead of proto versions.
|
||||
**Note:** History internals still use proto for persistence (expected - proto is good for disk serialization).
|
||||
|
||||
---
|
||||
|
||||
@@ -163,9 +166,9 @@ Change APIs to vend Scala `GameState` and `ActionResultT` instead of proto versi
|
||||
### Remaining Proto Usage ⏳
|
||||
|
||||
- LLM prompt generators (56 imports in 34 files) - Medium priority
|
||||
- MapGenerator (1 import) - Low priority
|
||||
- LLM request generators (2 imports in 1 file) - Medium priority
|
||||
- BattleFilter (1 import) - Keep (boundary)
|
||||
- History API internals - Low priority
|
||||
- Root library boundary code (7 imports in 4 files) - Keep (boundary)
|
||||
|
||||
---
|
||||
|
||||
@@ -174,10 +177,12 @@ Change APIs to vend Scala `GameState` and `ActionResultT` instead of proto versi
|
||||
| Component | Files | Imports | Priority |
|
||||
|-----------|-------|---------|----------|
|
||||
| View Filters | 0 | 0 | ✅ Complete |
|
||||
| Utility files | 0 | 0 | ✅ Complete |
|
||||
| History APIs | 0 | 0 | ✅ Complete |
|
||||
| LLM Prompt Generators | 34 | 56 | Medium |
|
||||
| Utility files | 1 | 1 | Low |
|
||||
| Root Library (boundary) | 3 | 8 | Keep |
|
||||
| **Total** | **37** | **~65** | |
|
||||
| LLM Request Generators | 1 | 2 | Medium |
|
||||
| Root Library (boundary) | 4 | 7 | Keep |
|
||||
| **Total** | **~35** | **~65** | |
|
||||
|
||||
---
|
||||
|
||||
@@ -190,8 +195,10 @@ Change APIs to vend Scala `GameState` and `ActionResultT` instead of proto versi
|
||||
- [x] Zero proto imports in `/library/util/command_choice_helpers/`
|
||||
- [x] Zero proto imports in `/library/actions/impl/action/`
|
||||
- [x] Zero proto imports in `/library/util/view_filters/` (except BattleFilter boundary code)
|
||||
- [x] Zero proto imports in `/library/util/` (except view filters)
|
||||
- [x] Zero proto imports in `/service/new_game_creation/` (except input GameParameters)
|
||||
- [ ] Zero proto imports in `/library/actions/llm_prompt_generators/`
|
||||
- [ ] Zero proto imports in `/library/util/` (except view filters)
|
||||
- [ ] Zero proto imports in `/library/actions/llm_request_generators/`
|
||||
|
||||
### Architecture
|
||||
- [x] Clear separation: Scala models (internal) vs Proto (boundaries)
|
||||
@@ -202,6 +209,8 @@ Change APIs to vend Scala `GameState` and `ActionResultT` instead of proto versi
|
||||
- [x] HeroViewFilter returns Scala types
|
||||
- [x] ProvinceViewFilter returns Scala types
|
||||
- [x] All view filters return Scala types
|
||||
- [x] NewGameCreation uses Scala types
|
||||
- [x] PersistedHistory accepts Scala types for new games
|
||||
- [ ] LLM layer uses Scala types
|
||||
- [ ] No "proto creep" into business logic
|
||||
|
||||
@@ -219,7 +228,7 @@ Change APIs to vend Scala `GameState` and `ActionResultT` instead of proto versi
|
||||
|
||||
## Proto Import Inventory (Detailed)
|
||||
|
||||
**Current: ~65 proto imports across 37 files** (as of 2026-01-14)
|
||||
**Current: ~65 proto imports across ~35 files** (as of 2026-01-17)
|
||||
|
||||
### Summary by Directory
|
||||
|
||||
@@ -230,10 +239,11 @@ Change APIs to vend Scala `GameState` and `ActionResultT` instead of proto versi
|
||||
| `actions/impl/action/` | 0 | 0 | ✅ Clean |
|
||||
| `actions/impl/command/` | 0 | 0 | ✅ Clean |
|
||||
| `actions/llm_prompt_generators/` | 34 | 56 | LLM request types |
|
||||
| `actions/llm_request_generators/` | 1 | 2 | LLM request types |
|
||||
| `util/command_choice_helpers/` | 0 | 0 | ✅ Clean |
|
||||
| `util/view_filters/` | 1 | 1 | ✅ Clean (except boundary) |
|
||||
| `util/` (other) | 1 | 1 | ✅ Clean |
|
||||
| Root (`library/`) | 3 | 9 | Boundary code |
|
||||
| `util/view_filters/` | 0 | 0 | ✅ Clean (except boundary) |
|
||||
| `util/` (other) | 0 | 0 | ✅ Clean |
|
||||
| Root (`library/`) | 4 | 7 | Boundary code |
|
||||
|
||||
### util/view_filters/ (0 imports, except boundary)
|
||||
|
||||
@@ -243,19 +253,20 @@ Change APIs to vend Scala `GameState` and `ActionResultT` instead of proto versi
|
||||
| `GameStateViewDiffer.scala` | ✅ **0** | Uses Scala diff types, converts at edge |
|
||||
| `BattleFilter.scala` | 1 | Battle view boundary (keep proto) |
|
||||
|
||||
### util/ other files (1 import, 1 file)
|
||||
### util/ other files ✅ Complete
|
||||
|
||||
| File | Imports | Notes |
|
||||
|------|---------|-------|
|
||||
| `MapGenerator.scala` | 1 | Map generation |
|
||||
| `MapGenerator.scala` | ✅ **0** | Returns Scala types (PR #5373) |
|
||||
|
||||
### Root library/ (8 imports, 3 files)
|
||||
### Root library/ (7 imports, 4 files)
|
||||
|
||||
| File | Imports | Notes |
|
||||
|------|---------|-------|
|
||||
| `ActionResultFilter.scala` | 2 | Action result filtering (boundary, uses Scala types internally) |
|
||||
| `Engine.scala` | 1 | Trait interface |
|
||||
| `EngineImpl.scala` | 5 | Returns proto for persistence (boundary) |
|
||||
| `ActionResultFilter.scala` | 3 | Action result filtering (boundary, uses Scala types internally) |
|
||||
| `Engine.scala` | 1 | Trait interface (returns `ActionResultView` proto for client) |
|
||||
| `EngineImpl.scala` | 1 | Returns proto for persistence (boundary) |
|
||||
| `ActionWithResultingState.scala` | 2 | Stores both proto and Scala (boundary for persistence) |
|
||||
|
||||
### actions/llm_prompt_generators/ (56 imports, 34 files)
|
||||
|
||||
@@ -263,6 +274,14 @@ These files generate LLM prompts and primarily use `internal.generated_text_requ
|
||||
|
||||
**Migration strategy**: Can be migrated to Scala types when convenient, but low priority as they're isolated from core game logic.
|
||||
|
||||
### actions/llm_request_generators/ (2 imports, 1 file)
|
||||
|
||||
| File | Imports | Notes |
|
||||
|------|---------|-------|
|
||||
| `DiplomacyResolutionLlmRequestGenerator.scala` | 2 | Uses `DiplomacyOfferStatus` enum and `GeneratedTextRequest` |
|
||||
|
||||
**Migration strategy**: Create Scala `DiplomacyOfferStatus` enum and `GeneratedTextRequest` types.
|
||||
|
||||
---
|
||||
|
||||
## Migration Pattern
|
||||
|
||||
@@ -119,6 +119,13 @@ http {
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
# Steam OAuth callback (Steam uses OpenID 2.0)
|
||||
location /oauth/steam/callback {
|
||||
proxy_pass http://auth:8080;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
# Invitation landing page (proxied to Go auth service)
|
||||
location /invite/ {
|
||||
proxy_pass http://auth:8080;
|
||||
|
||||
@@ -39,32 +39,51 @@ find "$APP_PATH" -name "*.bundle" -print0 | while IFS= read -r -d '' item; do
|
||||
--sign "$SIGNING_IDENTITY" "$item"
|
||||
done
|
||||
|
||||
# Sign XPC services (inside Sparkle framework)
|
||||
# Sign XPC services (but skip ones inside Sparkle.framework - they're already signed)
|
||||
find "$APP_PATH" -name "*.xpc" -print0 | while IFS= read -r -d '' item; do
|
||||
if [[ "$item" == *"Sparkle.framework"* ]]; then
|
||||
echo "Skipping Sparkle XPC service (pre-signed): $item"
|
||||
continue
|
||||
fi
|
||||
echo "Signing XPC service: $item"
|
||||
codesign --force --verify --verbose --timestamp --options runtime \
|
||||
--sign "$SIGNING_IDENTITY" "$item"
|
||||
done
|
||||
|
||||
# Sign nested apps (like Sparkle's Updater.app)
|
||||
# Sign nested apps (but skip ones inside Sparkle.framework - they're already signed)
|
||||
find "$APP_PATH" -path "*/Frameworks/*.app" -print0 | while IFS= read -r -d '' item; do
|
||||
if [[ "$item" == *"Sparkle.framework"* ]]; then
|
||||
echo "Skipping Sparkle nested app (pre-signed): $item"
|
||||
continue
|
||||
fi
|
||||
echo "Signing nested app: $item"
|
||||
codesign --force --verify --verbose --timestamp --options runtime \
|
||||
--sign "$SIGNING_IDENTITY" "$item"
|
||||
done
|
||||
|
||||
# Sign standalone executables inside frameworks (like Autoupdate)
|
||||
# Sign standalone executables inside frameworks (but skip Sparkle.framework internals)
|
||||
find "$APP_PATH" -path "*/Frameworks/*/Versions/*/Autoupdate" -type f -print0 | while IFS= read -r -d '' item; do
|
||||
if [[ "$item" == *"Sparkle.framework"* ]]; then
|
||||
echo "Skipping Sparkle executable (pre-signed): $item"
|
||||
continue
|
||||
fi
|
||||
echo "Signing executable: $item"
|
||||
codesign --force --verify --verbose --timestamp --options runtime \
|
||||
--sign "$SIGNING_IDENTITY" "$item"
|
||||
done
|
||||
|
||||
# Sign all frameworks (after their contents are signed)
|
||||
# Use --deep for Sparkle.framework to handle its XPC services
|
||||
find "$APP_PATH" -name "*.framework" -print0 | while IFS= read -r -d '' item; do
|
||||
echo "Signing framework: $item"
|
||||
codesign --force --verify --verbose --timestamp --options runtime \
|
||||
--sign "$SIGNING_IDENTITY" "$item"
|
||||
if [[ "$item" == *"Sparkle.framework" ]]; then
|
||||
echo "Signing Sparkle framework with --deep: $item"
|
||||
codesign --deep --force --verify --verbose --timestamp --options runtime \
|
||||
--sign "$SIGNING_IDENTITY" "$item"
|
||||
else
|
||||
echo "Signing framework: $item"
|
||||
codesign --force --verify --verbose --timestamp --options runtime \
|
||||
--sign "$SIGNING_IDENTITY" "$item"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "=== Signing main app bundle ==="
|
||||
|
||||
+46
-19
@@ -27,33 +27,59 @@ if [ -z "${SPARKLE_EDDSA_PUBLIC_KEY:-}" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Download Sparkle if not cached
|
||||
# Always use a fresh Sparkle download to avoid cache corruption issues
|
||||
SPARKLE_DIR="$SPARKLE_CACHE_DIR/Sparkle-$SPARKLE_VERSION"
|
||||
if [ ! -d "$SPARKLE_DIR/Sparkle.framework" ]; then
|
||||
echo "=== Downloading Sparkle $SPARKLE_VERSION ==="
|
||||
mkdir -p "$SPARKLE_CACHE_DIR"
|
||||
SPARKLE_URL="https://github.com/sparkle-project/Sparkle/releases/download/${SPARKLE_VERSION}/Sparkle-${SPARKLE_VERSION}.tar.xz"
|
||||
curl -L "$SPARKLE_URL" | tar -xJ -C "$SPARKLE_CACHE_DIR"
|
||||
mv "$SPARKLE_CACHE_DIR/Sparkle-$SPARKLE_VERSION" "$SPARKLE_DIR" 2>/dev/null || true
|
||||
# If the extracted directory doesn't match version pattern, it may just be "Sparkle"
|
||||
if [ ! -d "$SPARKLE_DIR" ]; then
|
||||
mkdir -p "$SPARKLE_DIR"
|
||||
mv "$SPARKLE_CACHE_DIR/Sparkle.framework" "$SPARKLE_DIR/" 2>/dev/null || true
|
||||
mv "$SPARKLE_CACHE_DIR/bin" "$SPARKLE_DIR/" 2>/dev/null || true
|
||||
fi
|
||||
echo "=== Clearing Sparkle cache and downloading fresh copy ==="
|
||||
rm -rf "$SPARKLE_DIR"
|
||||
mkdir -p "$SPARKLE_DIR"
|
||||
|
||||
SPARKLE_URL="https://github.com/sparkle-project/Sparkle/releases/download/${SPARKLE_VERSION}/Sparkle-${SPARKLE_VERSION}.tar.xz"
|
||||
echo "Downloading from: $SPARKLE_URL"
|
||||
curl -L "$SPARKLE_URL" -o /tmp/sparkle.tar.xz
|
||||
tar -xJf /tmp/sparkle.tar.xz -C "$SPARKLE_DIR"
|
||||
rm /tmp/sparkle.tar.xz
|
||||
|
||||
# Show what was extracted
|
||||
echo "=== Extracted contents ==="
|
||||
ls -la "$SPARKLE_DIR/"
|
||||
|
||||
# The tarball extracts files directly, not into a subdirectory
|
||||
# Verify the framework has proper symlink structure
|
||||
echo "=== Verifying Sparkle.framework structure ==="
|
||||
ls -la "$SPARKLE_DIR/Sparkle.framework/"
|
||||
|
||||
if [ ! -L "$SPARKLE_DIR/Sparkle.framework/Sparkle" ]; then
|
||||
echo "ERROR: Sparkle.framework/Sparkle is not a symlink"
|
||||
file "$SPARKLE_DIR/Sparkle.framework/Sparkle"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -L "$SPARKLE_DIR/Sparkle.framework/Versions/Current" ]; then
|
||||
echo "ERROR: Sparkle.framework/Versions/Current is not a symlink"
|
||||
ls -la "$SPARKLE_DIR/Sparkle.framework/Versions/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Sparkle framework structure verified OK"
|
||||
|
||||
echo "=== Injecting Sparkle framework ==="
|
||||
FRAMEWORKS_DIR="$APP_PATH/Contents/Frameworks"
|
||||
mkdir -p "$FRAMEWORKS_DIR"
|
||||
|
||||
# Copy Sparkle framework
|
||||
cp -R "$SPARKLE_DIR/Sparkle.framework" "$FRAMEWORKS_DIR/"
|
||||
# Remove any existing Sparkle.framework in the app
|
||||
rm -rf "$FRAMEWORKS_DIR/Sparkle.framework"
|
||||
|
||||
# Also copy the XPC services if present
|
||||
if [ -d "$SPARKLE_DIR/Sparkle.framework/Versions/B/XPCServices" ]; then
|
||||
echo "Sparkle XPC services present"
|
||||
# Copy Sparkle framework (use ditto to preserve symlinks and bundle structure)
|
||||
ditto "$SPARKLE_DIR/Sparkle.framework" "$FRAMEWORKS_DIR/Sparkle.framework"
|
||||
|
||||
# Verify the copied framework still has proper structure
|
||||
echo "=== Verifying copied Sparkle.framework structure ==="
|
||||
ls -la "$FRAMEWORKS_DIR/Sparkle.framework/"
|
||||
if [ ! -L "$FRAMEWORKS_DIR/Sparkle.framework/Sparkle" ]; then
|
||||
echo "ERROR: Copied framework lost symlink structure"
|
||||
exit 1
|
||||
fi
|
||||
echo "Copied framework structure OK"
|
||||
|
||||
echo "=== Updating Info.plist ==="
|
||||
PLIST_PATH="$APP_PATH/Contents/Info.plist"
|
||||
@@ -69,8 +95,9 @@ PLIST_PATH="$APP_PATH/Contents/Info.plist"
|
||||
/usr/libexec/PlistBuddy -c "Add :SUEnableAutomaticChecks bool true" "$PLIST_PATH"
|
||||
|
||||
# Set bundle version from git for Sparkle version comparison
|
||||
VERSION=$(git describe --tags --always 2>/dev/null || echo "1.0.0")
|
||||
# Use commit count for automatic incrementing versions (e.g., 1.0.9548)
|
||||
BUILD_NUMBER=$(git rev-list --count HEAD 2>/dev/null || echo "1")
|
||||
VERSION="1.0.${BUILD_NUMBER}"
|
||||
|
||||
echo "Setting version: $VERSION (build $BUILD_NUMBER)"
|
||||
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $VERSION" "$PLIST_PATH" 2>/dev/null || \
|
||||
|
||||
@@ -87,7 +87,6 @@
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Demo/LaunchURL.cs" />
|
||||
<Compile Include="Assets/Shardok/HexCoordinates.cs" />
|
||||
<Compile Include="Assets/Eagle/Table Rows/OrganizeTroopsTableRow.cs" />
|
||||
<Compile Include="Assets/Bluetooth/PickerRowController.cs" />
|
||||
<Compile Include="Assets/TouchHandler.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/TravelCommandSelector.cs" />
|
||||
<Compile Include="Assets/Eagle/ConnectionStatusUI.cs" />
|
||||
@@ -98,7 +97,6 @@
|
||||
<Compile Include="Assets/HoveringTooltipTextProvider.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/util/KeyModifiedAmount.cs" />
|
||||
<Compile Include="Assets/HoveringTooltip.cs" />
|
||||
<Compile Include="Assets/Bluetooth/DiceInterface.cs" />
|
||||
<Compile Include="Assets/ButtonColors.cs" />
|
||||
<Compile Include="Assets/Eagle/MapController.cs" />
|
||||
<Compile Include="Assets/common/DisclosureTriangle.cs" />
|
||||
@@ -151,10 +149,8 @@
|
||||
<Compile Include="Assets/Eagle/GeneratedTextUpdater.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/OutlawApprehendedDetailsNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Shardok/HexMetrics.cs" />
|
||||
<Compile Include="Assets/Bluetooth/OneDiceRollController.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Switch/SwitchManager.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/ProvinceHeldDetailsNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Bluetooth/DieInfo.cs" />
|
||||
<Compile Include="Assets/common/ResourceFetcher.cs" />
|
||||
<Compile Include="Assets/Tutorial/Content/TutorialStep.cs" />
|
||||
<Compile Include="Assets/Auth/AuthClient.cs" />
|
||||
@@ -162,12 +158,11 @@
|
||||
<Compile Include="Assets/Eagle/Notifications/RiotSuppressedNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Modal Window/ModalWindowManagerEditor.cs" />
|
||||
<Compile Include="Assets/common/GUIUtils/AutoScrollingText.cs" />
|
||||
<Compile Include="Assets/Bluetooth/NativeDiceInterfaceImports.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/ProvinceConqueredNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Eagle/Table Rows/ExtraTroopsRowController.cs" />
|
||||
<Compile Include="Assets/Auth/JwtAuthInterceptor.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandPanelController.cs" />
|
||||
<Compile Include="Assets/Tutorial/Content/TutorialContentDefinitions.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandPanelController.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/ShatteredArmyDetailsNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Shardok/TurnHistoryPanelController.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/HeroDepartureDetailsNotificationGenerator.cs" />
|
||||
@@ -212,7 +207,6 @@
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/ExileVassalCommandSelector.cs" />
|
||||
<Compile Include="Assets/Tutorial/TutorialState.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Button/ButtonManagerBasicWithIconEditor.cs" />
|
||||
<Compile Include="Assets/Bluetooth/DiceVectors.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/UI Manager/UIManagerProgressBarLoop.cs" />
|
||||
<Compile Include="Assets/Eagle/MovingArmiesTableController.cs" />
|
||||
<Compile Include="Assets/ConnectionHandler/ConnectionHandler.cs" />
|
||||
@@ -233,9 +227,11 @@
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/DiplomacyCommandSelector.cs" />
|
||||
<Compile Include="Assets/Tutorial/Triggers/TutorialTriggerRegistry.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/ResolveBreakAllianceCommandSelector.cs" />
|
||||
<Compile Include="Assets/common/WindowFocusManager.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/ResolveAllianceCommandSelector.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/WithdrewForTruceDetailsNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/MarchCommandSelector.cs" />
|
||||
<Compile Include="Assets/Tutorial/TutorialTargetRegistry.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/TrainCommandSelector.cs" />
|
||||
<Compile Include="Assets/Eagle/CustomFileLogger.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/InvitationAcceptedDetailsNotificationGenerator.cs" />
|
||||
@@ -269,7 +265,6 @@
|
||||
<Compile Include="Assets/Shardok/AnimationTestController.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/TradeCommandSelector.cs" />
|
||||
<Compile Include="Assets/Eagle/Table Rows/DominionTableRowController.cs" />
|
||||
<Compile Include="Assets/Bluetooth/RollPanelController.cs" />
|
||||
<Compile Include="Assets/Shardok/ChargeAnimator.cs" />
|
||||
<Compile Include="Assets/Eagle/ClientTextProvider.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/UI Manager/UIManager.cs" />
|
||||
@@ -304,6 +299,7 @@
|
||||
<Compile Include="Assets/Shardok/ControlAnimator.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Horizontal Selector/HorizontalSelector.cs" />
|
||||
<Compile Include="Assets/Eagle/Table Rows/OrganizeTroopsResultRow.cs" />
|
||||
<Compile Include="Assets/Eagle/SparkleUpdater.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Slider/SliderManagerEditor.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/SwearBrotherhoodDetailsNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Eagle/PersistentClientConnection.cs" />
|
||||
@@ -322,10 +318,10 @@
|
||||
<Compile Include="Assets/Shardok/ShardokGameModel.cs" />
|
||||
<Compile Include="Assets/common/GUIUtils/GeneralClickDetector.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/UI Manager/UIManagerContextMenu.cs" />
|
||||
<Compile Include="Assets/Eagle/SparkleInitializer.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/TruceAmbassadorImprisonedDetailsNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Button/ButtonManagerBasicWithIcon.cs" />
|
||||
<Compile Include="Assets/Tutorial/UI/TutorialHintIndicator.cs" />
|
||||
<Compile Include="Assets/Bluetooth/UnityDieColors.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/OrganizeTroopsCommandSelector.cs" />
|
||||
<Compile Include="Assets/Shardok/ExtinguishAnimator.cs" />
|
||||
<Compile Include="Assets/Eagle/PanelPositions.cs" />
|
||||
@@ -338,7 +334,6 @@
|
||||
<Compile Include="Assets/EagleConnection.cs" />
|
||||
<Compile Include="Assets/GUI Pro Kit Fantasy RPG/Scripts/CtrPanel.cs" />
|
||||
<Compile Include="Assets/UI/Scripts/SceneLoadTester.cs" />
|
||||
<Compile Include="Assets/Bluetooth/RollFetcher.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/HandleRiotGiveCommandSelector.cs" />
|
||||
<Compile Include="Assets/Eagle/ProvinceUtils.cs" />
|
||||
<Compile Include="Assets/common/GUIUtils/ProvinceStatUtils.cs" />
|
||||
@@ -372,16 +367,15 @@
|
||||
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/SuppressBeastsFailedDetailsNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Terrain Hexes/Example Scene/BasicHexArranger.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/ManagePrisonersCommandSelector.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/SendSuppliesCommandSelector.cs" />
|
||||
<Compile Include="Assets/ConnectionHandler/StoredAccountButton.cs" />
|
||||
<Compile Include="Assets/Tutorial/UI/TutorialUIManager.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/SendSuppliesCommandSelector.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Window/WindowManager.cs" />
|
||||
<Compile Include="Assets/Auth/InvitationCodeManager.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/CapturedHeroExiledDetailsNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/SuppressBeastsSucceededNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Shardok/ReservesTableController.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/CommandSelector.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/RestCommandSelector.cs" />
|
||||
<Compile Include="Assets/Bluetooth/DiceConfigurationPanelController.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/UI Manager/UIManagerDropdown.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/UI Manager/UIManagerButton.cs" />
|
||||
<Compile Include="Assets/common/GUIUtils/GUIUtils.cs" />
|
||||
|
||||
+55
-63
@@ -87,20 +87,20 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
public Button discordLoginButton;
|
||||
public Button googleLoginButton;
|
||||
public Button githubLoginButton;
|
||||
public Button appleLoginButton;
|
||||
public Button steamLoginButton;
|
||||
public Button twitchLoginButton;
|
||||
public TextMeshProUGUI oauthStatusText;
|
||||
|
||||
[Header("Stored Accounts")]
|
||||
public GameObject storedAccountsContainer;
|
||||
public GameObject storedAccountButtonPrefab;
|
||||
public StoredAccountButton storedAccountButtonPrefab;
|
||||
public Sprite discordProviderIcon;
|
||||
public Sprite googleProviderIcon;
|
||||
public Sprite githubProviderIcon;
|
||||
|
||||
[Header("Display Name Setup")]
|
||||
public GameObject displayNamePanel;
|
||||
public TMP_InputField displayNameField;
|
||||
public Button setDisplayNameButton;
|
||||
public TextMeshProUGUI displayNameErrorText;
|
||||
public Sprite appleProviderIcon;
|
||||
public Sprite steamProviderIcon;
|
||||
public Sprite twitchProviderIcon;
|
||||
|
||||
[Header("Status Display")]
|
||||
public TextMeshProUGUI connectionStatusText;
|
||||
@@ -121,6 +121,7 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
public Button cancelCustomBattleButton;
|
||||
public TMP_Dropdown lobbyEnvironmentDropdown;
|
||||
public TextMeshProUGUI lobbyUserText;
|
||||
public TextMeshProUGUI lobbyStatusText;
|
||||
|
||||
public GameObject runningGamesListArea;
|
||||
public GameObject runningGamesListItemPrefab;
|
||||
@@ -147,14 +148,12 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
private CancellationTokenSource _cancellationTokenSource;
|
||||
private readonly Object pendingReplyLock = new Object();
|
||||
private int _connectedEnvironmentIndex = -1; // -1 means not connected
|
||||
private List<GameObject> _storedAccountButtons = new List<GameObject>();
|
||||
private List<StoredAccountButton> _storedAccountButtons = new();
|
||||
|
||||
public ClientPregeneratedText clientPregeneratedText;
|
||||
|
||||
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.", "" };
|
||||
@@ -260,15 +259,20 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
if (githubLoginButton != null) {
|
||||
githubLoginButton.onClick.AddListener(() => OnOAuthLoginClicked(OAuthProvider.Github));
|
||||
}
|
||||
if (setDisplayNameButton != null) {
|
||||
setDisplayNameButton.onClick.AddListener(OnSetDisplayNameClicked);
|
||||
if (appleLoginButton != null) {
|
||||
appleLoginButton.onClick.AddListener(() => OnOAuthLoginClicked(OAuthProvider.Apple));
|
||||
}
|
||||
if (steamLoginButton != null) {
|
||||
steamLoginButton.onClick.AddListener(() => OnOAuthLoginClicked(OAuthProvider.Steam));
|
||||
}
|
||||
if (twitchLoginButton != null) {
|
||||
twitchLoginButton.onClick.AddListener(() => OnOAuthLoginClicked(OAuthProvider.Twitch));
|
||||
}
|
||||
|
||||
// Subscribe to OAuthManager events
|
||||
if (OAuthManager.Instance != null) {
|
||||
OAuthManager.Instance.OnLoginSuccess += OnOAuthLoginSuccess;
|
||||
OAuthManager.Instance.OnLoginFailed += OnOAuthLoginFailed;
|
||||
OAuthManager.Instance.OnNewUserNeedsDisplayName += OnNewUserNeedsDisplayName;
|
||||
OAuthManager.Instance.OnLogout += OnOAuthLogout;
|
||||
}
|
||||
|
||||
@@ -277,9 +281,6 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
}
|
||||
|
||||
private void ShowAuthPanel() {
|
||||
// Hide other panels
|
||||
if (displayNamePanel != null) displayNamePanel.SetActive(false);
|
||||
|
||||
// Show OAuth panel
|
||||
if (oauthPanel != null) oauthPanel.SetActive(true);
|
||||
|
||||
@@ -297,9 +298,13 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
}
|
||||
|
||||
private void RefreshStoredAccountButtons() {
|
||||
// Clear existing buttons
|
||||
foreach (var btn in _storedAccountButtons) {
|
||||
if (btn != null) Destroy(btn);
|
||||
// Clear ALL children of the container
|
||||
// Disable immediately (Destroy is deferred to end of frame)
|
||||
if (storedAccountsContainer != null) {
|
||||
foreach (Transform child in storedAccountsContainer.transform) {
|
||||
child.gameObject.SetActive(false);
|
||||
Destroy(child.gameObject);
|
||||
}
|
||||
}
|
||||
_storedAccountButtons.Clear();
|
||||
|
||||
@@ -316,28 +321,33 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
buttonObj.transform.localScale = Vector3.one;
|
||||
|
||||
// Set button text (just display name, icon shows provider)
|
||||
var buttonText = buttonObj.GetComponentInChildren<TextMeshProUGUI>();
|
||||
if (buttonText != null) { buttonText.text = account.DisplayName; }
|
||||
buttonObj.label.text = account.DisplayName;
|
||||
|
||||
// Set provider icon (look for child named "ProviderIcon")
|
||||
var providerIconTransform = buttonObj.transform.Find("ProviderIcon");
|
||||
if (providerIconTransform != null) {
|
||||
var providerImage = providerIconTransform.GetComponent<Image>();
|
||||
if (providerImage != null) {
|
||||
var providerLower = account.Provider.ToLowerInvariant();
|
||||
providerImage.sprite = providerLower switch { "google" => googleProviderIcon,
|
||||
"github" => githubProviderIcon,
|
||||
_ => discordProviderIcon };
|
||||
}
|
||||
var providerImage = buttonObj.icon;
|
||||
if (providerImage != null) {
|
||||
var providerLower = account.Provider.ToLowerInvariant();
|
||||
providerImage.sprite = providerLower switch { "google" => googleProviderIcon,
|
||||
"github" => githubProviderIcon,
|
||||
"apple" => appleProviderIcon,
|
||||
"steam" => steamProviderIcon,
|
||||
"twitch" => twitchProviderIcon,
|
||||
_ => discordProviderIcon };
|
||||
}
|
||||
|
||||
// Set click handler
|
||||
// Set click handler for sign-in
|
||||
var button = buttonObj.GetComponent<Button>();
|
||||
if (button != null) {
|
||||
var accountCopy = account; // Capture for lambda
|
||||
button.onClick.AddListener(() => OnStoredAccountClicked(accountCopy));
|
||||
}
|
||||
|
||||
// Set click handler for delete button
|
||||
if (buttonObj.deleteButton != null) {
|
||||
var accountCopy = account; // Capture for lambda
|
||||
buttonObj.deleteButton.onClick.AddListener(
|
||||
() => OnDeleteStoredAccount(accountCopy));
|
||||
}
|
||||
|
||||
_storedAccountButtons.Add(buttonObj);
|
||||
}
|
||||
}
|
||||
@@ -359,8 +369,13 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDeleteStoredAccount(StoredAccount account) {
|
||||
Debug.Log($"[ConnectionHandler] Deleting stored account: {account.DisplayName}");
|
||||
OAuthManager.Instance?.RemoveStoredAccount(account);
|
||||
RefreshStoredAccountButtons();
|
||||
}
|
||||
|
||||
private void SetupLobbyUI() {
|
||||
if (logoutButton != null) { logoutButton.onClick.AddListener(OnLogoutClicked); }
|
||||
if (customBattleButton != null) { customBattleButton.onClick.AddListener(CustomBattle); }
|
||||
if (cancelCustomBattleButton != null) {
|
||||
cancelCustomBattleButton.onClick.AddListener(CancelCustomBattle);
|
||||
@@ -411,13 +426,16 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
foreach (Transform row in runningGamesListArea.transform) { Destroy(row.gameObject); }
|
||||
foreach (Transform row in availableGamesListArea.transform) { Destroy(row.gameObject); }
|
||||
|
||||
// Show loading indicator
|
||||
if (lobbyStatusText != null) { lobbyStatusText.text = "Switching environment..."; }
|
||||
|
||||
// Reconnect to new environment
|
||||
_createConnection();
|
||||
RequestMaps();
|
||||
StartListeningForLobbyUpdates();
|
||||
}
|
||||
|
||||
private async void OnLogoutClicked() {
|
||||
public async void OnLogoutClicked() {
|
||||
Debug.Log("[ConnectionHandler] Logout button clicked");
|
||||
|
||||
// Clear OAuth tokens
|
||||
@@ -492,35 +510,6 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
});
|
||||
}
|
||||
|
||||
private void OnNewUserNeedsDisplayName(bool isNewUser) {
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
// Show display name panel
|
||||
if (oauthPanel != null) oauthPanel.SetActive(false);
|
||||
if (displayNamePanel != null) displayNamePanel.SetActive(true);
|
||||
if (displayNameErrorText != null) displayNameErrorText.text = "";
|
||||
});
|
||||
}
|
||||
|
||||
private async void OnSetDisplayNameClicked() {
|
||||
if (displayNameField == null || OAuthManager.Instance == null) return;
|
||||
|
||||
var displayName = displayNameField.text.Trim();
|
||||
if (string.IsNullOrEmpty(displayName)) {
|
||||
if (displayNameErrorText != null) {
|
||||
displayNameErrorText.text = "Please enter a display name";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (displayNameErrorText != null) { displayNameErrorText.text = "Setting display name..."; }
|
||||
|
||||
var success = await OAuthManager.Instance.SetDisplayNameAsync(displayName);
|
||||
if (!success && displayNameErrorText != null) {
|
||||
displayNameErrorText.text = "Name already taken or invalid. Try another.";
|
||||
}
|
||||
// OnLoginSuccess will be called if successful
|
||||
}
|
||||
|
||||
private void OnOAuthLogout() {
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
ShowAuthPanel();
|
||||
@@ -724,6 +713,9 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
availableGameItem.JoinCallback = this.JoinGame;
|
||||
availableGameItem.DropCallback = this.DropGame;
|
||||
}
|
||||
|
||||
// Clear loading indicator now that lobby is populated
|
||||
if (lobbyStatusText != null) { lobbyStatusText.text = ""; }
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -602,7 +602,7 @@ MonoBehaviour:
|
||||
m_SelectOnRight: {fileID: 0}
|
||||
m_Transition: 1
|
||||
m_Colors:
|
||||
m_NormalColor: {r: 1, g: 0.42745098, b: 0.42745098, a: 1}
|
||||
m_NormalColor: {r: 1, g: 1, b: 1, a: 0}
|
||||
m_HighlightedColor: {r: 0.84313726, g: 0.84313726, b: 0.84313726, a: 1}
|
||||
m_PressedColor: {r: 0, g: 0, b: 0, a: 1}
|
||||
m_SelectedColor: {r: 0.84313726, g: 0.84313726, b: 0.84313726, a: 1}
|
||||
@@ -1151,7 +1151,7 @@ MonoBehaviour:
|
||||
m_Right: 0
|
||||
m_Top: 0
|
||||
m_Bottom: 0
|
||||
m_ChildAlignment: 0
|
||||
m_ChildAlignment: 3
|
||||
m_Spacing: 0
|
||||
m_ChildForceExpandWidth: 0
|
||||
m_ChildForceExpandHeight: 0
|
||||
@@ -1256,14 +1256,14 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.RawImage
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 0, g: 0, b: 0, a: 1}
|
||||
m_Color: {r: 1, g: 0, b: 0, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Texture: {fileID: 2800000, guid: 8c998213d53883d4b8fdf7a3eb3c2909, type: 3}
|
||||
m_Texture: {fileID: 2800000, guid: 88dca061d041a2a4596563e6a38f66c8, type: 3}
|
||||
m_UVRect:
|
||||
serializedVersion: 2
|
||||
x: 0
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
public class StoredAccountButton : MonoBehaviour {
|
||||
public TMP_Text label;
|
||||
public Image icon;
|
||||
public Button deleteButton;
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c2ab265dfad44a1aac98c4eaba3969bc
|
||||
timeCreated: 1768606277
|
||||
+368
-29
@@ -1,5 +1,62 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!1 &602258008028479859
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 8558900591190483184}
|
||||
- component: {fileID: 5225486432737178588}
|
||||
m_Layer: 5
|
||||
m_Name: ProviderIconContainer
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &8558900591190483184
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 602258008028479859}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 6242087783640990751}
|
||||
m_Father: {fileID: 157686408557551303}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &5225486432737178588
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 602258008028479859}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
|
||||
m_IgnoreLayout: 0
|
||||
m_MinWidth: -1
|
||||
m_MinHeight: -1
|
||||
m_PreferredWidth: 80
|
||||
m_PreferredHeight: 50
|
||||
m_FlexibleWidth: -1
|
||||
m_FlexibleHeight: -1
|
||||
m_LayoutPriority: 1
|
||||
--- !u!1 &1262787222422545696
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -10,7 +67,6 @@ GameObject:
|
||||
m_Component:
|
||||
- component: {fileID: 6242087783640990751}
|
||||
- component: {fileID: 5444015614922696687}
|
||||
- component: {fileID: 8871991993173924918}
|
||||
- component: {fileID: 6965305925514130928}
|
||||
m_Layer: 5
|
||||
m_Name: ProviderIcon
|
||||
@@ -26,17 +82,17 @@ RectTransform:
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1262787222422545696}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 157686408557551303}
|
||||
m_Father: {fileID: 8558900591190483184}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: -10, y: -10}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &5444015614922696687
|
||||
CanvasRenderer:
|
||||
@@ -46,26 +102,6 @@ CanvasRenderer:
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1262787222422545696}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &8871991993173924918
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1262787222422545696}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
|
||||
m_IgnoreLayout: 0
|
||||
m_MinWidth: -1
|
||||
m_MinHeight: -1
|
||||
m_PreferredWidth: 80
|
||||
m_PreferredHeight: 50
|
||||
m_FlexibleWidth: -1
|
||||
m_FlexibleHeight: -1
|
||||
m_LayoutPriority: 1
|
||||
--- !u!114 &6965305925514130928
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -86,7 +122,7 @@ MonoBehaviour:
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 21300000, guid: b6b68c8c37d5cf1419267a80c426ac83, type: 3}
|
||||
m_Sprite: {fileID: 21300000, guid: af9f637b3a1f6433f819a0bcdb326453, type: 3}
|
||||
m_Type: 0
|
||||
m_PreserveAspect: 1
|
||||
m_FillCenter: 1
|
||||
@@ -96,6 +132,62 @@ MonoBehaviour:
|
||||
m_FillOrigin: 0
|
||||
m_UseSpriteMesh: 0
|
||||
m_PixelsPerUnitMultiplier: 1
|
||||
--- !u!1 &4773346950019690658
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 9042872047974783334}
|
||||
- component: {fileID: 1446804990396579382}
|
||||
m_Layer: 5
|
||||
m_Name: Spacer
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &9042872047974783334
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 4773346950019690658}
|
||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 157686408557551303}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1446804990396579382
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 4773346950019690658}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
|
||||
m_IgnoreLayout: 0
|
||||
m_MinWidth: -1
|
||||
m_MinHeight: -1
|
||||
m_PreferredWidth: 5
|
||||
m_PreferredHeight: -1
|
||||
m_FlexibleWidth: -1
|
||||
m_FlexibleHeight: -1
|
||||
m_LayoutPriority: 1
|
||||
--- !u!1 &5166012670666351313
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -110,6 +202,7 @@ GameObject:
|
||||
- component: {fileID: 646268409010340088}
|
||||
- component: {fileID: 3097176747511135857}
|
||||
- component: {fileID: 7691398338434978342}
|
||||
- component: {fileID: 369281190448318618}
|
||||
m_Layer: 5
|
||||
m_Name: StoredAccountButton
|
||||
m_TagString: Untagged
|
||||
@@ -130,13 +223,15 @@ RectTransform:
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 7974514965529143181}
|
||||
- {fileID: 6242087783640990751}
|
||||
- {fileID: 8558900591190483184}
|
||||
- {fileID: 1544713289774970384}
|
||||
- {fileID: 9042872047974783334}
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 400, y: 60}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &3634401029247236684
|
||||
CanvasRenderer:
|
||||
@@ -258,7 +353,7 @@ MonoBehaviour:
|
||||
m_Top: 0
|
||||
m_Bottom: 0
|
||||
m_ChildAlignment: 5
|
||||
m_Spacing: 0
|
||||
m_Spacing: 5
|
||||
m_ChildForceExpandWidth: 0
|
||||
m_ChildForceExpandHeight: 0
|
||||
m_ChildControlWidth: 1
|
||||
@@ -266,6 +361,21 @@ MonoBehaviour:
|
||||
m_ChildScaleWidth: 0
|
||||
m_ChildScaleHeight: 0
|
||||
m_ReverseArrangement: 0
|
||||
--- !u!114 &369281190448318618
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5166012670666351313}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: c2ab265dfad44a1aac98c4eaba3969bc, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::StoredAccountButton
|
||||
label: {fileID: 6210669843299902677}
|
||||
icon: {fileID: 6965305925514130928}
|
||||
deleteButton: {fileID: 5102831562105438405}
|
||||
--- !u!1 &5617793748733146591
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -425,3 +535,232 @@ MonoBehaviour:
|
||||
m_FlexibleWidth: 1
|
||||
m_FlexibleHeight: -1
|
||||
m_LayoutPriority: 1
|
||||
--- !u!1 &5969634697112825658
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 3505386829136548087}
|
||||
- component: {fileID: 4500436679700087771}
|
||||
- component: {fileID: 5081311949443699076}
|
||||
m_Layer: 5
|
||||
m_Name: Image
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &3505386829136548087
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5969634697112825658}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 1544713289774970384}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &4500436679700087771
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5969634697112825658}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &5081311949443699076
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5969634697112825658}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 0, b: 0, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 21300002, guid: 88dca061d041a2a4596563e6a38f66c8, type: 3}
|
||||
m_Type: 0
|
||||
m_PreserveAspect: 1
|
||||
m_FillCenter: 1
|
||||
m_FillMethod: 4
|
||||
m_FillAmount: 1
|
||||
m_FillClockwise: 1
|
||||
m_FillOrigin: 0
|
||||
m_UseSpriteMesh: 0
|
||||
m_PixelsPerUnitMultiplier: 1
|
||||
--- !u!1 &7624557558758837467
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 1544713289774970384}
|
||||
- component: {fileID: 1437169994306891023}
|
||||
- component: {fileID: 4243156207255156076}
|
||||
- component: {fileID: 5102831562105438405}
|
||||
- component: {fileID: 7366748879155754635}
|
||||
m_Layer: 5
|
||||
m_Name: Delete
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &1544713289774970384
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7624557558758837467}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 3505386829136548087}
|
||||
m_Father: {fileID: 157686408557551303}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &1437169994306891023
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7624557558758837467}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &4243156207255156076
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7624557558758837467}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 0}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 0}
|
||||
m_Type: 1
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
m_FillMethod: 4
|
||||
m_FillAmount: 1
|
||||
m_FillClockwise: 1
|
||||
m_FillOrigin: 0
|
||||
m_UseSpriteMesh: 0
|
||||
m_PixelsPerUnitMultiplier: 1
|
||||
--- !u!114 &5102831562105438405
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7624557558758837467}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Button
|
||||
m_Navigation:
|
||||
m_Mode: 3
|
||||
m_WrapAround: 0
|
||||
m_SelectOnUp: {fileID: 0}
|
||||
m_SelectOnDown: {fileID: 0}
|
||||
m_SelectOnLeft: {fileID: 0}
|
||||
m_SelectOnRight: {fileID: 0}
|
||||
m_Transition: 1
|
||||
m_Colors:
|
||||
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
|
||||
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
|
||||
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
|
||||
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
|
||||
m_ColorMultiplier: 1
|
||||
m_FadeDuration: 0.1
|
||||
m_SpriteState:
|
||||
m_HighlightedSprite: {fileID: 0}
|
||||
m_PressedSprite: {fileID: 0}
|
||||
m_SelectedSprite: {fileID: 0}
|
||||
m_DisabledSprite: {fileID: 0}
|
||||
m_AnimationTriggers:
|
||||
m_NormalTrigger: Normal
|
||||
m_HighlightedTrigger: Highlighted
|
||||
m_PressedTrigger: Pressed
|
||||
m_SelectedTrigger: Selected
|
||||
m_DisabledTrigger: Disabled
|
||||
m_Interactable: 1
|
||||
m_TargetGraphic: {fileID: 4243156207255156076}
|
||||
m_OnClick:
|
||||
m_PersistentCalls:
|
||||
m_Calls:
|
||||
- m_Target: {fileID: 5166012670666351313}
|
||||
m_TargetAssemblyTypeName:
|
||||
m_MethodName:
|
||||
m_Mode: 1
|
||||
m_Arguments:
|
||||
m_ObjectArgument: {fileID: 0}
|
||||
m_ObjectArgumentAssemblyTypeName:
|
||||
m_IntArgument: 0
|
||||
m_FloatArgument: 0
|
||||
m_StringArgument:
|
||||
m_BoolArgument: 0
|
||||
m_CallState: 2
|
||||
--- !u!114 &7366748879155754635
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7624557558758837467}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
|
||||
m_IgnoreLayout: 0
|
||||
m_MinWidth: -1
|
||||
m_MinHeight: -1
|
||||
m_PreferredWidth: 40
|
||||
m_PreferredHeight: 40
|
||||
m_FlexibleWidth: -1
|
||||
m_FlexibleHeight: -1
|
||||
m_LayoutPriority: 1
|
||||
|
||||
BIN
Binary file not shown.
-117
@@ -1,117 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1fb1f6deb68f9475281c9f7c427a7024
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 1
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 0
|
||||
wrapV: 0
|
||||
wrapW: 0
|
||||
nPOTScale: 1
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 0
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 0
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 0
|
||||
swizzle: 50462976
|
||||
cookieLightType: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 4
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
customData:
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spriteCustomMetadata:
|
||||
entries: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
LFS
BIN
Binary file not shown.
BIN
Binary file not shown.
+1
-1
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8d524cd6fa08d47c99b2462c2686b8d0
|
||||
guid: 2bbeb692cb0204e0c965a4f08dfeffce
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
LFS
BIN
Binary file not shown.
+1
-1
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7bf9379c6210e47b88bbe9c2a359d54f
|
||||
guid: 498227fe7963d42e0b240ce5c37f3665
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1e69ea88b87d34a48bbb45a1fbad81fc
|
||||
guid: da40d75c7a6fb481d9d2cf483c71ae3a
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
-117
@@ -1,117 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d3ab99e02ad304404a2d663e3d77db5d
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 1
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 0
|
||||
wrapV: 0
|
||||
wrapW: 0
|
||||
nPOTScale: 1
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 0
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 0
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 0
|
||||
swizzle: 50462976
|
||||
cookieLightType: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 4
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
customData:
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spriteCustomMetadata:
|
||||
entries: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Eagle/OAuth Buttons/web_neutral_sq_ctn@4x.png
LFS
BIN
Binary file not shown.
-117
@@ -1,117 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1850a7332b6e84e1a9b69f5f5b4cf482
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 1
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 0
|
||||
wrapV: 0
|
||||
wrapW: 0
|
||||
nPOTScale: 1
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 0
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 0
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 0
|
||||
swizzle: 50462976
|
||||
cookieLightType: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 4
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
customData:
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spriteCustomMetadata:
|
||||
entries: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
LFS
BIN
Binary file not shown.
-117
@@ -1,117 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8bac7b3a2d18d452197ea3aa4e3e4855
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 1
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 0
|
||||
wrapV: 0
|
||||
wrapW: 0
|
||||
nPOTScale: 1
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 0
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 0
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 0
|
||||
swizzle: 50462976
|
||||
cookieLightType: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 4
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
customData:
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spriteCustomMetadata:
|
||||
entries: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
BIN
Binary file not shown.
-117
@@ -1,117 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b6b68c8c37d5cf1419267a80c426ac83
|
||||
TextureImporter:
|
||||
fileIDToRecycleName: {}
|
||||
externalObjects: {}
|
||||
serializedVersion: 5
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 0
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
isReadable: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: -1
|
||||
aniso: -1
|
||||
mipBias: -1
|
||||
wrapU: 1
|
||||
wrapV: 1
|
||||
wrapW: 1
|
||||
nPOTScale: 0
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 1
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 1
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 8
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 2
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 128
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
- serializedVersion: 2
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 128
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
- serializedVersion: 2
|
||||
buildTarget: Android
|
||||
maxTextureSize: 128
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
- serializedVersion: 2
|
||||
buildTarget: WebGL
|
||||
maxTextureSize: 128
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID: e8701ed65f69e7b4385fc7a2dcf1f924
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
spritePackingTag:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Binary file not shown.
+7
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e082b4a1a25a044a99b7798107149f77
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+1
-1
@@ -1160,7 +1160,7 @@ namespace Shardok {
|
||||
int currentMouseGridIndex) {}
|
||||
|
||||
public void HandlePrimaryClick(int clickedIndex) {
|
||||
if (HasPlacementAction(clickedIndex)) {
|
||||
if (_selectedGridIndex != clickedIndex && HasPlacementAction(clickedIndex)) {
|
||||
PerformPlacementAction(clickedIndex);
|
||||
_selectedGridIndex = null;
|
||||
} else if (HasReinforceAction(clickedIndex)) {
|
||||
|
||||
@@ -2057,6 +2057,21 @@ MonoBehaviour:
|
||||
m_Scale: 1
|
||||
m_AtlasIndex: 0
|
||||
m_ClassDefinitionType: 0
|
||||
- m_Index: 130
|
||||
m_Metrics:
|
||||
m_Width: 0
|
||||
m_Height: 0
|
||||
m_HorizontalBearingX: 0
|
||||
m_HorizontalBearingY: 0
|
||||
m_HorizontalAdvance: 54.015625
|
||||
m_GlyphRect:
|
||||
m_X: 0
|
||||
m_Y: 0
|
||||
m_Width: 0
|
||||
m_Height: 0
|
||||
m_Scale: 1
|
||||
m_AtlasIndex: 0
|
||||
m_ClassDefinitionType: 0
|
||||
m_CharacterTable:
|
||||
- m_ElementType: 1
|
||||
m_Unicode: 2
|
||||
@@ -2582,6 +2597,10 @@ MonoBehaviour:
|
||||
m_Unicode: 8230
|
||||
m_GlyphIndex: 389
|
||||
m_Scale: 1
|
||||
- m_ElementType: 1
|
||||
m_Unicode: 173
|
||||
m_GlyphIndex: 130
|
||||
m_Scale: 1
|
||||
m_AtlasTextures:
|
||||
- {fileID: -953588492498898259}
|
||||
m_AtlasTextureIndex: 0
|
||||
|
||||
+3
-84
File diff suppressed because one or more lines are too long
+46
-2
@@ -24,6 +24,9 @@ namespace Eagle0.Tutorial {
|
||||
|
||||
// Register tactical contextual tutorials
|
||||
RegisterTacticalTutorials(registry);
|
||||
|
||||
// Register game state guidance tutorials
|
||||
RegisterGuidanceTutorials(registry);
|
||||
}
|
||||
|
||||
// ========== ONBOARDING SEQUENCE ==========
|
||||
@@ -420,8 +423,8 @@ namespace Eagle0.Tutorial {
|
||||
var divine = CreateCommandPanelTutorial(
|
||||
"command_divine",
|
||||
"Divine Heroes",
|
||||
"Use divination to learn a hero's hidden potential.\n\n" +
|
||||
"Costs gold but reveals the hero's true abilities and stats. Divine all available heroes at once for efficiency.");
|
||||
"Pay a soothsayer to divine a hero's hidden desires.\n\n" +
|
||||
"Reveals the intentions and aspirations of a free hero, showing you what you must do to win them to your cause.");
|
||||
registry.RegisterTutorial(divine, "command_panel_DivineCommand");
|
||||
|
||||
// Issue Orders command
|
||||
@@ -682,6 +685,47 @@ namespace Eagle0.Tutorial {
|
||||
registry.RegisterTutorial(charge, "ability_charge_available");
|
||||
}
|
||||
|
||||
// ========== GAME STATE GUIDANCE TUTORIALS ==========
|
||||
// These trigger based on complex game state analysis, not simple first-encounters.
|
||||
|
||||
private static void RegisterGuidanceTutorials(TutorialTriggerRegistry registry) {
|
||||
// Loyalty danger warning (November)
|
||||
var loyaltyDanger = CreateSingleStepTutorial(
|
||||
"guidance_loyalty_danger",
|
||||
"Loyalty Warning!",
|
||||
"Some of your heroes are unhappy. At year's end, disloyal heroes may leave your service—or worse, betray you.\n\n" +
|
||||
"Use <b>Feast</b> to boost loyalty and vigor for all heroes in a province, or <b>Give Gift</b> to greatly increase one hero's loyalty.",
|
||||
TutorialDisplayMode.Modal);
|
||||
registry.RegisterTutorial(loyaltyDanger, "guidance_loyalty_danger");
|
||||
|
||||
// Neighbor danger warning
|
||||
var neighborDanger = CreateSingleStepTutorial(
|
||||
"guidance_neighbor_danger",
|
||||
"Border Alert!",
|
||||
"Your province borders hostile territory. An enemy army could march in at any time.\n\n" +
|
||||
"Consider <b>Organizing Troops</b> for defense, or use <b>Diplomacy</b> to seek a Truce or Alliance with your neighbor.",
|
||||
TutorialDisplayMode.Modal);
|
||||
registry.RegisterTutorial(neighborDanger, "guidance_neighbor_danger");
|
||||
|
||||
// Recruit heroes guidance
|
||||
var recruitHeroes = CreateSingleStepTutorial(
|
||||
"guidance_recruit_heroes",
|
||||
"Grow Your Court",
|
||||
"Free heroes are nearby, seeking a lord to serve. More heroes means more armies and more provinces you can manage.\n\n" +
|
||||
"Use <b>Travel</b> to visit town, then <b>Divine</b> to learn what a hero desires, or <b>Recruit</b> those already willing to join.",
|
||||
TutorialDisplayMode.Modal);
|
||||
registry.RegisterTutorial(recruitHeroes, "guidance_recruit_heroes");
|
||||
|
||||
// Expand guidance
|
||||
var expand = CreateSingleStepTutorial(
|
||||
"guidance_expand",
|
||||
"Time to Expand",
|
||||
"Your province is stable. To grow your realm, you'll need to claim new territory.\n\n" +
|
||||
"Use <b>March</b> to send your Warlord and another hero to a neighboring province. Once there, develop it just like your first province.",
|
||||
TutorialDisplayMode.Modal);
|
||||
registry.RegisterTutorial(expand, "guidance_expand");
|
||||
}
|
||||
|
||||
// ========== HELPER METHODS ==========
|
||||
|
||||
private static TutorialSequence CreateSingleStepTutorial(
|
||||
|
||||
+140
@@ -134,6 +134,9 @@ namespace Eagle0.Tutorial {
|
||||
|
||||
// Check for free heroes (recruitment opportunity)
|
||||
CheckHeroRecruitmentAvailable(model, previousModel);
|
||||
|
||||
// Check game state guidance triggers
|
||||
CheckGuidanceTriggers(model);
|
||||
}
|
||||
|
||||
private void CheckStrategicCommandsAvailable(IGameModel model, IGameModel previousModel) {
|
||||
@@ -173,6 +176,143 @@ namespace Eagle0.Tutorial {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Game State Guidance Triggers ==========
|
||||
|
||||
private void CheckGuidanceTriggers(IGameModel model) {
|
||||
CheckLoyaltyDanger(model);
|
||||
CheckNeighborDanger(model);
|
||||
CheckRecruitHeroesGuidance(model);
|
||||
CheckExpandGuidance(model);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if it's November and any heroes have low loyalty.
|
||||
/// </summary>
|
||||
private void CheckLoyaltyDanger(IGameModel model) {
|
||||
if (_manager.State.HasCompletedTutorial("guidance_loyalty_danger")) return;
|
||||
|
||||
// Only warn in November (month 11)
|
||||
if (model.CurrentDate?.Month != 11) return;
|
||||
|
||||
// Get the player's faction ID
|
||||
var playerId = model.PlayerId;
|
||||
if (playerId == null) return;
|
||||
|
||||
// Check all heroes belonging to the player's faction
|
||||
var hasLowLoyaltyHero = model.Heroes?.Values.Any(
|
||||
h => h.FactionId == playerId && h.Loyalty != null && h.Loyalty.Stat < 70);
|
||||
|
||||
if (hasLowLoyaltyHero == true) { OnGameEvent("guidance_loyalty_danger"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if player has provinces adjacent to hostile factions.
|
||||
/// </summary>
|
||||
private void CheckNeighborDanger(IGameModel model) {
|
||||
if (_manager.State.HasCompletedTutorial("guidance_neighbor_danger")) return;
|
||||
|
||||
var playerId = model.PlayerId;
|
||||
if (playerId == null) return;
|
||||
|
||||
// Get player's faction relationships
|
||||
if (!model.ActiveFactions.TryGetValue(playerId.Value, out var playerFaction)) return;
|
||||
|
||||
// Check each province the player owns
|
||||
foreach (var province in model.Provinces.Values) {
|
||||
if (province.RulingFactionId != playerId) continue;
|
||||
|
||||
// Check available march destinations for hostile neighbors
|
||||
if (model.AvailableCommandsByProvince == null) continue;
|
||||
if (!model.AvailableCommandsByProvince.TryGetValue(province.Id, out var commands))
|
||||
continue;
|
||||
|
||||
var marchCommand = commands.Commands.FirstOrDefault(c => c.MarchCommand != null);
|
||||
if (marchCommand?.MarchCommand == null) continue;
|
||||
|
||||
// Check all one-province march commands for adjacent hostile provinces
|
||||
foreach (var oneProvinceCmd in marchCommand.MarchCommand.OneProvinceCommands) {
|
||||
foreach (var dest in oneProvinceCmd.AvailableDestinationProvinces) {
|
||||
var destProvince = model.Provinces[dest.ProvinceId];
|
||||
if (destProvince.RulingFactionId == null ||
|
||||
destProvince.RulingFactionId == playerId)
|
||||
continue;
|
||||
|
||||
// Check if we have a truce or alliance with this faction
|
||||
var relationship = playerFaction.FactionRelationships.FirstOrDefault(
|
||||
fr => fr.TargetFactionId == destProvince.RulingFactionId);
|
||||
|
||||
if (relationship == null ||
|
||||
relationship.RelationshipLevel ==
|
||||
Net.Eagle0.Eagle.Views.FactionRelationshipView.Types
|
||||
.RelationshipLevel.Hostile) {
|
||||
// Found a hostile neighbor!
|
||||
OnGameEvent("guidance_neighbor_danger");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if player should be recruiting heroes.
|
||||
/// </summary>
|
||||
private void CheckRecruitHeroesGuidance(IGameModel model) {
|
||||
if (_manager.State.HasCompletedTutorial("guidance_recruit_heroes")) return;
|
||||
|
||||
var playerId = model.PlayerId;
|
||||
if (playerId == null) return;
|
||||
|
||||
// Check for a province with support >= 40 that has free heroes
|
||||
foreach (var province in model.Provinces.Values) {
|
||||
if (province.RulingFactionId != playerId) continue;
|
||||
if (province.FullInfo?.Support?.Stat < 40) continue;
|
||||
|
||||
// Need at least 100 gold in the province to divine
|
||||
if (province.FullInfo?.Gold < 100) continue;
|
||||
|
||||
// Check for free heroes in this province
|
||||
var freeHeroes = province.FullInfo?.UnaffiliatedHeroes;
|
||||
if (freeHeroes == null || freeHeroes.Count == 0) continue;
|
||||
|
||||
// Check if any are divineable or ready to join
|
||||
var recruitableHeroes = freeHeroes.Where(
|
||||
uh => uh.RecruitmentInfo?.Status ==
|
||||
Net.Eagle0.Eagle.Common.RecruitmentStatus.WouldJoin ||
|
||||
uh.RecruitmentInfo?.Status ==
|
||||
Net.Eagle0.Eagle.Common.RecruitmentStatus.NotDivined);
|
||||
|
||||
if (recruitableHeroes.Any()) {
|
||||
OnGameEvent("guidance_recruit_heroes");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if player should consider expanding to new provinces.
|
||||
/// </summary>
|
||||
private void CheckExpandGuidance(IGameModel model) {
|
||||
if (_manager.State.HasCompletedTutorial("guidance_expand")) return;
|
||||
|
||||
var playerId = model.PlayerId;
|
||||
if (playerId == null) return;
|
||||
|
||||
// Check if player has any province with support >= 40
|
||||
var stableProvince = model.Provinces.Values.FirstOrDefault(
|
||||
p => p.RulingFactionId == playerId && p.FullInfo?.Support?.Stat >= 40);
|
||||
|
||||
if (stableProvince == null) return;
|
||||
|
||||
// Check if player only has one province (hasn't expanded yet)
|
||||
var playerProvinceCount =
|
||||
model.Provinces.Values.Count(p => p.RulingFactionId == playerId);
|
||||
if (playerProvinceCount > 1) return;
|
||||
|
||||
// Player has one stable province but hasn't expanded - suggest expansion
|
||||
OnGameEvent("guidance_expand");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when a province is selected.
|
||||
/// </summary>
|
||||
|
||||
+2
@@ -26,6 +26,8 @@ namespace Eagle0.Tutorial {
|
||||
[Header("Heroes Panel")]
|
||||
[Tooltip("The Resident Heroes panel")]
|
||||
public RectTransform HeroesPanel;
|
||||
// Note: WarlordRow, VassalRow1, VassalRow2 are registered dynamically
|
||||
// by HeroesAndBattalionsPanelController when hero rows are created
|
||||
|
||||
[Header("Command Panel")]
|
||||
[Tooltip("The command selector panel (parent of individual command selectors)")]
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7240f78031ac24c57a64dd5911bb6b51
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f30ffc2982a6b4c98a935d17858d81f5
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+1
@@ -0,0 +1 @@
|
||||
@import url("unity-theme://default");
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 21a93c7d741154ceab05bac2703c07f9
|
||||
ScriptedImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
script: {fileID: 12388, guid: 0000000000000000e000000000000000, type: 0}
|
||||
disableValidation: 0
|
||||
unsupportedSelectorAction: 0
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e345257eb90ab41f7b4719389942adc7
|
||||
@@ -2299,10 +2299,6 @@ func handleLoginPage(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func handleLoginStart(w http.ResponseWriter, r *http.Request) {
|
||||
provider := r.URL.Query().Get("provider")
|
||||
if provider != "discord" && provider != "google" {
|
||||
http.Error(w, "Invalid provider", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
@@ -2313,6 +2309,17 @@ func handleLoginStart(w http.ResponseWriter, r *http.Request) {
|
||||
pbProvider = authpb.OAuthProvider_OAUTH_PROVIDER_DISCORD
|
||||
case "google":
|
||||
pbProvider = authpb.OAuthProvider_OAUTH_PROVIDER_GOOGLE
|
||||
case "github":
|
||||
pbProvider = authpb.OAuthProvider_OAUTH_PROVIDER_GITHUB
|
||||
case "apple":
|
||||
pbProvider = authpb.OAuthProvider_OAUTH_PROVIDER_APPLE
|
||||
case "steam":
|
||||
pbProvider = authpb.OAuthProvider_OAUTH_PROVIDER_STEAM
|
||||
case "twitch":
|
||||
pbProvider = authpb.OAuthProvider_OAUTH_PROVIDER_TWITCH
|
||||
default:
|
||||
http.Error(w, "Invalid provider", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Build the return URL for the OAuth callback to redirect back to admin console
|
||||
|
||||
@@ -26,6 +26,30 @@
|
||||
</svg>
|
||||
Sign in with Google
|
||||
</a>
|
||||
<a href="/login/start?provider=github" class="login-btn github">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 0C5.374 0 0 5.373 0 12c0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23A11.509 11.509 0 0112 5.803c1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576C20.566 21.797 24 17.3 24 12c0-6.627-5.373-12-12-12z"/>
|
||||
</svg>
|
||||
Sign in with GitHub
|
||||
</a>
|
||||
<a href="/login/start?provider=apple" class="login-btn apple">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17.05 20.28c-.98.95-2.05.8-3.08.35-1.09-.46-2.09-.48-3.24 0-1.44.62-2.2.44-3.06-.35C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09l.01-.01zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z"/>
|
||||
</svg>
|
||||
Sign in with Apple
|
||||
</a>
|
||||
<a href="/login/start?provider=steam" class="login-btn steam">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.979 0C5.678 0 .511 4.86.022 11.037l6.432 2.658c.545-.371 1.203-.59 1.912-.59.063 0 .125.004.188.006l2.861-4.142V8.91c0-2.495 2.028-4.524 4.524-4.524 2.494 0 4.524 2.031 4.524 4.527s-2.03 4.525-4.524 4.525h-.105l-4.076 2.911c0 .052.004.105.004.159 0 1.875-1.515 3.396-3.39 3.396-1.635 0-3.016-1.173-3.331-2.727L.436 15.27C1.862 20.307 6.486 24 11.979 24c6.627 0 11.999-5.373 11.999-12S18.605 0 11.979 0zM7.54 18.21l-1.473-.61c.262.543.714.999 1.314 1.25 1.297.539 2.793-.076 3.332-1.375.263-.63.264-1.319.005-1.949s-.75-1.121-1.377-1.383c-.624-.26-1.29-.249-1.878-.03l1.523.63c.956.4 1.409 1.5 1.009 2.455-.397.957-1.497 1.41-2.454 1.012H7.54zm11.415-9.303c0-1.662-1.353-3.015-3.015-3.015-1.665 0-3.015 1.353-3.015 3.015 0 1.665 1.35 3.015 3.015 3.015 1.663 0 3.015-1.35 3.015-3.015zm-5.273-.005c0-1.252 1.013-2.266 2.265-2.266 1.249 0 2.266 1.014 2.266 2.266 0 1.251-1.017 2.265-2.266 2.265-1.253 0-2.265-1.014-2.265-2.265z"/>
|
||||
</svg>
|
||||
Sign in with Steam
|
||||
</a>
|
||||
<a href="/login/start?provider=twitch" class="login-btn twitch">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.571 4.714h1.715v5.143H11.57zm4.715 0H18v5.143h-1.714zM6 0L1.714 4.286v15.428h5.143V24l4.286-4.286h3.428L22.286 12V0zm14.571 11.143l-3.428 3.428h-3.429l-3 3v-3H6.857V1.714h13.714Z"/>
|
||||
</svg>
|
||||
Sign in with Twitch
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
@@ -115,6 +139,42 @@
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
|
||||
.login-btn.github {
|
||||
background-color: #24292e;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.login-btn.github:hover {
|
||||
background-color: #1b1f23;
|
||||
}
|
||||
|
||||
.login-btn.apple {
|
||||
background-color: #000000;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.login-btn.apple:hover {
|
||||
background-color: #1a1a1a;
|
||||
}
|
||||
|
||||
.login-btn.steam {
|
||||
background-color: #171a21;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.login-btn.steam:hover {
|
||||
background-color: #1b2838;
|
||||
}
|
||||
|
||||
.login-btn.twitch {
|
||||
background-color: #9146FF;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.login-btn.twitch:hover {
|
||||
background-color: #7d3bdb;
|
||||
}
|
||||
|
||||
.login-card footer {
|
||||
margin-top: 1.5rem;
|
||||
text-align: center;
|
||||
|
||||
@@ -12,6 +12,7 @@ go_library(
|
||||
"main.go",
|
||||
"oauth.go",
|
||||
"sendgrid.go",
|
||||
"steam_auth.go",
|
||||
"users.go",
|
||||
],
|
||||
importpath = "github.com/nolen777/eagle0/src/main/go/net/eagle0/authservice",
|
||||
|
||||
@@ -27,13 +27,38 @@ type AppleTokenResponse struct {
|
||||
IDToken string `json:"id_token"`
|
||||
}
|
||||
|
||||
// AppleBool handles Apple's inconsistent boolean fields that may be
|
||||
// either a JSON boolean (true/false) or a string ("true"/"false")
|
||||
type AppleBool bool
|
||||
|
||||
func (b *AppleBool) UnmarshalJSON(data []byte) error {
|
||||
// Try boolean first
|
||||
var boolVal bool
|
||||
if err := json.Unmarshal(data, &boolVal); err == nil {
|
||||
*b = AppleBool(boolVal)
|
||||
return nil
|
||||
}
|
||||
// Try string
|
||||
var strVal string
|
||||
if err := json.Unmarshal(data, &strVal); err == nil {
|
||||
*b = AppleBool(strVal == "true")
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("cannot unmarshal %s into AppleBool", string(data))
|
||||
}
|
||||
|
||||
// AppleIDTokenClaims represents the claims in Apple's id_token
|
||||
// See: https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api/authenticating_users_with_sign_in_with_apple
|
||||
type AppleIDTokenClaims struct {
|
||||
jwt.RegisteredClaims
|
||||
Email string `json:"email,omitempty"`
|
||||
EmailVerified string `json:"email_verified,omitempty"`
|
||||
IsPrivateEmail string `json:"is_private_email,omitempty"`
|
||||
RealUserStatus int `json:"real_user_status,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
EmailVerified AppleBool `json:"email_verified,omitempty"`
|
||||
IsPrivateEmail AppleBool `json:"is_private_email,omitempty"`
|
||||
RealUserStatus int `json:"real_user_status,omitempty"`
|
||||
Nonce string `json:"nonce,omitempty"`
|
||||
NonceSupported bool `json:"nonce_supported,omitempty"`
|
||||
CHash string `json:"c_hash,omitempty"`
|
||||
AuthTime int64 `json:"auth_time,omitempty"`
|
||||
}
|
||||
|
||||
// GenerateAppleClientSecret generates a JWT client_secret for Apple Sign-In
|
||||
@@ -48,14 +73,19 @@ func GenerateAppleClientSecret() (string, error) {
|
||||
}
|
||||
|
||||
// Parse the private key
|
||||
// Key may be: raw PEM, or base64-encoded PEM
|
||||
block, _ := pem.Decode([]byte(privateKeyPEM))
|
||||
if block == nil {
|
||||
// Try base64 decoding if not PEM format
|
||||
// Try base64 decoding (key might be base64-encoded PEM)
|
||||
keyBytes, err := base64.StdEncoding.DecodeString(privateKeyPEM)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to decode private key: %v", err)
|
||||
}
|
||||
block = &pem.Block{Type: "PRIVATE KEY", Bytes: keyBytes}
|
||||
// Now PEM decode the base64-decoded content
|
||||
block, _ = pem.Decode(keyBytes)
|
||||
if block == nil {
|
||||
return "", fmt.Errorf("failed to PEM decode private key after base64 decode")
|
||||
}
|
||||
}
|
||||
|
||||
privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||
@@ -112,8 +142,6 @@ func (s *OAuthService) ExchangeAppleCode(config OAuthProviderConfig, code string
|
||||
data.Set("code", code)
|
||||
data.Set("redirect_uri", callbackURL)
|
||||
|
||||
log.Printf("Apple token exchange: client_id=%s redirect_uri=%s", config.ClientID, callbackURL)
|
||||
|
||||
req, err := http.NewRequest("POST", config.TokenEndpoint, strings.NewReader(data.Encode()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -259,6 +287,7 @@ func (s *OAuthService) HandleAppleCallback(w http.ResponseWriter, r *http.Reques
|
||||
// Parse user info from id_token
|
||||
userInfo, err := ParseAppleIDToken(tokenResp.IDToken)
|
||||
if err != nil {
|
||||
log.Printf("Apple parse user info failed: %v", err)
|
||||
s.completeWithError(state, err.Error())
|
||||
http.Error(w, "Failed to parse user info", http.StatusInternalServerError)
|
||||
return
|
||||
|
||||
@@ -335,6 +335,10 @@ func providerToString(p auth.OAuthProvider) string {
|
||||
return "github"
|
||||
case auth.OAuthProvider_OAUTH_PROVIDER_APPLE:
|
||||
return "apple"
|
||||
case auth.OAuthProvider_OAUTH_PROVIDER_STEAM:
|
||||
return "steam"
|
||||
case auth.OAuthProvider_OAUTH_PROVIDER_TWITCH:
|
||||
return "twitch"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
@@ -350,6 +354,10 @@ func stringToProvider(s string) auth.OAuthProvider {
|
||||
return auth.OAuthProvider_OAUTH_PROVIDER_GITHUB
|
||||
case "apple":
|
||||
return auth.OAuthProvider_OAUTH_PROVIDER_APPLE
|
||||
case "steam":
|
||||
return auth.OAuthProvider_OAUTH_PROVIDER_STEAM
|
||||
case "twitch":
|
||||
return auth.OAuthProvider_OAUTH_PROVIDER_TWITCH
|
||||
default:
|
||||
return auth.OAuthProvider_OAUTH_PROVIDER_UNSPECIFIED
|
||||
}
|
||||
|
||||
@@ -165,10 +165,14 @@ func (h *InvitationHTTPHandler) handleLandingPage(w http.ResponseWriter, r *http
|
||||
GoogleAuthURL string
|
||||
GitHubAuthURL string
|
||||
AppleAuthURL string
|
||||
SteamAuthURL string
|
||||
TwitchAuthURL string
|
||||
ShowDiscord bool
|
||||
ShowGoogle bool
|
||||
ShowGitHub bool
|
||||
ShowApple bool
|
||||
ShowSteam bool
|
||||
ShowTwitch bool
|
||||
}{
|
||||
Code: code,
|
||||
Valid: valid,
|
||||
@@ -185,6 +189,8 @@ func (h *InvitationHTTPHandler) handleLandingPage(w http.ResponseWriter, r *http
|
||||
data.GoogleAuthURL = fmt.Sprintf("/invite/%s/auth/google", code)
|
||||
data.GitHubAuthURL = fmt.Sprintf("/invite/%s/auth/github", code)
|
||||
data.AppleAuthURL = fmt.Sprintf("/invite/%s/auth/apple", code)
|
||||
data.SteamAuthURL = fmt.Sprintf("/invite/%s/auth/steam", code)
|
||||
data.TwitchAuthURL = fmt.Sprintf("/invite/%s/auth/twitch", code)
|
||||
|
||||
// Check which providers are configured
|
||||
if _, _, err := h.oauthService.GetAuthURL("discord", "", ""); err == nil {
|
||||
@@ -199,6 +205,12 @@ func (h *InvitationHTTPHandler) handleLandingPage(w http.ResponseWriter, r *http
|
||||
if _, _, err := h.oauthService.GetAuthURL("apple", "", ""); err == nil {
|
||||
data.ShowApple = true
|
||||
}
|
||||
if _, _, err := h.oauthService.GetAuthURL("steam", "", ""); err == nil {
|
||||
data.ShowSteam = true
|
||||
}
|
||||
if _, _, err := h.oauthService.GetAuthURL("twitch", "", ""); err == nil {
|
||||
data.ShowTwitch = true
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
@@ -765,6 +777,14 @@ var landingPageTemplate = template.Must(template.New("landing").Parse(`<!DOCTYPE
|
||||
background: black;
|
||||
color: white;
|
||||
}
|
||||
.oauth-steam {
|
||||
background: #171a21;
|
||||
color: white;
|
||||
}
|
||||
.oauth-twitch {
|
||||
background: #9146FF;
|
||||
color: white;
|
||||
}
|
||||
.expires {
|
||||
color: #856404;
|
||||
background: #fff3cd;
|
||||
@@ -845,6 +865,22 @@ var landingPageTemplate = template.Must(template.New("landing").Parse(`<!DOCTYPE
|
||||
Continue with Apple
|
||||
</a>
|
||||
{{end}}
|
||||
{{if .ShowSteam}}
|
||||
<a href="{{.SteamAuthURL}}" class="oauth-button oauth-steam">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M11.979 0C5.678 0 .511 4.86.022 11.037l6.432 2.658c.545-.371 1.203-.59 1.912-.59.063 0 .125.004.188.006l2.861-4.142V8.91c0-2.495 2.028-4.524 4.524-4.524 2.494 0 4.524 2.031 4.524 4.527s-2.03 4.525-4.524 4.525h-.105l-4.076 2.911c0 .052.004.105.004.159 0 1.875-1.515 3.396-3.39 3.396-1.635 0-3.016-1.173-3.331-2.727L.436 15.27C1.862 20.307 6.486 24 11.979 24c6.627 0 11.999-5.373 11.999-12S18.605 0 11.979 0zM7.54 18.21l-1.473-.61c.262.543.714.999 1.314 1.25 1.297.539 2.793-.076 3.332-1.375.263-.63.264-1.319.005-1.949s-.75-1.121-1.377-1.383c-.624-.26-1.29-.249-1.878-.03l1.523.63c.956.4 1.409 1.5 1.009 2.455-.397.957-1.497 1.41-2.454 1.012H7.54zm11.415-9.303c0-1.662-1.353-3.015-3.015-3.015-1.665 0-3.015 1.353-3.015 3.015 0 1.665 1.35 3.015 3.015 3.015 1.663 0 3.015-1.35 3.015-3.015zm-5.273-.005c0-1.252 1.013-2.266 2.265-2.266 1.249 0 2.266 1.014 2.266 2.266 0 1.251-1.017 2.265-2.266 2.265-1.253 0-2.265-1.014-2.265-2.265z"/>
|
||||
</svg>
|
||||
Continue with Steam
|
||||
</a>
|
||||
{{end}}
|
||||
{{if .ShowTwitch}}
|
||||
<a href="{{.TwitchAuthURL}}" class="oauth-button oauth-twitch">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M11.571 4.714h1.715v5.143H11.57zm4.715 0H18v5.143h-1.714zM6 0L1.714 4.286v15.428h5.143V24l4.286-4.286h3.428L22.286 12V0zm14.571 11.143l-3.428 3.428h-3.429l-3 3v-3H6.857V1.714h13.714Z"/>
|
||||
</svg>
|
||||
Continue with Twitch
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<div class="instructions">
|
||||
|
||||
@@ -147,6 +147,7 @@ func main() {
|
||||
// Start HTTP server for OAuth callbacks and invitation pages
|
||||
http.HandleFunc("/oauth/callback", oauthSvc.HandleCallback)
|
||||
http.HandleFunc("/oauth/apple/callback", oauthSvc.HandleAppleCallback) // Apple uses POST
|
||||
http.HandleFunc("/oauth/steam/callback", oauthSvc.HandleSteamCallback) // Steam uses OpenID 2.0
|
||||
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprintf(w, "OK")
|
||||
@@ -198,5 +199,24 @@ func getOAuthConfigs() map[string]OAuthProviderConfig {
|
||||
Scopes: []string{"name", "email"},
|
||||
CallbackPath: "/oauth/apple/callback", // Apple uses POST callback
|
||||
},
|
||||
"steam": {
|
||||
// Steam uses OpenID 2.0, not OAuth 2.0
|
||||
// No ClientID/ClientSecret needed - handled specially in GetAuthURL
|
||||
ClientID: "steam", // Marker to indicate Steam is "configured"
|
||||
ClientSecret: "steam", // Marker (not actually used)
|
||||
AuthorizationEndpoint: "https://steamcommunity.com/openid/login",
|
||||
TokenEndpoint: "", // Not used for OpenID 2.0
|
||||
UserInfoEndpoint: "", // Not used for OpenID 2.0
|
||||
Scopes: []string{},
|
||||
CallbackPath: "/oauth/steam/callback",
|
||||
},
|
||||
"twitch": {
|
||||
ClientID: os.Getenv("TWITCH_CLIENT_ID"),
|
||||
ClientSecret: os.Getenv("TWITCH_CLIENT_SECRET"),
|
||||
AuthorizationEndpoint: "https://id.twitch.tv/oauth2/authorize",
|
||||
TokenEndpoint: "https://id.twitch.tv/oauth2/token",
|
||||
UserInfoEndpoint: "https://api.twitch.tv/helix/users",
|
||||
Scopes: []string{"user:read:email"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,8 +106,8 @@ func (s *OAuthService) GetAuthURL(provider string, returnURL string, invitationC
|
||||
if config.ClientID == "" {
|
||||
return "", "", fmt.Errorf("provider %s not configured (missing client ID)", provider)
|
||||
}
|
||||
// Apple generates client_secret dynamically, so we don't check it here
|
||||
if provider != "apple" && config.ClientSecret == "" {
|
||||
// Apple generates client_secret dynamically, Steam uses OpenID 2.0 (no secret needed)
|
||||
if provider != "apple" && provider != "steam" && config.ClientSecret == "" {
|
||||
return "", "", fmt.Errorf("provider %s not configured (missing client secret)", provider)
|
||||
}
|
||||
|
||||
@@ -121,6 +121,19 @@ func (s *OAuthService) GetAuthURL(provider string, returnURL string, invitationC
|
||||
|
||||
log.Printf("OAuth: created state=%s for provider=%s returnURL=%s hasInvitationCode=%v", state, provider, returnURL, invitationCode != "")
|
||||
|
||||
// Steam uses OpenID 2.0, not OAuth 2.0
|
||||
if provider == "steam" {
|
||||
// Get base URL for realm
|
||||
baseURL := s.callbackURL
|
||||
if idx := strings.LastIndex(baseURL, "/oauth/callback"); idx > 0 {
|
||||
baseURL = baseURL[:idx]
|
||||
}
|
||||
callbackURL := baseURL + "/oauth/steam/callback"
|
||||
steamAuth := NewSteamOpenID(baseURL, callbackURL)
|
||||
authURL = steamAuth.GetAuthURL(state)
|
||||
return authURL, state, nil
|
||||
}
|
||||
|
||||
// Use custom callback path if configured, otherwise use default
|
||||
callbackURL := s.callbackURL
|
||||
if config.CallbackPath != "" {
|
||||
@@ -325,6 +338,11 @@ func (s *OAuthService) fetchUserInfo(provider string, config OAuthProviderConfig
|
||||
req.Header.Set("Authorization", "Bearer "+accessToken)
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
// Twitch requires Client-ID header in addition to Bearer token
|
||||
if provider == "twitch" {
|
||||
req.Header.Set("Client-Id", config.ClientID)
|
||||
}
|
||||
|
||||
resp, err := s.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -461,6 +479,29 @@ func (s *OAuthService) parseUserInfo(provider string, data []byte) (*ProviderUse
|
||||
AvatarURL: avatarURL,
|
||||
}, nil
|
||||
|
||||
case "twitch":
|
||||
// Twitch returns {data: [{id, login, display_name, email, profile_image_url}]}
|
||||
dataArr, ok := result["data"].([]interface{})
|
||||
if !ok || len(dataArr) == 0 {
|
||||
return nil, fmt.Errorf("invalid Twitch response: missing data array")
|
||||
}
|
||||
user, ok := dataArr[0].(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid Twitch response: invalid user data")
|
||||
}
|
||||
|
||||
id, _ := user["id"].(string)
|
||||
displayName, _ := user["display_name"].(string)
|
||||
email, _ := user["email"].(string)
|
||||
profileImageURL, _ := user["profile_image_url"].(string)
|
||||
|
||||
return &ProviderUserInfo{
|
||||
ID: id,
|
||||
Username: displayName,
|
||||
Email: email,
|
||||
AvatarURL: profileImageURL,
|
||||
}, nil
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported provider: %s", provider)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SteamOpenID handles Steam's OpenID 2.0 authentication
|
||||
// Steam uses OpenID 2.0, not OAuth 2.0, so it requires different handling
|
||||
type SteamOpenID struct {
|
||||
realm string // Our server URL (e.g., https://auth.eagle0.net)
|
||||
callbackURL string // Where Steam redirects back
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
const (
|
||||
steamOpenIDURL = "https://steamcommunity.com/openid/login"
|
||||
// Steam ID pattern in claimed_id: https://steamcommunity.com/openid/id/76561198012345678
|
||||
steamIDPattern = `^https://steamcommunity\.com/openid/id/(\d+)$`
|
||||
)
|
||||
|
||||
var steamIDRegex = regexp.MustCompile(steamIDPattern)
|
||||
|
||||
// NewSteamOpenID creates a new Steam OpenID handler
|
||||
func NewSteamOpenID(realm, callbackURL string) *SteamOpenID {
|
||||
return &SteamOpenID{
|
||||
realm: realm,
|
||||
callbackURL: callbackURL,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GetAuthURL generates the Steam login URL for OpenID 2.0
|
||||
func (s *SteamOpenID) GetAuthURL(state string) string {
|
||||
params := url.Values{}
|
||||
params.Set("openid.ns", "http://specs.openid.net/auth/2.0")
|
||||
params.Set("openid.mode", "checkid_setup")
|
||||
params.Set("openid.return_to", s.callbackURL+"?state="+state)
|
||||
params.Set("openid.realm", s.realm)
|
||||
params.Set("openid.identity", "http://specs.openid.net/auth/2.0/identifier_select")
|
||||
params.Set("openid.claimed_id", "http://specs.openid.net/auth/2.0/identifier_select")
|
||||
|
||||
return steamOpenIDURL + "?" + params.Encode()
|
||||
}
|
||||
|
||||
// HandleCallback validates the OpenID response and extracts Steam ID
|
||||
// Returns the Steam ID (e.g., "76561198012345678") on success
|
||||
func (s *SteamOpenID) HandleCallback(r *http.Request) (steamID string, err error) {
|
||||
// Get OpenID parameters from the callback
|
||||
query := r.URL.Query()
|
||||
|
||||
// Check for errors
|
||||
if mode := query.Get("openid.mode"); mode == "error" || mode == "cancel" {
|
||||
return "", fmt.Errorf("Steam login was cancelled or failed")
|
||||
}
|
||||
|
||||
// Validate the response signature with Steam
|
||||
if err := s.ValidateResponse(query); err != nil {
|
||||
return "", fmt.Errorf("Steam response validation failed: %v", err)
|
||||
}
|
||||
|
||||
// Extract Steam ID from claimed_id
|
||||
claimedID := query.Get("openid.claimed_id")
|
||||
matches := steamIDRegex.FindStringSubmatch(claimedID)
|
||||
if len(matches) != 2 {
|
||||
return "", fmt.Errorf("invalid Steam claimed_id format: %s", claimedID)
|
||||
}
|
||||
|
||||
return matches[1], nil
|
||||
}
|
||||
|
||||
// ValidateResponse verifies the OpenID response signature with Steam
|
||||
func (s *SteamOpenID) ValidateResponse(params url.Values) error {
|
||||
// Create validation request with same params but mode=check_authentication
|
||||
validationParams := url.Values{}
|
||||
for key, values := range params {
|
||||
if len(values) > 0 {
|
||||
validationParams.Set(key, values[0])
|
||||
}
|
||||
}
|
||||
validationParams.Set("openid.mode", "check_authentication")
|
||||
|
||||
// POST to Steam to verify
|
||||
req, err := http.NewRequest("POST", steamOpenIDURL, strings.NewReader(validationParams.Encode()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
resp, err := s.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Steam returns key-value pairs, one per line
|
||||
// We're looking for "is_valid:true"
|
||||
responseStr := string(body)
|
||||
if !strings.Contains(responseStr, "is_valid:true") {
|
||||
return fmt.Errorf("Steam validation failed: %s", responseStr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleSteamCallback handles the Steam OpenID callback for OAuthService
|
||||
func (s *OAuthService) HandleSteamCallback(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("Steam callback received")
|
||||
|
||||
// Get state parameter
|
||||
state := r.URL.Query().Get("state")
|
||||
if state == "" {
|
||||
log.Printf("Steam callback: missing state parameter")
|
||||
http.Error(w, "Missing state parameter", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Get pending OAuth info
|
||||
stateData, ok := s.pendingOAuth.Load(state)
|
||||
if !ok {
|
||||
log.Printf("Steam callback: state not found: %s", state)
|
||||
http.Error(w, "Invalid or expired state", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
oauthState := stateData.(OAuthState)
|
||||
if time.Since(oauthState.CreatedAt) >= stateExpiration {
|
||||
s.pendingOAuth.Delete(state)
|
||||
s.completeWithError(state, "OAuth session expired")
|
||||
http.Error(w, "OAuth session expired", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Create Steam OpenID handler
|
||||
realm := getServerBaseURL()
|
||||
callbackURL := realm + "/oauth/steam/callback"
|
||||
steamAuth := NewSteamOpenID(realm, callbackURL)
|
||||
|
||||
// Handle the callback
|
||||
steamID, err := steamAuth.HandleCallback(r)
|
||||
if err != nil {
|
||||
log.Printf("Steam callback failed: %v", err)
|
||||
s.completeWithError(state, err.Error())
|
||||
http.Error(w, "Steam login failed: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Steam callback: success, steamID=%s", steamID)
|
||||
|
||||
// Create user info from Steam ID
|
||||
// Note: Steam OpenID only provides the Steam ID, no email or username
|
||||
userInfo := &ProviderUserInfo{
|
||||
ID: steamID,
|
||||
Email: "", // Steam doesn't provide email via OpenID
|
||||
Username: "", // Steam doesn't provide username via OpenID
|
||||
AvatarURL: "", // Steam doesn't provide avatar via OpenID
|
||||
}
|
||||
|
||||
// Mark as completed
|
||||
s.pendingOAuth.Delete(state)
|
||||
s.completedOAuth.Store(state, OAuthResult{
|
||||
Success: true,
|
||||
UserInfo: userInfo,
|
||||
Provider: "steam",
|
||||
InvitationCode: oauthState.InvitationCode,
|
||||
})
|
||||
|
||||
// Redirect or show close message
|
||||
if oauthState.ReturnURL != "" {
|
||||
redirectURL := oauthState.ReturnURL
|
||||
if strings.Contains(redirectURL, "?") {
|
||||
redirectURL += "&state=" + state
|
||||
} else {
|
||||
redirectURL += "?state=" + state
|
||||
}
|
||||
http.Redirect(w, r, redirectURL, http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
fmt.Fprintf(w, `<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>Login Successful</title></head>
|
||||
<body>
|
||||
<h1>Login Successful!</h1>
|
||||
<p>You can close this window and return to the game.</p>
|
||||
<script>window.close();</script>
|
||||
</body>
|
||||
</html>`)
|
||||
}
|
||||
|
||||
// getServerBaseURL returns the server base URL from environment
|
||||
func getServerBaseURL() string {
|
||||
if baseURL := os.Getenv("SERVER_BASE_URL"); baseURL != "" {
|
||||
return baseURL
|
||||
}
|
||||
return "http://localhost:8080"
|
||||
}
|
||||
@@ -41,6 +41,8 @@ enum OAuthProvider {
|
||||
OAUTH_PROVIDER_GOOGLE = 2;
|
||||
OAUTH_PROVIDER_GITHUB = 3;
|
||||
OAUTH_PROVIDER_APPLE = 4;
|
||||
OAUTH_PROVIDER_STEAM = 5;
|
||||
OAUTH_PROVIDER_TWITCH = 6;
|
||||
}
|
||||
|
||||
// Request to initiate OAuth flow
|
||||
|
||||
@@ -68,20 +68,6 @@ scala_binary(
|
||||
# ],
|
||||
#)
|
||||
|
||||
#scala_binary(
|
||||
# name = "saved_game_utils",
|
||||
# srcs = ["SavedGameUtils.scala"],
|
||||
# main_class = "net.eagle0.eagle.SavedGameUtils",
|
||||
# resources = [],
|
||||
# visibility = ["//visibility:public"],
|
||||
# deps = [
|
||||
# "//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
# "//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
# "//src/main/scala/net/eagle0/eagle/service:persisted_history",
|
||||
# "//src/main/scala/net/eagle0/eagle/service/persistence:local_file_persister",
|
||||
# ],
|
||||
#)
|
||||
|
||||
scala_library(
|
||||
name = "eagle_pkg",
|
||||
srcs = ["package.scala"],
|
||||
|
||||
@@ -9,7 +9,7 @@ import io.sentry.Sentry
|
||||
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, OAuthService, OAuthServiceImpl, UserService, UserServiceImpl}
|
||||
import net.eagle0.eagle.auth.{JwtService, OAuthService, OAuthServiceImpl, UserService, UserServiceImpl}
|
||||
import net.eagle0.eagle.service.*
|
||||
import net.eagle0.eagle.service.persistence.{AsyncS3Persister, LocalFilePersister, SaveDirectory}
|
||||
|
||||
@@ -200,8 +200,8 @@ object Main {
|
||||
): Server = {
|
||||
val serverBuilder = ServerBuilder.forPort(grpcPort)
|
||||
|
||||
// Initialize JWT service
|
||||
val jwtService: Option[JwtService] = JwtServiceImpl.fromEnvironment()
|
||||
// Initialize JWT service (validation only - signing is handled by Auth service)
|
||||
val jwtService: Option[JwtService] = JwtService.fromEnvironment()
|
||||
|
||||
// Add Eagle game service
|
||||
serverBuilder.addService(
|
||||
|
||||
@@ -1,266 +0,0 @@
|
||||
package net.eagle0.eagle
|
||||
|
||||
import scala.annotation.tailrec
|
||||
|
||||
import net.eagle0.eagle.internal.action_result.ActionResult
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.library.actions.impl.common.ActionWithResultingState
|
||||
import net.eagle0.eagle.service.persistence.LocalFilePersister
|
||||
import net.eagle0.eagle.service.PersistedHistory
|
||||
import scalapb.{GeneratedMessage, GeneratedMessageCompanion}
|
||||
import scalapb.descriptors.*
|
||||
|
||||
object MessageComparators {
|
||||
@tailrec
|
||||
def fieldDescriptors(
|
||||
comp: GeneratedMessageCompanion[?],
|
||||
fieldNames: Vector[String],
|
||||
acc: Vector[FieldDescriptor] = Vector()
|
||||
): Vector[FieldDescriptor] = fieldNames match {
|
||||
case name +: tail =>
|
||||
val field =
|
||||
comp.scalaDescriptor.fields
|
||||
.find(_.name == name)
|
||||
.getOrElse {
|
||||
println(s"No field $name in ${comp.scalaDescriptor.name}")
|
||||
sys.exit(1)
|
||||
}
|
||||
|
||||
if tail.isEmpty then acc :+ field
|
||||
else
|
||||
fieldDescriptors(
|
||||
comp.messageCompanionForFieldNumber(field.number),
|
||||
tail,
|
||||
acc :+ field
|
||||
)
|
||||
case _ => acc
|
||||
}
|
||||
|
||||
def nestedPValue(
|
||||
gm: GeneratedMessage,
|
||||
fieldDescriptors: Vector[FieldDescriptor]
|
||||
): PValue =
|
||||
if gm == null then PEmpty
|
||||
else
|
||||
fieldDescriptors match {
|
||||
case Vector(fd) => gm.getField(fd)
|
||||
case fd +: tail =>
|
||||
nestedPValue(
|
||||
gm.getFieldByNumber(fd.number).asInstanceOf[GeneratedMessage],
|
||||
tail
|
||||
)
|
||||
case _ => gm.toPMessage
|
||||
}
|
||||
|
||||
def typedComparison[T](
|
||||
fieldDescriptors: Vector[FieldDescriptor],
|
||||
comparator: String,
|
||||
value: String
|
||||
): (GeneratedMessage => Boolean) = comparator match {
|
||||
case "=" =>
|
||||
gm =>
|
||||
nestedPValue(gm, fieldDescriptors) match {
|
||||
case PInt(i) => i == value.toInt
|
||||
case PDouble(d) => d == value.toDouble
|
||||
case PString(s) => s == value
|
||||
case PBoolean(b) => b == value.toBoolean
|
||||
case PEmpty => value.isEmpty
|
||||
case x =>
|
||||
throw new NotImplementedError(s"$x pvalue not implemented")
|
||||
}
|
||||
case x => throw new NotImplementedError(s"$x comparator not implemented")
|
||||
}
|
||||
}
|
||||
|
||||
object GameStateFilters {
|
||||
val comparisonChars: Vector[Char] = Vector('=')
|
||||
|
||||
def filter(filter: String): ActionWithResultingState => Boolean = {
|
||||
val (left, rightWithComparator) = filter.span(!comparisonChars.contains(_))
|
||||
val (comparator, right) = rightWithComparator.splitAt(1)
|
||||
|
||||
val fieldNames = left.split('.').toVector
|
||||
val (prefix, remaining) = fieldNames.splitAt(1)
|
||||
prefix.head match {
|
||||
case "gs" =>
|
||||
val fieldDescriptors =
|
||||
MessageComparators.fieldDescriptors(
|
||||
GameState.messageCompanion,
|
||||
remaining
|
||||
)
|
||||
awrs =>
|
||||
MessageComparators.typedComparison(
|
||||
fieldDescriptors,
|
||||
comparator,
|
||||
right
|
||||
)(
|
||||
awrs.gameState
|
||||
)
|
||||
case "ar" =>
|
||||
val fieldDescriptors =
|
||||
MessageComparators.fieldDescriptors(
|
||||
ActionResult.messageCompanion,
|
||||
remaining
|
||||
)
|
||||
awrs =>
|
||||
MessageComparators.typedComparison(
|
||||
fieldDescriptors,
|
||||
comparator,
|
||||
right
|
||||
)(
|
||||
awrs.actionResult
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object SavedGameUtils {
|
||||
def filterOne(
|
||||
results: Vector[ActionWithResultingState],
|
||||
filter: String
|
||||
): Vector[ActionWithResultingState] =
|
||||
results.filter {
|
||||
case awrs =>
|
||||
GameStateFilters.filter(filter)(awrs)
|
||||
}
|
||||
|
||||
def filteredResults(
|
||||
results: Vector[ActionWithResultingState],
|
||||
filters: Vector[String]
|
||||
): Vector[ActionWithResultingState] =
|
||||
filters.foldLeft(results)(filterOne)
|
||||
|
||||
case class FieldWithValue(
|
||||
name: String,
|
||||
value: PValue
|
||||
)
|
||||
|
||||
def fieldValues(
|
||||
results: Vector[ActionWithResultingState],
|
||||
fields: Vector[String]
|
||||
): Vector[FieldWithValue] =
|
||||
for {
|
||||
result <- results
|
||||
qualifiedFieldName <- fields
|
||||
} yield {
|
||||
val (ident, subfieldNames) = qualifiedFieldName.split('.').splitAt(1)
|
||||
ident.head match {
|
||||
case "ar" =>
|
||||
FieldWithValue(
|
||||
name = qualifiedFieldName,
|
||||
value = MessageComparators.nestedPValue(
|
||||
result.actionResult,
|
||||
MessageComparators.fieldDescriptors(
|
||||
ActionResult.messageCompanion,
|
||||
subfieldNames.toVector
|
||||
)
|
||||
)
|
||||
)
|
||||
case "gs" =>
|
||||
FieldWithValue(
|
||||
name = qualifiedFieldName,
|
||||
value = MessageComparators.nestedPValue(
|
||||
result.gameState,
|
||||
MessageComparators.fieldDescriptors(
|
||||
GameState.messageCompanion,
|
||||
subfieldNames.toVector
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
def main(args: Array[String]): Unit = {
|
||||
val argList = args.toList
|
||||
|
||||
if argList.size < 1 then {
|
||||
println("Must supply a directory path")
|
||||
sys.exit(1)
|
||||
}
|
||||
|
||||
val dirPath = argList.head
|
||||
|
||||
val allResults = PersistedHistory(123L, LocalFilePersister(dirPath)).get.all
|
||||
|
||||
val _ = go(FilteredResultsState(allResults, Vector(), Vector(), allResults))
|
||||
}
|
||||
|
||||
case class FilteredResultsState(
|
||||
unfilteredResults: Vector[ActionWithResultingState],
|
||||
filters: Vector[String],
|
||||
prints: Vector[String],
|
||||
partialResults: Vector[ActionWithResultingState]
|
||||
) {
|
||||
def withFilters(newFilters: Vector[String]): FilteredResultsState =
|
||||
this.copy(
|
||||
filters = filters ++ newFilters,
|
||||
partialResults = filteredResults(partialResults, newFilters)
|
||||
)
|
||||
|
||||
def withPrints(newPrints: Vector[String]): FilteredResultsState =
|
||||
this.copy(
|
||||
prints = prints ++ newPrints
|
||||
)
|
||||
|
||||
def withoutFilterIndices(indices: Vector[Int]): FilteredResultsState = {
|
||||
val newFilters = filters.zipWithIndex.filterNot { case (_, idx) => indices.contains(idx) }
|
||||
.map(_._1)
|
||||
this.copy(
|
||||
filters = newFilters,
|
||||
partialResults = filteredResults(unfilteredResults, newFilters)
|
||||
)
|
||||
}
|
||||
|
||||
def withoutPrintIndices(indices: Vector[Int]): FilteredResultsState =
|
||||
this.copy(
|
||||
prints = prints.zipWithIndex.filterNot { case (_, idx) => indices.contains(idx) }
|
||||
.map(_._1)
|
||||
)
|
||||
}
|
||||
|
||||
def go(
|
||||
state: FilteredResultsState
|
||||
): FilteredResultsState = {
|
||||
println("Your filters:")
|
||||
state.filters.zipWithIndex.foreach {
|
||||
case (filt, idx) =>
|
||||
println(s" ($idx) $filt")
|
||||
}
|
||||
println("Your prints:")
|
||||
state.prints.zipWithIndex.foreach {
|
||||
case (pf, idx) =>
|
||||
println(s" ($idx) $pf")
|
||||
}
|
||||
println(s"${state.partialResults.size} filtered results")
|
||||
|
||||
println("Your command: ")
|
||||
val args = scala.io.StdIn.readLine().split(' ').toList
|
||||
|
||||
val newState = args match {
|
||||
case Nil => state
|
||||
case "f" :: filters => state.withFilters(filters.toVector)
|
||||
case "p" :: prints => state.withPrints(prints.toVector)
|
||||
case "df" :: idxStrings =>
|
||||
state.withoutFilterIndices(idxStrings.map(_.toInt).toVector)
|
||||
case "dp" :: idxStrings =>
|
||||
state.withoutPrintIndices(idxStrings.map(_.toInt).toVector)
|
||||
case "q" :: _ => sys.exit(0)
|
||||
case x :: _ =>
|
||||
println(s"Unknown command $x, stopping")
|
||||
state
|
||||
}
|
||||
|
||||
if newState.partialResults.size > 1000 then
|
||||
println(
|
||||
s"Not printing now, too many results (${newState.partialResults.size}"
|
||||
)
|
||||
else if newState.prints.nonEmpty then
|
||||
fieldValues(newState.partialResults, newState.prints)
|
||||
.foreach(v => println(s"${v.name}: ${v.value}"))
|
||||
else newState.partialResults.foreach(println)
|
||||
|
||||
println(s"${newState.partialResults.size} filtered results")
|
||||
|
||||
go(newState)
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,15 @@
|
||||
package net.eagle0.eagle.auth
|
||||
|
||||
import java.security.MessageDigest
|
||||
import java.security.interfaces.RSAPublicKey
|
||||
import java.security.spec.X509EncodedKeySpec
|
||||
import java.security.KeyFactory
|
||||
import java.time.Instant
|
||||
import java.util.{Date, UUID}
|
||||
import java.util.{Base64, Date}
|
||||
|
||||
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}
|
||||
import com.nimbusds.jose.crypto.RSASSAVerifier
|
||||
import com.nimbusds.jwt.SignedJWT
|
||||
|
||||
/** JWT claims extracted from a validated token */
|
||||
case class JwtClaims(
|
||||
@@ -22,84 +20,29 @@ case class JwtClaims(
|
||||
expiresAt: Instant
|
||||
)
|
||||
|
||||
/** Refresh token with metadata */
|
||||
case class RefreshTokenInfo(
|
||||
tokenId: String,
|
||||
userId: String,
|
||||
tokenHash: String,
|
||||
expiresAt: Instant
|
||||
)
|
||||
|
||||
/** Service for creating and validating JWT tokens */
|
||||
/**
|
||||
* Service for validating JWT tokens.
|
||||
*
|
||||
* This service is validation-only. Token signing is handled by the Auth service (Go).
|
||||
*/
|
||||
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(val rsaKey: RSAKey) extends JwtService {
|
||||
/**
|
||||
* JWT validation service using an RSA public key.
|
||||
*
|
||||
* This implementation validates tokens signed by the Auth service. It cannot create or sign tokens.
|
||||
*
|
||||
* @param publicKey
|
||||
* RSA public key for signature verification
|
||||
*/
|
||||
class JwtValidationService(publicKey: RSAPublicKey) 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)
|
||||
}
|
||||
private val issuer = "eagle0"
|
||||
private val verifier = new RSASSAVerifier(publicKey)
|
||||
|
||||
override def validateAccessToken(token: String): Option[JwtClaims] =
|
||||
Try {
|
||||
@@ -139,59 +82,69 @@ class JwtServiceImpl(val rsaKey: RSAKey) extends JwtService {
|
||||
)
|
||||
)
|
||||
}.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 {
|
||||
object JwtService {
|
||||
|
||||
/** 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))
|
||||
private val defaultPublicKeyPath = "/etc/eagle0/keys/public.pem"
|
||||
|
||||
/**
|
||||
* Create a JwtService from environment variables.
|
||||
* Create a validation-only service from a PEM-format public key file.
|
||||
*
|
||||
* 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).
|
||||
* @param path
|
||||
* Path to the PEM file containing the RSA public key
|
||||
* @return
|
||||
* JwtService for token validation
|
||||
*/
|
||||
def fromEnvironment(): Option[JwtServiceImpl] =
|
||||
sys.env.get("JWT_PRIVATE_KEY") match {
|
||||
case None =>
|
||||
println("[JwtService] JWT_PRIVATE_KEY environment variable not set")
|
||||
None
|
||||
case Some(keyJson) if keyJson.trim.isEmpty =>
|
||||
println("[JwtService] JWT_PRIVATE_KEY environment variable is empty")
|
||||
None
|
||||
case Some(keyJson) =>
|
||||
Try {
|
||||
val service = fromKeyJson(keyJson)
|
||||
val keyId = service.rsaKey.getKeyID
|
||||
val keyLen = service.rsaKey.size()
|
||||
println(s"[JwtService] Loaded RSA key: id=$keyId, size=${keyLen}bits")
|
||||
service
|
||||
}.recover {
|
||||
case e: Exception =>
|
||||
println(s"[JwtService] Failed to parse JWT_PRIVATE_KEY: ${e.getMessage}")
|
||||
throw e
|
||||
}.toOption
|
||||
def fromPublicKeyFile(path: String): JwtService = {
|
||||
val pem = scala.io.Source.fromFile(path).mkString
|
||||
val publicKey = loadPublicKeyFromPem(pem)
|
||||
new JwtValidationService(publicKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a JwtService from environment or public key file.
|
||||
*
|
||||
* Attempts to load JWT validation keys in order:
|
||||
* 1. JWT_PUBLIC_KEY_FILE environment variable (explicit path)
|
||||
* 2. /etc/eagle0/keys/public.pem (default location from shared volume with auth service)
|
||||
*
|
||||
* @return
|
||||
* Some(JwtService) if keys are available, None otherwise
|
||||
*/
|
||||
def fromEnvironment(): Option[JwtService] = {
|
||||
val path = sys.env.get("JWT_PUBLIC_KEY_FILE").filter(_.trim.nonEmpty) match {
|
||||
case Some(p) => p
|
||||
case None =>
|
||||
val defaultFile = new java.io.File(defaultPublicKeyPath)
|
||||
if defaultFile.exists() then defaultPublicKeyPath
|
||||
else {
|
||||
println(s"[JwtService] No public key found (checked JWT_PUBLIC_KEY_FILE env and $defaultPublicKeyPath)")
|
||||
return None
|
||||
}
|
||||
}
|
||||
|
||||
Try {
|
||||
val service = fromPublicKeyFile(path)
|
||||
println(s"[JwtService] Loaded public key from: $path")
|
||||
service
|
||||
}.recover {
|
||||
case e: Exception =>
|
||||
println(s"[JwtService] Failed to load public key from $path: ${e.getMessage}")
|
||||
throw e
|
||||
}.toOption
|
||||
}
|
||||
|
||||
private def loadPublicKeyFromPem(pem: String): RSAPublicKey = {
|
||||
// Strip PEM headers/footers and whitespace, then base64 decode
|
||||
val base64 = pem
|
||||
.replace("-----BEGIN PUBLIC KEY-----", "")
|
||||
.replace("-----END PUBLIC KEY-----", "")
|
||||
.replaceAll("\\s", "")
|
||||
|
||||
val keyBytes = Base64.getDecoder.decode(base64)
|
||||
val keySpec = new X509EncodedKeySpec(keyBytes)
|
||||
val keyFactory = KeyFactory.getInstance("RSA")
|
||||
keyFactory.generatePublic(keySpec).asInstanceOf[RSAPublicKey]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ scala_library(
|
||||
"//src/test/scala/net/eagle0/eagle:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
"//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/model/action_result/generated_text_request",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -20,13 +20,16 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle:__subpackages__",
|
||||
],
|
||||
exports = [":pregenerated_text_store"],
|
||||
exports = [
|
||||
":pregenerated_text_store",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
|
||||
],
|
||||
deps = [
|
||||
":client_text",
|
||||
":pregenerated_text_store",
|
||||
":text_generation_result",
|
||||
"//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/model/action_result/generated_text_request",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -48,6 +51,8 @@ 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/util:eagle_require",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:generated_text_request_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/service/persistence:persister",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package net.eagle0.eagle.client_text
|
||||
|
||||
import net.eagle0.eagle.internal.generated_text_request.GeneratedTextRequest
|
||||
import net.eagle0.eagle.model.action_result.generated_text_request.GeneratedTextRequestT
|
||||
import net.eagle0.eagle.ClientTextId
|
||||
|
||||
sealed trait ClientText {
|
||||
@@ -21,7 +21,7 @@ case class IncompleteClientText(
|
||||
id: ClientTextId,
|
||||
partialText: String,
|
||||
requestedAfterHistoryCount: Int,
|
||||
llmRequest: GeneratedTextRequest,
|
||||
llmRequest: GeneratedTextRequestT,
|
||||
requestedAtMillis: Long,
|
||||
lastUpdateAtMillis: Long
|
||||
) extends ClientText {
|
||||
@@ -36,7 +36,7 @@ case class IncompleteClientText(
|
||||
case class UnrequestedClientText(
|
||||
id: ClientTextId,
|
||||
requestedAfterHistoryCount: Int,
|
||||
llmRequest: GeneratedTextRequest
|
||||
llmRequest: GeneratedTextRequestT
|
||||
) extends ClientText {
|
||||
def complete: Boolean = false
|
||||
def text: String = ""
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package net.eagle0.eagle.client_text
|
||||
|
||||
import net.eagle0.eagle.{ClientTextId, FactionId}
|
||||
import net.eagle0.eagle.internal.generated_text_request.GeneratedTextRequest
|
||||
import net.eagle0.eagle.model.action_result.generated_text_request.GeneratedTextRequestT
|
||||
|
||||
case class ClientTextStoreWithUpdate(
|
||||
clientTextStore: ClientTextStore,
|
||||
@@ -31,7 +31,7 @@ trait ClientTextStore {
|
||||
def withAddedTextRequest(
|
||||
id: ClientTextId,
|
||||
accessibleTo: Vector[FactionId],
|
||||
llmRequest: GeneratedTextRequest,
|
||||
llmRequest: GeneratedTextRequestT,
|
||||
requestedAfterHistoryCount: Int
|
||||
): ClientTextStore
|
||||
|
||||
|
||||
@@ -10,9 +10,10 @@ import scala.util.Try
|
||||
import net.eagle0.common.ProtoParser
|
||||
import net.eagle0.eagle.{ClientTextId, FactionId}
|
||||
import net.eagle0.eagle.internal.client_text.{IncompleteText, IncompleteTextStore, UnrequestedText}
|
||||
import net.eagle0.eagle.internal.generated_text_request.GeneratedTextRequest
|
||||
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
|
||||
import net.eagle0.eagle.library.EagleInternalException
|
||||
import net.eagle0.eagle.model.action_result.generated_text_request.GeneratedTextRequestT
|
||||
import net.eagle0.eagle.model.proto_converters.GeneratedTextRequestConverter
|
||||
import net.eagle0.eagle.service.persistence.Persister
|
||||
|
||||
case class ClientTextStoreImpl(
|
||||
@@ -31,7 +32,7 @@ case class ClientTextStoreImpl(
|
||||
def withAddedTextRequest(
|
||||
id: ClientTextId,
|
||||
accessibleTo: Vector[FactionId],
|
||||
llmRequest: GeneratedTextRequest,
|
||||
llmRequest: GeneratedTextRequestT,
|
||||
requestedAfterHistoryCount: Int
|
||||
): ClientTextStore = {
|
||||
internalRequire(
|
||||
@@ -240,7 +241,7 @@ object ClientTextStoreImpl {
|
||||
IncompleteText(
|
||||
id = ict.id,
|
||||
partialText = ict.text,
|
||||
llmRequest = Some(ict.llmRequest),
|
||||
llmRequest = Some(GeneratedTextRequestConverter.toProto(ict.llmRequest)),
|
||||
requestedAfterHistoryCount = ict.requestedAfterHistoryCount,
|
||||
requestedAtMillis = ict.requestedAtMillis,
|
||||
lastUpdateAtMillis = ict.lastUpdateAtMillis
|
||||
@@ -250,7 +251,7 @@ object ClientTextStoreImpl {
|
||||
case (_: String, ur: UnrequestedClientText) =>
|
||||
UnrequestedText(
|
||||
id = ur.id,
|
||||
llmRequest = Some(ur.llmRequest),
|
||||
llmRequest = Some(GeneratedTextRequestConverter.toProto(ur.llmRequest)),
|
||||
requestedAfterHistoryCount = ur.requestedAfterHistoryCount
|
||||
)
|
||||
}.toVector
|
||||
@@ -335,7 +336,7 @@ object ClientTextStoreImpl {
|
||||
IncompleteClientText(
|
||||
id = it.id,
|
||||
partialText = it.partialText,
|
||||
llmRequest = it.llmRequest.get,
|
||||
llmRequest = GeneratedTextRequestConverter.fromProto(it.llmRequest.get),
|
||||
requestedAfterHistoryCount = it.requestedAfterHistoryCount,
|
||||
// Use persisted timestamp if available, otherwise use current time
|
||||
// (for backwards compatibility with old persisted data)
|
||||
@@ -350,7 +351,7 @@ object ClientTextStoreImpl {
|
||||
icts.unrequestedTexts.map { it =>
|
||||
UnrequestedClientText(
|
||||
id = it.id,
|
||||
llmRequest = it.llmRequest.get,
|
||||
llmRequest = GeneratedTextRequestConverter.fromProto(it.llmRequest.get),
|
||||
requestedAfterHistoryCount = it.requestedAfterHistoryCount
|
||||
)
|
||||
}.toVector
|
||||
@@ -374,13 +375,17 @@ object ClientTextStoreImpl {
|
||||
.fromInputStream(inputStream)
|
||||
.mkString
|
||||
.split(separatorPattern)
|
||||
.map { entryText =>
|
||||
.flatMap { entryText =>
|
||||
entryText.split("\n") match {
|
||||
case Array(
|
||||
id,
|
||||
accessibleToString
|
||||
) =>
|
||||
id -> accessibleToString.split(",").map(_.toInt).toVector
|
||||
Some(id -> accessibleToString.split(",").map(_.toInt).toVector)
|
||||
|
||||
case x =>
|
||||
println(s"Unexpected entry text: ${x.mkString(", ")}")
|
||||
None
|
||||
}
|
||||
}
|
||||
.toMap
|
||||
@@ -389,6 +394,8 @@ object ClientTextStoreImpl {
|
||||
case _: FileNotFoundException =>
|
||||
println("No accessibleTo store found")
|
||||
Map()
|
||||
|
||||
case x => throw x
|
||||
}
|
||||
|
||||
def loaded(
|
||||
|
||||
@@ -1,49 +1,22 @@
|
||||
package net.eagle0.eagle.library
|
||||
|
||||
import net.eagle0.eagle.common.action_result_notification_details.Notification
|
||||
import net.eagle0.eagle.common.action_result_type.ActionResultType.*
|
||||
import net.eagle0.eagle.common.action_result_type.ActionResultType as ActionResultTypeProto
|
||||
import net.eagle0.eagle.library.actions.impl.common.ActionWithResultingState
|
||||
import net.eagle0.eagle.library.actions.applier.ActionResultWithResultingState
|
||||
import net.eagle0.eagle.library.util.faction_utils.FactionUtils
|
||||
import net.eagle0.eagle.library.util.view_filters.GameStateViewFilter
|
||||
import net.eagle0.eagle.library.util.GameStateViewDiffer
|
||||
import net.eagle0.eagle.model.action_result.types.ActionResultType as ActionResultTypeScala
|
||||
import net.eagle0.eagle.model.action_result.ActionResultT
|
||||
import net.eagle0.eagle.model.proto_converters.view.game_state.GameStateViewDiffConverter
|
||||
import net.eagle0.eagle.model.proto_converters.NotificationConverter
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.RoundPhase
|
||||
import net.eagle0.eagle.model.view.game_state.GameStateViewDiff as ScalaGameStateViewDiff
|
||||
import net.eagle0.eagle.views.action_result_view.ActionResultView
|
||||
import net.eagle0.eagle.views.game_state_view.GameStateViewDiff as GameStateViewDiffProto
|
||||
import net.eagle0.eagle.FactionId
|
||||
|
||||
object ActionResultFilter {
|
||||
private val UNIVERSALLY_VISIBLE_TYPES: Vector[ActionResultTypeProto] = Vector(
|
||||
GAME_START,
|
||||
START_BATTLE,
|
||||
BATTLE_ENDED,
|
||||
PROVINCE_CONQUERED,
|
||||
PROVINCE_HELD,
|
||||
END_PROVINCE_EVENTS_PHASE,
|
||||
END_UNAFFILIATED_HERO_ACTIONS_PHASE,
|
||||
END_VASSAL_COMMANDS_PHASE,
|
||||
END_PLAYER_COMMANDS_PHASE,
|
||||
END_RESOLUTION_PHASE,
|
||||
END_AFTERMATH_PHASE,
|
||||
FACTION_LEADER_REMOVED,
|
||||
NEW_ROUND_ACTION,
|
||||
NEW_YEAR_ACTION,
|
||||
TRUCE_ACCEPTED,
|
||||
ARMY_SHATTERED,
|
||||
RIOT_OCCURRED,
|
||||
WEATHER_TOOK_EFFECT,
|
||||
EPIDEMIC_TOOK_EFFECT,
|
||||
SUPPRESS_BEASTS_FAILED,
|
||||
INVITATION_ACCEPTED,
|
||||
PRISONERS_EXCHANGED,
|
||||
RANSOM_ACCEPTED
|
||||
)
|
||||
|
||||
private val UNIVERSALLY_VISIBLE_TYPES_SCALA: Vector[ActionResultTypeScala] = Vector(
|
||||
private val UNIVERSALLY_VISIBLE_TYPES: Vector[ActionResultTypeScala] = Vector(
|
||||
ActionResultTypeScala.GameStart,
|
||||
ActionResultTypeScala.StartBattle,
|
||||
ActionResultTypeScala.BattleEnded,
|
||||
@@ -71,15 +44,15 @@ object ActionResultFilter {
|
||||
|
||||
private def includeForPlayer(
|
||||
factionId: Option[FactionId],
|
||||
result: ActionWithResultingState
|
||||
result: ActionResultWithResultingState
|
||||
): Boolean =
|
||||
factionId.forall { fid =>
|
||||
val scalaResult = result.scalaActionResult
|
||||
val scalaResult = result.actionResult
|
||||
scalaResult.actingFactionId.contains(fid) ||
|
||||
scalaResult.affectedFactionIds.contains(fid) ||
|
||||
UNIVERSALLY_VISIBLE_TYPES_SCALA.contains(scalaResult.actionResultType) ||
|
||||
UNIVERSALLY_VISIBLE_TYPES.contains(scalaResult.actionResultType) ||
|
||||
scalaResult.provinceId.exists(pid =>
|
||||
result.scalaGameState
|
||||
result.resultingState
|
||||
.provinces(pid)
|
||||
.rulingFactionId
|
||||
.contains(fid)
|
||||
@@ -91,7 +64,7 @@ object ActionResultFilter {
|
||||
|
||||
private def observeOne(
|
||||
startingState: GameState,
|
||||
result: ActionWithResultingState
|
||||
result: ActionResultWithResultingState
|
||||
): Option[ActionResultView] =
|
||||
transformOne(
|
||||
result = result,
|
||||
@@ -101,7 +74,7 @@ object ActionResultFilter {
|
||||
|
||||
private def filterOne(
|
||||
startingState: GameState,
|
||||
result: ActionWithResultingState,
|
||||
result: ActionResultWithResultingState,
|
||||
factionId: FactionId
|
||||
): Option[ActionResultView] =
|
||||
transformOne(
|
||||
@@ -133,7 +106,7 @@ object ActionResultFilter {
|
||||
|
||||
private def transformOne(
|
||||
startingState: GameState,
|
||||
result: ActionWithResultingState,
|
||||
result: ActionResultWithResultingState,
|
||||
factionId: Option[FactionId]
|
||||
): Option[ActionResultView] = {
|
||||
val actionResult = result.actionResult
|
||||
@@ -141,17 +114,17 @@ object ActionResultFilter {
|
||||
|
||||
val gsDiff = filteredGameStateDiff(
|
||||
before = startingState,
|
||||
after = result.scalaGameState,
|
||||
after = result.resultingState,
|
||||
factionId = factionId
|
||||
)
|
||||
|
||||
def shouldInclude(note: Notification): Boolean =
|
||||
note.targetFactions.isEmpty || factionId.exists(fid => note.targetFactions.exists(_.factionId == fid))
|
||||
def shouldIncludeNotification(targetFactionIds: Vector[FactionId]): Boolean =
|
||||
targetFactionIds.isEmpty || factionId.exists(targetFactionIds.contains)
|
||||
|
||||
if gsDiff.isDefined &&
|
||||
result.actionResult.player.isDefined &&
|
||||
actionResult.actingFactionId.isDefined &&
|
||||
factionId.exists { askingFid =>
|
||||
!result.actionResult.player.exists(targetFid =>
|
||||
!actionResult.actingFactionId.exists(targetFid =>
|
||||
targetFid == askingFid ||
|
||||
FactionUtils.hasAlliance(
|
||||
askingFid,
|
||||
@@ -166,20 +139,26 @@ object ActionResultFilter {
|
||||
println(actionResult)
|
||||
}
|
||||
Option.when(includeForPlayer(factionId, result) || gsDiff.nonEmpty) {
|
||||
// Convert Scala types to proto for the view
|
||||
val filteredNotifications = actionResult.newNotifications
|
||||
.filter(n => shouldIncludeNotification(n.targetFactionIds))
|
||||
.map(NotificationConverter.toProto)
|
||||
.collect { case (proto, true) => proto }
|
||||
|
||||
ActionResultView(
|
||||
`type` = actionResult.`type`,
|
||||
player = actionResult.player,
|
||||
province = actionResult.province,
|
||||
leader = actionResult.leader,
|
||||
`type` = ActionResultTypeProto.fromValue(actionResult.actionResultType.value),
|
||||
player = actionResult.actingFactionId,
|
||||
province = actionResult.provinceId,
|
||||
leader = actionResult.actingHeroId,
|
||||
gameStateDiff = gsDiff,
|
||||
notifications = actionResult.notificationsToDeliver.filter(shouldInclude)
|
||||
notifications = filteredNotifications
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
def filterForOptionalPlayer(
|
||||
startingState: GameState,
|
||||
results: Vector[ActionWithResultingState],
|
||||
results: Vector[ActionResultWithResultingState],
|
||||
factionId: Option[FactionId]
|
||||
): Vector[ActionResultView] =
|
||||
factionId.map { pid =>
|
||||
@@ -189,7 +168,7 @@ object ActionResultFilter {
|
||||
|
||||
def filterForPlayer(
|
||||
startingState: GameState,
|
||||
results: Vector[ActionWithResultingState],
|
||||
results: Vector[ActionResultWithResultingState],
|
||||
factionId: FactionId
|
||||
): Vector[ActionResultView] =
|
||||
results
|
||||
@@ -201,13 +180,13 @@ object ActionResultFilter {
|
||||
factionId = factionId
|
||||
)
|
||||
|
||||
(res.scalaGameState, acc ++ maybeNewRes)
|
||||
(res.resultingState, acc ++ maybeNewRes)
|
||||
}
|
||||
._2
|
||||
|
||||
def filterForObserver(
|
||||
startingState: GameState,
|
||||
results: Vector[ActionWithResultingState]
|
||||
results: Vector[ActionResultWithResultingState]
|
||||
): Vector[ActionResultView] =
|
||||
results.flatMap(res => observeOne(result = res, startingState = startingState))
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ scala_library(
|
||||
"//src/test/scala/net/eagle0/eagle/service/controller:__pkg__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/command/selected",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
@@ -18,10 +19,9 @@ scala_library(
|
||||
],
|
||||
deps = [
|
||||
":game_history",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/views:action_result_view_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/command/selected",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
@@ -45,8 +45,6 @@ scala_library(
|
||||
":engine",
|
||||
":game_history",
|
||||
":round_phase_advancer",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/views:action_result_view_scala_proto",
|
||||
"//src/main/scala/net/eagle0/common:functional_random",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
@@ -70,9 +68,7 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:action_result_proto_converter",
|
||||
"//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/state/battalion",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/command:command_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
|
||||
@@ -94,19 +90,19 @@ scala_library(
|
||||
],
|
||||
deps = [
|
||||
"//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/views:action_result_view_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/views:game_state_view_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_applier",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:game_state_view_differ",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/view_filters:game_state_view_filter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:notification_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/view/game_state:game_state_view_diff_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/game_state:game_state_view_diff",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -151,21 +147,16 @@ scala_library(
|
||||
"//src/main/protobuf/net/eagle0/shardok/api:action_result_view_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/shardok/api:command_descriptor_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/shardok/storage:action_result_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_applier",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/date",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/shardok/api:action_result_view_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/shardok/api:command_descriptor_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/shardok/storage:action_result_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_applier",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:action_result_proto_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/date",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
|
||||
@@ -3,7 +3,7 @@ package net.eagle0.eagle.library
|
||||
import scala.collection.immutable.SortedMap
|
||||
|
||||
import net.eagle0.eagle.{FactionId, GameId, ProvinceId}
|
||||
import net.eagle0.eagle.internal.action_result.ActionResult
|
||||
import net.eagle0.eagle.model.action_result.ActionResultT
|
||||
import net.eagle0.eagle.model.state.command.available.OneProvinceAvailableCommands
|
||||
import net.eagle0.eagle.model.state.command.selected.SelectedCommand
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
@@ -12,7 +12,7 @@ import net.eagle0.eagle.views.action_result_view.ActionResultView
|
||||
|
||||
trait EngineAndResults {
|
||||
def engine: Engine
|
||||
def results: Vector[ActionResult]
|
||||
def results: Vector[ActionResultT]
|
||||
def saveNow: EngineAndResults
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import scala.collection.immutable.SortedMap
|
||||
|
||||
import net.eagle0.common.SeededRandom
|
||||
import net.eagle0.eagle.{FactionId, GameId, ProvinceId}
|
||||
import net.eagle0.eagle.internal.action_result.ActionResult
|
||||
import net.eagle0.eagle.library.actions.applier.{ActionResultApplierImpl, ActionResultWithResultingState}
|
||||
import net.eagle0.eagle.library.actions.availability.AvailableCommandsFactory
|
||||
import net.eagle0.eagle.library.actions.impl.action.{
|
||||
@@ -20,7 +19,6 @@ import net.eagle0.eagle.library.util.validations.RuntimeValidator
|
||||
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
|
||||
import net.eagle0.eagle.library.EngineImpl.{appliedResults, withUpdateChecks}
|
||||
import net.eagle0.eagle.model.action_result.ActionResultT
|
||||
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
|
||||
import net.eagle0.eagle.model.state.command.available.OneProvinceAvailableCommands
|
||||
import net.eagle0.eagle.model.state.command.selected.SelectedCommand
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
@@ -35,7 +33,7 @@ object EngineImpl {
|
||||
): EngineImpl =
|
||||
EngineImpl(
|
||||
gameId = gameId,
|
||||
currentState = GameStateConverter.fromProto(history.last.gameState).copy(gameId = gameId),
|
||||
currentState = history.last.resultingState.copy(gameId = gameId),
|
||||
heroGenerator = heroGenerator,
|
||||
history = history
|
||||
)
|
||||
@@ -92,18 +90,16 @@ object EngineImpl {
|
||||
currentState = results.lastOption
|
||||
.map(_.resultingState)
|
||||
.getOrElse(engine.currentState),
|
||||
history = engine.history.withNewResultsScala(results)
|
||||
history = engine.history.withNewResults(results)
|
||||
),
|
||||
results = results.map(awrs =>
|
||||
net.eagle0.eagle.model.proto_converters.ActionResultProtoConverter.toProto(awrs.actionResult)
|
||||
)
|
||||
results = results.map(_.actionResult)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
final case class EngineAndResultsImpl(
|
||||
engine: EngineImpl,
|
||||
results: Vector[ActionResult]
|
||||
results: Vector[ActionResultT]
|
||||
) extends EngineAndResults {
|
||||
def recursiveTransformScala(
|
||||
f: EngineImpl => Vector[ActionResultWithResultingState]
|
||||
@@ -111,7 +107,7 @@ final case class EngineAndResultsImpl(
|
||||
@tailrec
|
||||
def go(
|
||||
eng: EngineImpl,
|
||||
acc: Vector[ActionResult]
|
||||
acc: Vector[ActionResultT]
|
||||
): EngineAndResultsImpl = {
|
||||
val goResults = f(eng)
|
||||
|
||||
@@ -244,7 +240,7 @@ case class EngineImpl(
|
||||
|
||||
val truncatedHistory = history.truncateTo(targetActionCount)
|
||||
copy(
|
||||
currentState = GameStateConverter.fromProto(truncatedHistory.last.gameState).copy(gameId = gameId),
|
||||
currentState = truncatedHistory.last.resultingState.copy(gameId = gameId),
|
||||
history = truncatedHistory
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,9 +2,6 @@ package net.eagle0.eagle.library
|
||||
|
||||
import net.eagle0.eagle.{FactionId, RoundId, ShardokGameId}
|
||||
import net.eagle0.eagle.library.actions.applier.ActionResultWithResultingState
|
||||
import net.eagle0.eagle.library.actions.impl.common.ActionWithResultingState
|
||||
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
|
||||
import net.eagle0.eagle.model.proto_converters.ActionResultProtoConverter
|
||||
import net.eagle0.eagle.model.state.date.Date
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.shardok.api.action_result_view.ActionResultView as ShardokActionResultView
|
||||
@@ -12,13 +9,13 @@ import net.eagle0.shardok.api.command_descriptor.AvailableCommands as ShardokAva
|
||||
import net.eagle0.shardok.storage.action_result.ActionResult as ShardokActionResult
|
||||
|
||||
case class CappedResults(
|
||||
results: Vector[ActionWithResultingState],
|
||||
results: Vector[ActionResultWithResultingState],
|
||||
startingState: Option[GameState]
|
||||
)
|
||||
|
||||
trait GameHistory {
|
||||
def all: Vector[ActionWithResultingState]
|
||||
def since(count: Int): Vector[ActionWithResultingState]
|
||||
def all: Vector[ActionResultWithResultingState]
|
||||
def since(count: Int): Vector[ActionResultWithResultingState]
|
||||
def sinceCapped(count: Int, resultSizeCap: Int): CappedResults =
|
||||
if this.count - count <= resultSizeCap then CappedResults(since(count), None)
|
||||
else
|
||||
@@ -27,31 +24,21 @@ trait GameHistory {
|
||||
Some(stateAfter(this.count - resultSizeCap))
|
||||
)
|
||||
def stateAfter(count: Int): GameState
|
||||
def sinceDate(date: Date): Vector[ActionWithResultingState]
|
||||
def sinceDate(date: Date): Vector[ActionResultWithResultingState]
|
||||
def count: Int
|
||||
def last: ActionWithResultingState
|
||||
def last: ActionResultWithResultingState
|
||||
|
||||
def recentResultsForRound(
|
||||
roundId: RoundId,
|
||||
predicate: ActionWithResultingState => Boolean = _ => true
|
||||
): Vector[ActionWithResultingState]
|
||||
predicate: ActionResultWithResultingState => Boolean = _ => true
|
||||
): Vector[ActionResultWithResultingState]
|
||||
|
||||
def saveNow: GameHistory
|
||||
|
||||
def withNewResults(newResults: Vector[ActionWithResultingState]): GameHistory
|
||||
def withNewResults(newResults: Vector[ActionResultWithResultingState]): GameHistory
|
||||
|
||||
def truncateTo(targetActionCount: Int): GameHistory
|
||||
|
||||
def withNewResultsScala(newResults: Vector[ActionResultWithResultingState]): GameHistory =
|
||||
withNewResults(newResults.map { awrs =>
|
||||
ActionWithResultingState(
|
||||
actionResult = ActionResultProtoConverter.toProto(awrs.actionResult),
|
||||
gameState = GameStateConverter.toProto(awrs.resultingState),
|
||||
precomputedScalaState = Some(awrs.resultingState), // Preserve Scala state to avoid re-conversion
|
||||
precomputedScalaActionResult = Some(awrs.actionResult) // Preserve Scala result to avoid re-conversion
|
||||
)
|
||||
})
|
||||
|
||||
def shardokCount(shardokGameId: ShardokGameId): Int
|
||||
|
||||
def shardokGameState(
|
||||
|
||||
@@ -7,6 +7,7 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/library:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/actions:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/service:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
|
||||
@@ -128,15 +128,12 @@ scala_library(
|
||||
exports = [
|
||||
],
|
||||
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: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:notification_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request/chronicle_event",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:notification_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/date",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/date",
|
||||
],
|
||||
)
|
||||
|
||||
+13
-13
@@ -1,25 +1,25 @@
|
||||
package net.eagle0.eagle.library.actions.impl.action
|
||||
|
||||
import net.eagle0.eagle.library.actions.impl.common.ActionWithResultingState
|
||||
import net.eagle0.eagle.library.actions.applier.ActionResultWithResultingState
|
||||
import net.eagle0.eagle.library.GameHistory
|
||||
import net.eagle0.eagle.model.action_result.generated_text_request.chronicle_event.*
|
||||
import net.eagle0.eagle.model.action_result.types.ActionResultType
|
||||
import net.eagle0.eagle.model.action_result.NotificationDetails
|
||||
import net.eagle0.eagle.model.proto_converters.date.DateConverter
|
||||
import net.eagle0.eagle.model.proto_converters.NotificationConverter
|
||||
import net.eagle0.eagle.model.state.date.Date
|
||||
|
||||
object ChronicleEventGenerator {
|
||||
private def relevantNotificationDetails(
|
||||
awrs: ActionWithResultingState
|
||||
awrs: ActionResultWithResultingState
|
||||
): Vector[NotificationDetails] =
|
||||
awrs.actionResult.notificationsToDeliver.distinct.toVector
|
||||
.map(n => NotificationConverter.fromProto(n.details))
|
||||
awrs.actionResult.newNotifications
|
||||
.map(_.details)
|
||||
.distinct
|
||||
|
||||
private def notificationEntries(
|
||||
awrs: ActionWithResultingState
|
||||
awrs: ActionResultWithResultingState
|
||||
): Vector[ChronicleEvent] = {
|
||||
val date = DateConverter.fromProto(awrs.gameState.currentDate)
|
||||
// Chronicle events are only generated during active gameplay when currentDate is present
|
||||
val date = awrs.resultingState.currentDate.get
|
||||
relevantNotificationDetails(awrs).collect {
|
||||
case NotificationDetails.AllianceAccepted(
|
||||
offeringFid,
|
||||
@@ -317,17 +317,17 @@ object ChronicleEventGenerator {
|
||||
}
|
||||
|
||||
private def basicActionResultEntries(
|
||||
awrs: ActionWithResultingState
|
||||
awrs: ActionResultWithResultingState
|
||||
): Vector[ChronicleEvent] =
|
||||
ActionResultType.fromValue(awrs.actionResult.`type`.value) match {
|
||||
awrs.actionResult.actionResultType match {
|
||||
case ActionResultType.FactionDestroyed =>
|
||||
val date = DateConverter.fromProto(awrs.gameState.currentDate)
|
||||
val date = awrs.resultingState.currentDate.get
|
||||
awrs.actionResult.removedFactionIds.map { factionId =>
|
||||
FactionDestroyedChronicleEvent(
|
||||
date = date,
|
||||
factionId = factionId
|
||||
)
|
||||
}.toVector
|
||||
}
|
||||
|
||||
case _ => Vector()
|
||||
}
|
||||
@@ -336,7 +336,7 @@ object ChronicleEventGenerator {
|
||||
gameHistory: GameHistory,
|
||||
since: Date
|
||||
): Vector[ChronicleEvent] =
|
||||
gameHistory.sinceDate(since).flatMap { (awrs: ActionWithResultingState) =>
|
||||
gameHistory.sinceDate(since).flatMap { (awrs: ActionResultWithResultingState) =>
|
||||
notificationEntries(awrs) ++ basicActionResultEntries(awrs)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,7 +191,6 @@ scala_library(
|
||||
srcs = ["GeneratorUtilities.scala"],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//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",
|
||||
@@ -226,7 +225,6 @@ scala_library(
|
||||
":faction_info_utilities",
|
||||
":generator_utilities",
|
||||
":hero_info_utilities",
|
||||
"//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",
|
||||
@@ -343,7 +341,6 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
|
||||
"//src/main/scala/net/eagle0/common:functional_random",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:client_text_store",
|
||||
@@ -490,7 +487,6 @@ scala_library(
|
||||
":faction_info_utilities",
|
||||
":hero_description_generator",
|
||||
":hero_info_utilities",
|
||||
"//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",
|
||||
|
||||
+2
-2
@@ -8,8 +8,8 @@ scala_library(
|
||||
"//src/test/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:province_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/diplomacy_offer/status",
|
||||
],
|
||||
)
|
||||
|
||||
+44
-59
@@ -1,15 +1,8 @@
|
||||
package net.eagle0.eagle.library.actions.generated_text_request_generators.diplomacy_llm_helpers
|
||||
|
||||
import net.eagle0.eagle.{FactionId, GameId, HeroId, RoundId}
|
||||
import net.eagle0.eagle.common.diplomacy_offer_status.DiplomacyOfferStatus
|
||||
import net.eagle0.eagle.internal.generated_text_request.{
|
||||
AllianceOfferResolutionMessage,
|
||||
BreakAllianceResolutionMessage,
|
||||
GeneratedTextRequest,
|
||||
InvitationResolutionMessage,
|
||||
RansomResolutionMessage,
|
||||
TruceResolutionMessage
|
||||
}
|
||||
import net.eagle0.eagle.model.action_result.generated_text_request.LlmRequestT
|
||||
import net.eagle0.eagle.model.state.diplomacy_offer.status.Status
|
||||
|
||||
object DiplomacyResolutionLlmRequestGenerator {
|
||||
def truceResolutionRequest(
|
||||
@@ -18,17 +11,15 @@ object DiplomacyResolutionLlmRequestGenerator {
|
||||
originatingFactionId: FactionId,
|
||||
targetFactionId: FactionId,
|
||||
messengerHeroId: HeroId,
|
||||
resolution: DiplomacyOfferStatus
|
||||
): GeneratedTextRequest =
|
||||
GeneratedTextRequest(
|
||||
id = s"$currentRoundId truce offer $originatingFactionId $targetFactionId resolution $resolution",
|
||||
resolution: Status
|
||||
): LlmRequestT =
|
||||
LlmRequestT.TruceResolutionMessage(
|
||||
requestId = s"$currentRoundId truce offer $originatingFactionId $targetFactionId resolution $resolution",
|
||||
eagleGameId = gameId,
|
||||
details = TruceResolutionMessage(
|
||||
offeringFactionId = originatingFactionId,
|
||||
targetFactionId = targetFactionId,
|
||||
messengerHeroId = messengerHeroId,
|
||||
resolutionStatus = resolution
|
||||
)
|
||||
offeringFactionId = originatingFactionId,
|
||||
targetFactionId = targetFactionId,
|
||||
messengerHeroId = messengerHeroId,
|
||||
resolution = resolution
|
||||
)
|
||||
|
||||
def allianceResolutionRequest(
|
||||
@@ -37,17 +28,15 @@ object DiplomacyResolutionLlmRequestGenerator {
|
||||
originatingFactionId: FactionId,
|
||||
targetFactionId: FactionId,
|
||||
messengerHeroId: HeroId,
|
||||
resolution: DiplomacyOfferStatus
|
||||
): GeneratedTextRequest =
|
||||
GeneratedTextRequest(
|
||||
id = s"$currentRoundId alliance offer $originatingFactionId $targetFactionId resolution $resolution",
|
||||
resolution: Status
|
||||
): LlmRequestT =
|
||||
LlmRequestT.AllianceOfferResolutionMessage(
|
||||
requestId = s"$currentRoundId alliance offer $originatingFactionId $targetFactionId resolution $resolution",
|
||||
eagleGameId = gameId,
|
||||
details = AllianceOfferResolutionMessage(
|
||||
offeringFactionId = originatingFactionId,
|
||||
targetFactionId = targetFactionId,
|
||||
messengerHeroId = messengerHeroId,
|
||||
resolutionStatus = resolution
|
||||
)
|
||||
offeringFactionId = originatingFactionId,
|
||||
targetFactionId = targetFactionId,
|
||||
messengerHeroId = messengerHeroId,
|
||||
resolution = resolution
|
||||
)
|
||||
|
||||
def breakAllianceResolutionRequest(
|
||||
@@ -56,17 +45,15 @@ object DiplomacyResolutionLlmRequestGenerator {
|
||||
originatingFactionId: FactionId,
|
||||
targetFactionId: FactionId,
|
||||
messengerHeroId: HeroId,
|
||||
resolution: DiplomacyOfferStatus
|
||||
): GeneratedTextRequest =
|
||||
GeneratedTextRequest(
|
||||
id = s"$currentRoundId break alliance $originatingFactionId $targetFactionId resolution $resolution",
|
||||
resolution: Status
|
||||
): LlmRequestT =
|
||||
LlmRequestT.BreakAllianceResolutionMessage(
|
||||
requestId = s"$currentRoundId break alliance $originatingFactionId $targetFactionId resolution $resolution",
|
||||
eagleGameId = gameId,
|
||||
details = BreakAllianceResolutionMessage(
|
||||
breakingFactionId = originatingFactionId,
|
||||
targetFactionId = targetFactionId,
|
||||
messengerHeroId = messengerHeroId,
|
||||
resolutionStatus = resolution
|
||||
)
|
||||
breakingFactionId = originatingFactionId,
|
||||
targetFactionId = targetFactionId,
|
||||
messengerHeroId = messengerHeroId,
|
||||
resolution = resolution
|
||||
)
|
||||
|
||||
def invitationResolutionRequest(
|
||||
@@ -75,17 +62,16 @@ object DiplomacyResolutionLlmRequestGenerator {
|
||||
originatingFactionId: FactionId,
|
||||
invitedFactionId: FactionId,
|
||||
messengerHeroId: HeroId,
|
||||
resolution: DiplomacyOfferStatus
|
||||
): GeneratedTextRequest =
|
||||
GeneratedTextRequest(
|
||||
id = s"$currentRoundId invitation resolution $originatingFactionId $invitedFactionId resolution $resolution",
|
||||
resolution: Status
|
||||
): LlmRequestT =
|
||||
LlmRequestT.InvitationResolutionMessage(
|
||||
requestId =
|
||||
s"$currentRoundId invitation resolution $originatingFactionId $invitedFactionId resolution $resolution",
|
||||
eagleGameId = gameId,
|
||||
details = InvitationResolutionMessage(
|
||||
offeringFactionId = originatingFactionId,
|
||||
targetFactionId = invitedFactionId,
|
||||
messengerHeroId = messengerHeroId,
|
||||
resolutionStatus = resolution
|
||||
)
|
||||
invitingFactionId = originatingFactionId,
|
||||
targetFactionId = invitedFactionId,
|
||||
messengerHeroId = messengerHeroId,
|
||||
resolution = resolution
|
||||
)
|
||||
|
||||
def ransomResolutionRequest(
|
||||
@@ -94,16 +80,15 @@ object DiplomacyResolutionLlmRequestGenerator {
|
||||
originatingFactionId: FactionId,
|
||||
targetFactionId: FactionId,
|
||||
ransomedHeroId: HeroId,
|
||||
resolution: DiplomacyOfferStatus
|
||||
): GeneratedTextRequest =
|
||||
GeneratedTextRequest(
|
||||
id = s"$currentRoundId ransom resolution $originatingFactionId $targetFactionId resolution $resolution",
|
||||
resolution: Status
|
||||
): LlmRequestT =
|
||||
LlmRequestT.RansomResolutionMessage(
|
||||
requestId = s"$currentRoundId ransom resolution $originatingFactionId $targetFactionId resolution $resolution",
|
||||
eagleGameId = gameId,
|
||||
details = RansomResolutionMessage(
|
||||
offeringFactionId = originatingFactionId,
|
||||
targetFactionId = targetFactionId,
|
||||
ransomedHeroId = ransomedHeroId,
|
||||
resolutionStatus = resolution
|
||||
)
|
||||
offeringFactionId = originatingFactionId,
|
||||
targetFactionId = targetFactionId,
|
||||
offeringHeroId = ransomedHeroId, // Note: This seems like it should be a separate field
|
||||
ransomedHeroId = ransomedHeroId,
|
||||
resolution = resolution
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,8 +11,9 @@ scala_library(
|
||||
"//src/test/scala/net/eagle0/eagle:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:battalion_type_scala_proto",
|
||||
"//src/main/scala/net/eagle0/common:tsv_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type_id",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import java.net.URL
|
||||
import scala.util.Try
|
||||
|
||||
import net.eagle0.common.TsvUtils
|
||||
import net.eagle0.eagle.common.battalion_type.{BattalionType, BattalionTypeId}
|
||||
import net.eagle0.eagle.model.state.{BattalionType, BattalionTypeId}
|
||||
|
||||
object BattalionTypeLoader {
|
||||
private def loadBattalionTypesFromTsv(
|
||||
@@ -38,7 +38,7 @@ object BattalionTypeLoader {
|
||||
|
||||
private def battalionTypeFromMap(m: Map[String, String]): BattalionType =
|
||||
BattalionType(
|
||||
typeId = BattalionTypeId.fromValue(m("TypeId").toInt),
|
||||
typeId = BattalionTypeId.fromInt(m("TypeId").toInt),
|
||||
name = m("TypeName"),
|
||||
capacity = m("Capacity").toInt,
|
||||
allowsCasting = m("AllowsCasting").toBoolean,
|
||||
|
||||
@@ -87,6 +87,7 @@ scala_library(
|
||||
deps = [
|
||||
":map_generator",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
"@maven//:org_json4s_json4s_ast_3",
|
||||
"@maven//:org_json4s_json4s_core_3",
|
||||
"@maven//:org_json4s_json4s_native_3",
|
||||
@@ -184,10 +185,12 @@ scala_library(
|
||||
],
|
||||
exports = ["//src/main/protobuf/net/eagle0/eagle/internal:province_scala_proto"],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:province_scala_proto",
|
||||
"//src/main/scala/net/eagle0/common:tsv_utils",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:hero_cap_per_castle",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:eagle_require",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province/concrete",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -3,19 +3,21 @@ package net.eagle0.eagle.library.util
|
||||
import java.net.URL
|
||||
|
||||
import net.eagle0.common.TsvUtils
|
||||
import net.eagle0.eagle.internal.province.{Neighbor, Province}
|
||||
import net.eagle0.eagle.library.settings.HeroCapPerCastle
|
||||
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
|
||||
import net.eagle0.eagle.model.state.province.{Neighbor, ProvinceT}
|
||||
import net.eagle0.eagle.model.state.province.concrete.ProvinceC
|
||||
import net.eagle0.eagle.ProvinceId
|
||||
|
||||
object MapGenerator {
|
||||
def loadMap(tsvUrl: URL): Vector[Province] =
|
||||
def loadMap(tsvUrl: URL): Vector[ProvinceT] =
|
||||
TsvUtils
|
||||
.loadMaps(url = tsvUrl)
|
||||
.get
|
||||
.map(provinceFromMap)
|
||||
|
||||
def provinceFromMap(map: Map[String, String]): Province =
|
||||
Province(
|
||||
def provinceFromMap(map: Map[String, String]): ProvinceC =
|
||||
ProvinceC(
|
||||
id = map("id").toInt,
|
||||
name = map("name"),
|
||||
neighbors = neighborsFromMap(map),
|
||||
|
||||
@@ -23,6 +23,7 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/quest_fulfillment:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/validations:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/service:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/actions:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/util/validations:__pkg__",
|
||||
],
|
||||
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
package net.eagle0.eagle.model.action_result
|
||||
|
||||
import net.eagle0.eagle.ClientTextId
|
||||
import net.eagle0.eagle.{ClientTextId, FactionId}
|
||||
|
||||
trait ClientTextVisibilityExtensionT {
|
||||
def textId: ClientTextId
|
||||
def recipientFactionIds: Vector[FactionId]
|
||||
}
|
||||
|
||||
@@ -7,8 +7,11 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions:__subpackages__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:__subpackages__",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/service/new_game_creation:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/actions:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/util:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/service:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/service/new_game_creation:__pkg__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_component_trait",
|
||||
|
||||
@@ -4,6 +4,7 @@ scala_library(
|
||||
name = "generated_text_request",
|
||||
srcs = ["GeneratedTextRequestT.scala"],
|
||||
visibility = [
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/library:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions:__subpackages__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:__subpackages__",
|
||||
@@ -12,9 +13,12 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request/concrete:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/llm_request:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/service:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/service/new_game_creation:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/client_text:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/actions:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/util:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/service:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
|
||||
+2
@@ -33,6 +33,7 @@ import net.eagle0.eagle.model.proto_converters.date.DateConverter
|
||||
import net.eagle0.eagle.model.proto_converters.faction.FactionConverter
|
||||
import net.eagle0.eagle.model.proto_converters.generated_text_request.LlmRequestConverter
|
||||
import net.eagle0.eagle.model.proto_converters.hero.{GenderConverter, HeroConverter}
|
||||
import net.eagle0.eagle.model.proto_converters.province.ProvinceConverter
|
||||
import net.eagle0.eagle.model.proto_converters.shardok_battle.ShardokBattleConverter
|
||||
import net.eagle0.eagle.model.state.battalion.BattalionT
|
||||
import net.eagle0.eagle.model.state.chronicle_entry.ChronicleEntry
|
||||
@@ -118,6 +119,7 @@ object ActionResultProtoConverter {
|
||||
newBattalions = newBattalions.map(BattalionConverter.toProto),
|
||||
newFactions = newFactions.map(FactionConverter.toProto),
|
||||
newHeroes = newHeroes.map(HeroConverter.toProto),
|
||||
newProvinces = newProvinces.map(ProvinceConverter.toProto),
|
||||
newBattalionTypes = newBattalionTypes.map(BattalionTypeConverter.toProto),
|
||||
destroyedBattalionIds = removedBattalionIds,
|
||||
removedHeroes = removedHeroIds,
|
||||
|
||||
@@ -9,10 +9,12 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/service:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/service/new_game_creation:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
|
||||
@@ -60,6 +62,24 @@ scala_library(
|
||||
],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "generated_text_request_converter",
|
||||
srcs = ["GeneratedTextRequestConverter.scala"],
|
||||
visibility = [
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/service:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/service:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/service:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
"//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/model/action_result/generated_text_request",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/hero:gender",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/llm_request",
|
||||
],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "army_converter",
|
||||
srcs = ["ArmyConverter.scala"],
|
||||
@@ -97,6 +117,7 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/view_filters:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/command/available:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/service/new_game_creation:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/shardok_interface:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
|
||||
@@ -117,15 +138,7 @@ scala_library(
|
||||
scala_library(
|
||||
name = "battalion_type_id_converter",
|
||||
srcs = ["BattalionTypeIdConverter.scala"],
|
||||
visibility = [
|
||||
"//src/main/scala/net/eagle0/eagle/library:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/llm_prompt_generators:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/view_filters:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/province:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/view/battalion:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
|
||||
],
|
||||
visibility = ["//visibility:public"],
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type_id",
|
||||
],
|
||||
@@ -140,19 +153,7 @@ scala_library(
|
||||
scala_library(
|
||||
name = "battalion_type_converter",
|
||||
srcs = ["BattalionTypeConverter.scala"],
|
||||
visibility = [
|
||||
"//src/main/scala/net/eagle0/eagle/library:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/province:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/view_filters:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/view/game_state:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/util:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
|
||||
],
|
||||
visibility = ["//visibility:public"],
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type",
|
||||
],
|
||||
@@ -373,6 +374,7 @@ scala_library(
|
||||
name = "notification_converter",
|
||||
srcs = ["NotificationConverter.scala"],
|
||||
visibility = [
|
||||
"//src/main/scala/net/eagle0/eagle/library:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
|
||||
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
package net.eagle0.eagle.model.proto_converters
|
||||
|
||||
import net.eagle0.eagle.{FactionId, GameId, HeroId}
|
||||
import net.eagle0.eagle.internal.generated_text_request.{
|
||||
FixedHeroNameMessage,
|
||||
GeneratedHeroNameMessage,
|
||||
GeneratedTextRequest
|
||||
}
|
||||
import net.eagle0.eagle.model.action_result.generated_text_request.{
|
||||
FixedHeroName,
|
||||
GeneratedHeroName,
|
||||
GeneratedTextRequestT,
|
||||
LlmRequestT
|
||||
}
|
||||
import net.eagle0.eagle.model.proto_converters.generated_text_request.LlmRequestConverter
|
||||
import net.eagle0.eagle.model.proto_converters.hero.GenderConverter
|
||||
|
||||
object GeneratedTextRequestConverter {
|
||||
def toProto(gtr: GeneratedTextRequestT): GeneratedTextRequest = gtr match {
|
||||
case llmRequestT: LlmRequestT =>
|
||||
LlmRequestConverter.toProto(llmRequestT)
|
||||
case fixedHeroName: FixedHeroName =>
|
||||
GeneratedTextRequest(
|
||||
id = fixedHeroName.requestId,
|
||||
eagleGameId = fixedHeroName.eagleGameId,
|
||||
recipientFactionIds = fixedHeroName.recipientFactionIds,
|
||||
alwaysGenerate = fixedHeroName.alwaysGenerate,
|
||||
details = FixedHeroNameMessage(
|
||||
heroId = fixedHeroName.heroId,
|
||||
fixedName = fixedHeroName.fixedName
|
||||
)
|
||||
)
|
||||
case generatedHeroName: GeneratedHeroName =>
|
||||
GeneratedTextRequest(
|
||||
id = generatedHeroName.requestId,
|
||||
eagleGameId = generatedHeroName.eagleGameId,
|
||||
recipientFactionIds = generatedHeroName.recipientFactionIds,
|
||||
alwaysGenerate = generatedHeroName.alwaysGenerate,
|
||||
details = GeneratedHeroNameMessage(
|
||||
gender = GenderConverter.toProto(generatedHeroName.gender)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
def fromProto(proto: GeneratedTextRequest): GeneratedTextRequestT =
|
||||
proto.details match {
|
||||
case FixedHeroNameMessage(heroId, fixedName, _ /* unknownFields */ ) =>
|
||||
FixedHeroName(
|
||||
requestId = proto.id,
|
||||
eagleGameId = proto.eagleGameId,
|
||||
heroId = heroId,
|
||||
fixedName = fixedName
|
||||
)
|
||||
case GeneratedHeroNameMessage(gender, _ /* unknownFields */ ) =>
|
||||
GeneratedHeroName(
|
||||
requestId = proto.id,
|
||||
eagleGameId = proto.eagleGameId,
|
||||
gender = GenderConverter.fromProto(gender)
|
||||
)
|
||||
case _ =>
|
||||
LlmRequestConverter.fromProto(proto)
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,9 @@ scala_library(
|
||||
"//src/test/scala/net/eagle0/eagle/library/util/command_choice_helpers:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/service:__subpackages__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
|
||||
+646
@@ -14,6 +14,7 @@ import net.eagle0.eagle.internal.generated_text_request.{
|
||||
DivineMessage as DivineMessageProto,
|
||||
ExileVassalMessage as ExileVassalMessageProto,
|
||||
GeneratedTextRequest as LlmRequestProto,
|
||||
GeneratedTextRequestDetails,
|
||||
HandleCapturedHeroPlea as HandleCapturedHeroPleaProto,
|
||||
HeroBackstoryUpdateRequest as HeroBackstoryUpdateRequestProto,
|
||||
HeroDepartureMessage as HeroDepartureMessageProto,
|
||||
@@ -703,4 +704,649 @@ object LlmRequestConverter {
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
def fromProto(
|
||||
llmRequestProto: LlmRequestProto
|
||||
): LlmRequestT = {
|
||||
val requestId = llmRequestProto.id
|
||||
val eagleGameId = llmRequestProto.eagleGameId
|
||||
val recipientFactionIds = llmRequestProto.recipientFactionIds.toVector
|
||||
val alwaysGenerate = llmRequestProto.alwaysGenerate
|
||||
|
||||
llmRequestProto.details match {
|
||||
case AllianceOfferMessageProto(
|
||||
offeringFactionId,
|
||||
targetFactionId,
|
||||
messengerHeroId,
|
||||
_ /* unknownFields */
|
||||
) =>
|
||||
AllianceOfferMessage(
|
||||
requestId = requestId,
|
||||
eagleGameId = eagleGameId,
|
||||
recipientFactionIds = recipientFactionIds,
|
||||
alwaysGenerate = alwaysGenerate,
|
||||
offeringFactionId = offeringFactionId,
|
||||
targetFactionId = targetFactionId,
|
||||
messengerHeroId = messengerHeroId
|
||||
)
|
||||
|
||||
case AllianceOfferResolutionMessageProto(
|
||||
offeringFactionId,
|
||||
targetFactionId,
|
||||
messengerHeroId,
|
||||
resolutionStatus,
|
||||
_ /* unknownFields */
|
||||
) =>
|
||||
AllianceOfferResolutionMessage(
|
||||
requestId = requestId,
|
||||
eagleGameId = eagleGameId,
|
||||
recipientFactionIds = recipientFactionIds,
|
||||
alwaysGenerate = alwaysGenerate,
|
||||
offeringFactionId = offeringFactionId,
|
||||
targetFactionId = targetFactionId,
|
||||
messengerHeroId = messengerHeroId,
|
||||
resolution = StatusConverter.fromProto(resolutionStatus)
|
||||
)
|
||||
|
||||
case BreakAllianceMessageProto(
|
||||
breakingFactionId,
|
||||
targetFactionId,
|
||||
messengerHeroId,
|
||||
_ /* unknownFields */
|
||||
) =>
|
||||
BreakAllianceMessage(
|
||||
requestId = requestId,
|
||||
eagleGameId = eagleGameId,
|
||||
recipientFactionIds = recipientFactionIds,
|
||||
alwaysGenerate = alwaysGenerate,
|
||||
breakingFactionId = breakingFactionId,
|
||||
targetFactionId = targetFactionId,
|
||||
messengerHeroId = messengerHeroId
|
||||
)
|
||||
|
||||
case BreakAllianceResolutionMessageProto(
|
||||
breakingFactionId,
|
||||
targetFactionId,
|
||||
messengerHeroId,
|
||||
resolutionStatus,
|
||||
_ /* unknownFields */
|
||||
) =>
|
||||
BreakAllianceResolutionMessage(
|
||||
requestId = requestId,
|
||||
eagleGameId = eagleGameId,
|
||||
recipientFactionIds = recipientFactionIds,
|
||||
alwaysGenerate = alwaysGenerate,
|
||||
breakingFactionId = breakingFactionId,
|
||||
targetFactionId = targetFactionId,
|
||||
messengerHeroId = messengerHeroId,
|
||||
resolution = StatusConverter.fromProto(resolutionStatus)
|
||||
)
|
||||
|
||||
case CapturedHeroExecutedMessageProto(
|
||||
capturedHeroId,
|
||||
capturedHeroFactionId,
|
||||
actingHeroId,
|
||||
actingFactionId,
|
||||
provinceId,
|
||||
_ /* unknownFields */
|
||||
) =>
|
||||
CapturedHeroExecutedMessage(
|
||||
requestId = requestId,
|
||||
eagleGameId = eagleGameId,
|
||||
recipientFactionIds = recipientFactionIds,
|
||||
alwaysGenerate = alwaysGenerate,
|
||||
capturedHeroId = capturedHeroId,
|
||||
capturedHeroFactionId = capturedHeroFactionId,
|
||||
actingHeroId = actingHeroId,
|
||||
actingFactionId = actingFactionId,
|
||||
provinceId = provinceId
|
||||
)
|
||||
|
||||
case CapturedHeroExiledMessageProto(
|
||||
capturedHeroId,
|
||||
capturedHeroFactionId,
|
||||
actingHeroId,
|
||||
actingFactionId,
|
||||
provinceId,
|
||||
_ /* unknownFields */
|
||||
) =>
|
||||
CapturedHeroExiledMessage(
|
||||
requestId = requestId,
|
||||
eagleGameId = eagleGameId,
|
||||
recipientFactionIds = recipientFactionIds,
|
||||
alwaysGenerate = alwaysGenerate,
|
||||
capturedHeroId = capturedHeroId,
|
||||
capturedHeroFactionId = capturedHeroFactionId,
|
||||
actingHeroId = actingHeroId,
|
||||
actingFactionId = actingFactionId,
|
||||
provinceId = provinceId
|
||||
)
|
||||
|
||||
case CapturedHeroImprisonedMessageProto(
|
||||
capturedHeroId,
|
||||
capturedHeroFactionId,
|
||||
actingHeroId,
|
||||
actingFactionId,
|
||||
provinceId,
|
||||
_ /* unknownFields */
|
||||
) =>
|
||||
CapturedHeroImprisonedMessage(
|
||||
requestId = requestId,
|
||||
eagleGameId = eagleGameId,
|
||||
recipientFactionIds = recipientFactionIds,
|
||||
alwaysGenerate = alwaysGenerate,
|
||||
capturedHeroId = capturedHeroId,
|
||||
capturedHeroFactionId = capturedHeroFactionId,
|
||||
actingHeroId = actingHeroId,
|
||||
actingFactionId = actingFactionId,
|
||||
provinceId = provinceId
|
||||
)
|
||||
|
||||
case ChronicleUpdateMessageProto(
|
||||
currentDate,
|
||||
previousEntries,
|
||||
newEntries,
|
||||
chroniclerStyle,
|
||||
_ /* unknownFields */
|
||||
) =>
|
||||
ChronicleUpdateMessage(
|
||||
requestId = requestId,
|
||||
eagleGameId = eagleGameId,
|
||||
recipientFactionIds = recipientFactionIds,
|
||||
alwaysGenerate = alwaysGenerate,
|
||||
current_date = DateConverter.fromProto(currentDate),
|
||||
previous_entries = previousEntries.map { pe =>
|
||||
ChronicleUpdatePreviousEntry(
|
||||
date = DateConverter.fromProto(pe.date),
|
||||
generatedTextId = pe.generatedTextId,
|
||||
chroniclerStyle =
|
||||
if pe.chroniclerStyle.isEmpty then None
|
||||
else Some(pe.chroniclerStyle)
|
||||
)
|
||||
}.toVector,
|
||||
new_entries = newEntries.map(ChronicleEventConverter.fromProto).toVector,
|
||||
chronicler_style = chroniclerStyle
|
||||
)
|
||||
|
||||
case DivineMessageProto(
|
||||
divinedHeroId,
|
||||
actingHeroId,
|
||||
actingFactionId,
|
||||
provinceId,
|
||||
_ /* unknownFields */
|
||||
) =>
|
||||
DivineMessage(
|
||||
requestId = requestId,
|
||||
eagleGameId = eagleGameId,
|
||||
recipientFactionIds = recipientFactionIds,
|
||||
alwaysGenerate = alwaysGenerate,
|
||||
divinedHeroId = divinedHeroId,
|
||||
actingHeroId = actingHeroId,
|
||||
actingFactionId = actingFactionId,
|
||||
provinceId = provinceId
|
||||
)
|
||||
|
||||
case ExileVassalMessageProto(
|
||||
exiledHeroId,
|
||||
actingHeroId,
|
||||
actingFactionId,
|
||||
provinceId,
|
||||
requestingHeroIds,
|
||||
_ /* unknownFields */
|
||||
) =>
|
||||
ExileVassalMessage(
|
||||
requestId = requestId,
|
||||
eagleGameId = eagleGameId,
|
||||
recipientFactionIds = recipientFactionIds,
|
||||
alwaysGenerate = alwaysGenerate,
|
||||
exiledHeroId = exiledHeroId,
|
||||
actingHeroId = actingHeroId,
|
||||
actingFactionId = actingFactionId,
|
||||
provinceId = provinceId,
|
||||
requestingHeroIds = requestingHeroIds.toVector
|
||||
)
|
||||
|
||||
case HandleCapturedHeroPleaProto(
|
||||
actingFactionId,
|
||||
capturedHeroId,
|
||||
provinceId,
|
||||
alreadyExecutedHeroIds,
|
||||
alreadyImprisonedHeroIds,
|
||||
alreadyExiledHeroIds,
|
||||
_ /* unknownFields */
|
||||
) =>
|
||||
HandleCapturedHeroPlea(
|
||||
requestId = requestId,
|
||||
eagleGameId = eagleGameId,
|
||||
recipientFactionIds = recipientFactionIds,
|
||||
alwaysGenerate = alwaysGenerate,
|
||||
actingFactionId = actingFactionId,
|
||||
capturedHeroId = capturedHeroId,
|
||||
provinceId = provinceId,
|
||||
alreadyExecutedHeroIds = alreadyExecutedHeroIds.toVector,
|
||||
alreadyImprisonedHeroIds = alreadyImprisonedHeroIds.toVector,
|
||||
alreadyExiledHeroIds = alreadyExiledHeroIds.toVector
|
||||
)
|
||||
|
||||
case HeroBackstoryUpdateRequestProto(
|
||||
heroId,
|
||||
previousBackstoryVersions,
|
||||
events,
|
||||
_ /* unknownFields */
|
||||
) =>
|
||||
HeroBackstoryUpdateRequest(
|
||||
requestId = requestId,
|
||||
eagleGameId = eagleGameId,
|
||||
recipientFactionIds = recipientFactionIds,
|
||||
alwaysGenerate = alwaysGenerate,
|
||||
heroId = heroId,
|
||||
previousBackstoryVersions = previousBackstoryVersions.map { bv =>
|
||||
BackstoryVersion(
|
||||
textId = bv.textId,
|
||||
date = DateConverter.fromProto(bv.date)
|
||||
)
|
||||
}.toVector,
|
||||
events = events.map(EventForHeroBackstoryConverter.fromProto).toVector
|
||||
)
|
||||
|
||||
case HeroDepartureMessageProto(
|
||||
departingHeroId,
|
||||
fromFactionId,
|
||||
provinceId,
|
||||
_ /* unknownFields */
|
||||
) =>
|
||||
HeroDepartureMessage(
|
||||
requestId = requestId,
|
||||
eagleGameId = eagleGameId,
|
||||
recipientFactionIds = recipientFactionIds,
|
||||
alwaysGenerate = alwaysGenerate,
|
||||
departingHeroId = departingHeroId,
|
||||
fromFactionId = fromFactionId,
|
||||
provinceId = provinceId
|
||||
)
|
||||
|
||||
case HeroInitialBackstoryRequestProto(
|
||||
heroId,
|
||||
factionId,
|
||||
personalityWords,
|
||||
_ /* unknownFields */
|
||||
) =>
|
||||
HeroInitialBackstoryRequest(
|
||||
requestId = requestId,
|
||||
eagleGameId = eagleGameId,
|
||||
recipientFactionIds = recipientFactionIds,
|
||||
alwaysGenerate = alwaysGenerate,
|
||||
heroId = heroId,
|
||||
factionId = factionId,
|
||||
personalityWords = personalityWords.toVector
|
||||
)
|
||||
|
||||
case InvitationMessageProto(
|
||||
offeringFactionId,
|
||||
targetFactionId,
|
||||
messengerHeroId,
|
||||
_ /* unknownFields */
|
||||
) =>
|
||||
InvitationMessage(
|
||||
requestId = requestId,
|
||||
eagleGameId = eagleGameId,
|
||||
recipientFactionIds = recipientFactionIds,
|
||||
alwaysGenerate = alwaysGenerate,
|
||||
invitingFactionId = offeringFactionId,
|
||||
targetFactionId = targetFactionId,
|
||||
messengerHeroId = messengerHeroId
|
||||
)
|
||||
|
||||
case InvitationResolutionMessageProto(
|
||||
offeringFactionId,
|
||||
targetFactionId,
|
||||
messengerHeroId,
|
||||
resolutionStatus,
|
||||
_ /* unknownFields */
|
||||
) =>
|
||||
InvitationResolutionMessage(
|
||||
requestId = requestId,
|
||||
eagleGameId = eagleGameId,
|
||||
recipientFactionIds = recipientFactionIds,
|
||||
alwaysGenerate = alwaysGenerate,
|
||||
invitingFactionId = offeringFactionId,
|
||||
targetFactionId = targetFactionId,
|
||||
messengerHeroId = messengerHeroId,
|
||||
resolution = StatusConverter.fromProto(resolutionStatus)
|
||||
)
|
||||
|
||||
case PleaseRecruitMeMessageProto(
|
||||
heroId,
|
||||
targetFactionId,
|
||||
provinceId,
|
||||
_ /* unknownFields */
|
||||
) =>
|
||||
PleaseRecruitMeMessage(
|
||||
requestId = requestId,
|
||||
eagleGameId = eagleGameId,
|
||||
recipientFactionIds = recipientFactionIds,
|
||||
alwaysGenerate = alwaysGenerate,
|
||||
heroId = heroId,
|
||||
targetFactionId = targetFactionId,
|
||||
provinceId = provinceId
|
||||
)
|
||||
|
||||
case QuestFailedMessageProto(
|
||||
heroId,
|
||||
provinceId,
|
||||
factionId,
|
||||
quest,
|
||||
_ /* unknownFields */
|
||||
) =>
|
||||
QuestFailedMessage(
|
||||
requestId = requestId,
|
||||
eagleGameId = eagleGameId,
|
||||
recipientFactionIds = recipientFactionIds,
|
||||
alwaysGenerate = alwaysGenerate,
|
||||
heroId = heroId,
|
||||
provinceId = provinceId,
|
||||
factionId = factionId,
|
||||
quest = QuestConverter.fromProto(quest.get)
|
||||
)
|
||||
|
||||
case QuestFulfilledMessageProto(
|
||||
heroId,
|
||||
provinceId,
|
||||
factionId,
|
||||
quest,
|
||||
_ /* unknownFields */
|
||||
) =>
|
||||
QuestFulfilledMessage(
|
||||
requestId = requestId,
|
||||
eagleGameId = eagleGameId,
|
||||
recipientFactionIds = recipientFactionIds,
|
||||
alwaysGenerate = alwaysGenerate,
|
||||
heroId = heroId,
|
||||
provinceId = provinceId,
|
||||
factionId = factionId,
|
||||
quest = QuestConverter.fromProto(quest.get)
|
||||
)
|
||||
|
||||
case RansomOfferMessageProto(
|
||||
offeringFactionId,
|
||||
targetFactionId,
|
||||
offeringHeroId,
|
||||
ransomedHeroId,
|
||||
goldOffered,
|
||||
hostageHeroIdsOffered,
|
||||
prisonerHeroIdsOffered,
|
||||
_ /* unknownFields */
|
||||
) =>
|
||||
RansomOfferMessage(
|
||||
requestId = requestId,
|
||||
eagleGameId = eagleGameId,
|
||||
recipientFactionIds = recipientFactionIds,
|
||||
alwaysGenerate = alwaysGenerate,
|
||||
offeringFactionId = offeringFactionId,
|
||||
targetFactionId = targetFactionId,
|
||||
offeringHeroId = offeringHeroId,
|
||||
ransomedHeroId = ransomedHeroId,
|
||||
goldOffered = goldOffered,
|
||||
hostageHeroIdsOffered = hostageHeroIdsOffered.toVector,
|
||||
prisonerHeroIdsOffered = prisonerHeroIdsOffered.toVector
|
||||
)
|
||||
|
||||
case RansomResolutionMessageProto(
|
||||
offeringFactionId,
|
||||
targetFactionId,
|
||||
offeringHeroId,
|
||||
ransomedHeroId,
|
||||
resolutionStatus,
|
||||
_ /* unknownFields */
|
||||
) =>
|
||||
RansomResolutionMessage(
|
||||
requestId = requestId,
|
||||
eagleGameId = eagleGameId,
|
||||
recipientFactionIds = recipientFactionIds,
|
||||
alwaysGenerate = alwaysGenerate,
|
||||
offeringFactionId = offeringFactionId,
|
||||
targetFactionId = targetFactionId,
|
||||
offeringHeroId = offeringHeroId,
|
||||
ransomedHeroId = ransomedHeroId,
|
||||
resolution = StatusConverter.fromProto(resolutionStatus)
|
||||
)
|
||||
|
||||
case RecruitmentRefusedMessageProto(
|
||||
capturedHeroId,
|
||||
capturedHeroFactionId,
|
||||
actingHeroId,
|
||||
actingFactionId,
|
||||
provinceId,
|
||||
alreadyExecutedHeroIds,
|
||||
alreadyImprisonedHeroIds,
|
||||
alreadyExiledHeroIds,
|
||||
_ /* unknownFields */
|
||||
) =>
|
||||
RecruitmentRefusedMessage(
|
||||
requestId = requestId,
|
||||
eagleGameId = eagleGameId,
|
||||
recipientFactionIds = recipientFactionIds,
|
||||
alwaysGenerate = alwaysGenerate,
|
||||
capturedHeroId = capturedHeroId,
|
||||
capturedHeroFactionId = capturedHeroFactionId,
|
||||
actingHeroId = actingHeroId,
|
||||
actingFactionId = actingFactionId,
|
||||
provinceId = provinceId,
|
||||
alreadyExecutedHeroIds = alreadyExecutedHeroIds.toVector,
|
||||
alreadyImprisonedHeroIds = alreadyImprisonedHeroIds.toVector,
|
||||
alreadyExiledHeroIds = alreadyExiledHeroIds.toVector
|
||||
)
|
||||
|
||||
case SuppressBeastsFailedMessageProto(
|
||||
heroId,
|
||||
factionId,
|
||||
battalionId,
|
||||
beastTypeId,
|
||||
beastCount,
|
||||
provinceId,
|
||||
_ /* unknownFields */
|
||||
) =>
|
||||
SuppressBeastsFailedMessage(
|
||||
requestId = requestId,
|
||||
eagleGameId = eagleGameId,
|
||||
recipientFactionIds = recipientFactionIds,
|
||||
alwaysGenerate = alwaysGenerate,
|
||||
heroId = heroId,
|
||||
factionId = factionId,
|
||||
battalionTypeId = battalionId,
|
||||
beastTypeId = beastTypeId,
|
||||
beastCount = beastCount,
|
||||
provinceId = provinceId
|
||||
)
|
||||
|
||||
case SuppressBeastsSucceededMessageProto(
|
||||
heroId,
|
||||
factionId,
|
||||
battalionId,
|
||||
beastTypeId,
|
||||
beastCount,
|
||||
provinceId,
|
||||
casualtyCount,
|
||||
foodGained,
|
||||
goldGained,
|
||||
_ /* unknownFields */
|
||||
) =>
|
||||
SuppressBeastsSucceededMessage(
|
||||
requestId = requestId,
|
||||
eagleGameId = eagleGameId,
|
||||
recipientFactionIds = recipientFactionIds,
|
||||
alwaysGenerate = alwaysGenerate,
|
||||
heroId = heroId,
|
||||
factionId = factionId,
|
||||
battalionTypeId = battalionId,
|
||||
beastTypeId = beastTypeId,
|
||||
beastCount = beastCount,
|
||||
provinceId = provinceId,
|
||||
casualtyCount = casualtyCount,
|
||||
foodGained = foodGained,
|
||||
goldGained = goldGained
|
||||
)
|
||||
|
||||
case SwearBrotherhoodMessageProto(
|
||||
factionId,
|
||||
swornBrotherHeroId,
|
||||
_ /* unknownFields */
|
||||
) =>
|
||||
SwearBrotherhoodMessage(
|
||||
requestId = requestId,
|
||||
eagleGameId = eagleGameId,
|
||||
recipientFactionIds = recipientFactionIds,
|
||||
alwaysGenerate = alwaysGenerate,
|
||||
factionId = factionId,
|
||||
swornBrotherHeroId = swornBrotherHeroId
|
||||
)
|
||||
|
||||
case TruceOfferMessageProto(
|
||||
offeringFactionId,
|
||||
targetFactionId,
|
||||
messengerHeroId,
|
||||
_ /* unknownFields */
|
||||
) =>
|
||||
TruceOfferMessage(
|
||||
requestId = requestId,
|
||||
eagleGameId = eagleGameId,
|
||||
recipientFactionIds = recipientFactionIds,
|
||||
alwaysGenerate = alwaysGenerate,
|
||||
offeringFactionId = offeringFactionId,
|
||||
targetFactionId = targetFactionId,
|
||||
messengerHeroId = messengerHeroId
|
||||
)
|
||||
|
||||
case TruceResolutionMessageProto(
|
||||
offeringFactionId,
|
||||
targetFactionId,
|
||||
messengerHeroId,
|
||||
resolutionStatus,
|
||||
_ /* unknownFields */
|
||||
) =>
|
||||
TruceResolutionMessage(
|
||||
requestId = requestId,
|
||||
eagleGameId = eagleGameId,
|
||||
recipientFactionIds = recipientFactionIds,
|
||||
alwaysGenerate = alwaysGenerate,
|
||||
offeringFactionId = offeringFactionId,
|
||||
targetFactionId = targetFactionId,
|
||||
messengerHeroId = messengerHeroId,
|
||||
resolution = StatusConverter.fromProto(resolutionStatus)
|
||||
)
|
||||
|
||||
case ProfessionGainedMessageProto(
|
||||
heroId,
|
||||
factionId,
|
||||
newProfession,
|
||||
_ /* unknownFields */
|
||||
) =>
|
||||
ProfessionGainedMessage(
|
||||
requestId = requestId,
|
||||
eagleGameId = eagleGameId,
|
||||
recipientFactionIds = recipientFactionIds,
|
||||
alwaysGenerate = alwaysGenerate,
|
||||
heroId = heroId,
|
||||
factionId = factionId,
|
||||
newProfession = ProfessionConverter.fromProto(newProfession)
|
||||
)
|
||||
|
||||
case PrisonerExecutedMessageProto(
|
||||
executedHeroId,
|
||||
lastFactionId,
|
||||
executingFactionId,
|
||||
provinceId,
|
||||
_ /* unknownFields */
|
||||
) =>
|
||||
PrisonerExecutedMessage(
|
||||
requestId = requestId,
|
||||
eagleGameId = eagleGameId,
|
||||
recipientFactionIds = recipientFactionIds,
|
||||
alwaysGenerate = alwaysGenerate,
|
||||
executedHeroId = executedHeroId,
|
||||
lastFactionId = lastFactionId,
|
||||
executingFactionId = executingFactionId,
|
||||
provinceId = provinceId
|
||||
)
|
||||
|
||||
case PrisonerReleasedMessageProto(
|
||||
releasedHeroId,
|
||||
lastFactionId,
|
||||
releasingFactionId,
|
||||
provinceId,
|
||||
_ /* unknownFields */
|
||||
) =>
|
||||
PrisonerReleasedMessage(
|
||||
requestId = requestId,
|
||||
eagleGameId = eagleGameId,
|
||||
recipientFactionIds = recipientFactionIds,
|
||||
alwaysGenerate = alwaysGenerate,
|
||||
releasedHeroId = releasedHeroId,
|
||||
lastFactionId = lastFactionId,
|
||||
releasingFactionId = releasingFactionId,
|
||||
provinceId = provinceId
|
||||
)
|
||||
|
||||
case PrisonerExiledMessageProto(
|
||||
exiledHeroId,
|
||||
lastFactionId,
|
||||
exilingFactionId,
|
||||
provinceId,
|
||||
_ /* unknownFields */
|
||||
) =>
|
||||
PrisonerExiledMessage(
|
||||
requestId = requestId,
|
||||
eagleGameId = eagleGameId,
|
||||
recipientFactionIds = recipientFactionIds,
|
||||
alwaysGenerate = alwaysGenerate,
|
||||
exiledHeroId = exiledHeroId,
|
||||
lastFactionId = lastFactionId,
|
||||
exilingFactionId = exilingFactionId,
|
||||
provinceId = provinceId
|
||||
)
|
||||
|
||||
case PrisonerReturnedMessageProto(
|
||||
returnedHeroId,
|
||||
lastFactionId,
|
||||
returningFactionId,
|
||||
toFactionId,
|
||||
provinceId,
|
||||
_ /* unknownFields */
|
||||
) =>
|
||||
PrisonerReturnedMessage(
|
||||
requestId = requestId,
|
||||
eagleGameId = eagleGameId,
|
||||
recipientFactionIds = recipientFactionIds,
|
||||
alwaysGenerate = alwaysGenerate,
|
||||
returnedHeroId = returnedHeroId,
|
||||
lastFactionId = lastFactionId,
|
||||
returningFactionId = returningFactionId,
|
||||
toFactionId = toFactionId,
|
||||
provinceId = provinceId
|
||||
)
|
||||
|
||||
case NewFactionHeadMessageProto(
|
||||
newHeadHeroId,
|
||||
factionId,
|
||||
previousFactionName,
|
||||
newFactionName,
|
||||
previousHeadHeroId,
|
||||
_ /* unknownFields */
|
||||
) =>
|
||||
NewFactionHeadMessage(
|
||||
requestId = requestId,
|
||||
eagleGameId = eagleGameId,
|
||||
recipientFactionIds = recipientFactionIds,
|
||||
alwaysGenerate = alwaysGenerate,
|
||||
newHeadHeroId = newHeadHeroId,
|
||||
factionId = factionId,
|
||||
previousFactionName = previousFactionName,
|
||||
newFactionName = if newFactionName.isEmpty then None else Some(newFactionName),
|
||||
previousHeadHeroId = previousHeadHeroId
|
||||
)
|
||||
|
||||
case GeneratedTextRequestDetails.Empty =>
|
||||
throw new IllegalArgumentException("Empty GeneratedTextRequestDetails")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,20 +3,7 @@ load("@rules_scala//scala:scala.bzl", "scala_binary", "scala_library")
|
||||
scala_library(
|
||||
name = "province",
|
||||
srcs = ["ProvinceConverter.scala"],
|
||||
visibility = [
|
||||
"//src/main/scala/net/eagle0/eagle/ai:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/library:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:__subpackages__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:__subpackages__",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/library:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
|
||||
],
|
||||
visibility = ["//visibility:public"],
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province:orders",
|
||||
@@ -77,6 +64,7 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/view_filters:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/view:__subpackages__",
|
||||
"//src/main/scala/net/eagle0/eagle/service/new_game_creation:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
|
||||
],
|
||||
exports = [
|
||||
|
||||
@@ -9,6 +9,9 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:__subpackages__",
|
||||
"//src/main/scala/net/eagle0/eagle/service:__pkg__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:shardok_battle_scala_proto",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/common:victory_condition_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:shardok_battle_scala_proto",
|
||||
|
||||
@@ -52,16 +52,7 @@ scala_library(
|
||||
scala_library(
|
||||
name = "battalion_type",
|
||||
srcs = ["BattalionType.scala"],
|
||||
visibility = [
|
||||
":__subpackages__",
|
||||
"//src/main/scala/net/eagle0/eagle/library:__subpackages__",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:__subpackages__",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/game_state:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/ai:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/library:__subpackages__",
|
||||
],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
":battalion_type_id",
|
||||
],
|
||||
@@ -156,6 +147,7 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/view/game_state:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/game_state:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/service/new_game_creation:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/ai:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/library:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/util:__subpackages__",
|
||||
|
||||
@@ -17,6 +17,7 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/service/new_game_creation:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/shardok_interface:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/ai:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/library:__subpackages__",
|
||||
|
||||
@@ -27,4 +27,6 @@ trait BattalionT {
|
||||
|
||||
def withSize(newSize: Int): BattalionT =
|
||||
updateWith(size = newSize)
|
||||
|
||||
def withId(id: BattalionId): BattalionT
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ scala_library(
|
||||
visibility = [
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions:__subpackages__",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/service/new_game_creation:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/shardok_interface:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/ai:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/actions:__subpackages__",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user