Compare commits

..
Author SHA1 Message Date
adminandClaude Opus 4.5 c7358b2252 Wire up ImproveCommandSelector toggle buttons in Unity
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 13:20:33 -08:00
adminandClaude Opus 4.5 ed96430458 Show unavailable improvement types as grayed-out instead of hidden
Uses CanvasGroup alpha to dim unavailable toggles and sets
interactable=false so they cannot be selected.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 11:20:27 -08:00
adminandClaude Opus 4.5 bc76b402fb Remove unused variable in ImproveCommandSelector
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 11:18:22 -08:00
adminandClaude Opus 4.5 8279a1dc61 Replace Improve command dropdown with toggle buttons
- Replace TMP_Dropdown with 4 toggle buttons (Economy, Agriculture,
  Infrastructure, Devastation) showing type name and current value
- Each toggle shows the improvement type and its current/effective value
- Maintains existing default selection logic (Devastation priority, then
  lowest stat)

Note: Unity prefab changes are needed to wire up the new toggle buttons.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 10:47:30 -08:00
628 changed files with 25896 additions and 52063 deletions
-4
View File
@@ -36,7 +36,3 @@ common --java_language_version=17
common --java_runtime_version=remotejdk_17
common --tool_java_language_version=17
common --tool_java_runtime_version=remotejdk_17
# Workspace status for build stamping (git commit, timestamp)
common --workspace_status_command=tools/workspace_status.sh
common --stamp
-177
View File
@@ -1,177 +0,0 @@
name: Auth Service Build and Deploy
on:
push:
branches: [ "main" ]
paths:
- 'src/main/go/net/eagle0/authservice/**'
- 'src/main/go/net/eagle0/authcli/**'
- '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'
- 'ci/BUILD.bazel'
- '.github/workflows/auth_build.yml'
workflow_dispatch:
inputs:
push_images:
description: 'Push images to container registry'
required: true
default: 'false'
type: boolean
permissions:
contents: read
jobs:
build-auth:
runs-on: self-hosted
outputs:
image_tag: ${{ steps.push-auth.outputs.image_tag }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
lfs: false
- name: Build Auth Server Docker image
id: build-auth
run: |
set -ex
# Build auth server image (Go binary has explicit goos/goarch in BUILD.bazel)
bazel build //ci:auth_server_image
# Save the resolved path before any other bazel command changes bazel-bin symlink
IMAGE_PATH=$(readlink -f bazel-bin/ci/auth_server_image)
echo "Image path: $IMAGE_PATH"
echo "image_path=$IMAGE_PATH" >> $GITHUB_OUTPUT
- name: Login to DigitalOcean Container Registry
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
env:
DO_TOKEN: ${{ secrets.DO_REGISTRY_TOKEN }}
run: |
mkdir -p ~/.docker
AUTH=$(echo -n "${DO_TOKEN}:${DO_TOKEN}" | base64)
echo "{\"auths\":{\"registry.digitalocean.com\":{\"auth\":\"${AUTH}\"}}}" > ~/.docker/config.json
mkdir -p .docker
cp ~/.docker/config.json .docker/
- name: Push Auth image to DO registry
id: push-auth
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
env:
DOCKER_CONFIG: ${{ github.workspace }}/.docker
run: |
set -ex
AUTH_IMAGE="${{ steps.build-auth.outputs.image_path }}"
echo "Using Auth image: $AUTH_IMAGE"
if [ -z "$AUTH_IMAGE" ] || [ ! -d "$AUTH_IMAGE" ]; then
echo "ERROR: Auth image not found at: $AUTH_IMAGE"
exit 1
fi
# Build the push target to get crane in runfiles
bazel build //ci:auth_server_push
# Use crane directly for push
CRANE="bazel-bin/ci/push_auth_server_push.sh.runfiles/rules_oci~~oci~oci_crane_darwin_arm64/crane"
echo "Using crane: $CRANE"
# Push with SHA tag
GIT_SHA=$(git rev-parse --short=8 HEAD)
IMAGE_TAG="registry.digitalocean.com/eagle0/auth-server:${GIT_SHA}"
echo "Pushing auth image: $IMAGE_TAG"
$CRANE push "$AUTH_IMAGE" "$IMAGE_TAG"
# Output the full image tag for deploy step
echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT
# Also update :latest for convenience
echo "Copying to :latest tag"
$CRANE copy "$IMAGE_TAG" "registry.digitalocean.com/eagle0/auth-server:latest"
deploy:
runs-on: ubuntu-latest
needs: [build-auth]
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
environment: production
env:
AUTH_IMAGE: ${{ needs.build-auth.outputs.image_tag }}
DISCORD_CLIENT_ID: ${{ secrets.DISCORD_CLIENT_ID }}
DISCORD_CLIENT_SECRET: ${{ secrets.DISCORD_CLIENT_SECRET }}
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
JWT_PRIVATE_KEY: ${{ secrets.JWT_PRIVATE_KEY }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Deploy auth service to production
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.DO_DROPLET_IP }}
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,JWT_PRIVATE_KEY
script: |
set -x
cd /opt/eagle0
# Update AUTH_IMAGE in .env file (preserve other vars)
if [ -f .env ]; then
# Remove old AUTH_IMAGE line and add new one
grep -v '^AUTH_IMAGE=' .env > .env.tmp || true
echo "AUTH_IMAGE=${AUTH_IMAGE}" >> .env.tmp
# Also update OAuth credentials
grep -v '^DISCORD_CLIENT_ID=' .env.tmp > .env.tmp2 || true
echo "DISCORD_CLIENT_ID=${DISCORD_CLIENT_ID:-}" >> .env.tmp2
grep -v '^DISCORD_CLIENT_SECRET=' .env.tmp2 > .env.tmp3 || true
echo "DISCORD_CLIENT_SECRET=${DISCORD_CLIENT_SECRET:-}" >> .env.tmp3
grep -v '^GOOGLE_CLIENT_ID=' .env.tmp3 > .env.tmp4 || true
echo "GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID:-}" >> .env.tmp4
grep -v '^GOOGLE_CLIENT_SECRET=' .env.tmp4 > .env.tmp5 || true
echo "GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET:-}" >> .env.tmp5
# Update JWT private key (JWK format for auth service bootstrap)
grep -v '^JWT_PRIVATE_KEY=' .env.tmp5 > .env || true
echo "JWT_PRIVATE_KEY=${JWT_PRIVATE_KEY:-}" >> .env
rm -f .env.tmp .env.tmp2 .env.tmp3 .env.tmp4 .env.tmp5
chmod 600 .env
else
echo "ERROR: .env file not found. Run main deploy first."
exit 1
fi
# Login to registry
echo "${{ secrets.DO_REGISTRY_TOKEN }}" | docker login registry.digitalocean.com -u "${{ secrets.DO_REGISTRY_TOKEN }}" --password-stdin
echo "Deploying auth service: $AUTH_IMAGE"
# Use crane to pull image
echo "Installing crane..."
curl -sL https://github.com/google/go-containerregistry/releases/download/v0.20.2/go-containerregistry_Linux_x86_64.tar.gz | tar xzf - crane
chmod +x crane
echo "Pulling Auth image with crane..."
./crane pull "${AUTH_IMAGE}" auth.tar || { echo "ERROR: Failed to pull auth image"; exit 1; }
echo "Loading Auth image into Docker..."
docker load -i auth.tar
rm auth.tar
rm ./crane
# Only recreate the auth container (not eagle, shardok, etc.)
docker compose -f docker-compose.prod.yml up -d --no-deps --force-recreate auth
# Wait for health check
sleep 5
docker compose -f docker-compose.prod.yml ps auth
# Verify container is using correct image
echo "=== Verifying auth container image ==="
docker compose -f docker-compose.prod.yml images auth
# Cleanup old images
docker image prune -f
+4 -75
View File
@@ -8,22 +8,12 @@ on:
required: true
default: 'v2'
type: string
architecture:
description: 'Target architecture'
required: true
default: 'amd64'
type: choice
options:
- amd64
- arm64
- both
permissions:
contents: read
jobs:
build-sysroot-amd64:
if: ${{ inputs.architecture == 'amd64' || inputs.architecture == 'both' }}
build-sysroot:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
@@ -35,7 +25,7 @@ jobs:
- name: Upload sysroot artifact
uses: actions/upload-artifact@v4
with:
name: ubuntu-noble-sysroot-amd64
name: ubuntu-noble-sysroot
path: tools/sysroot/output/
- name: Install AWS CLI
@@ -51,7 +41,7 @@ jobs:
AWS_ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.SECRET_KEY }}
run: |
# Upload sysroot tarball to DO Spaces
# Upload sysroot tarball to DO Spaces (using eagle0-windows bucket, same as other workflows)
aws s3 cp tools/sysroot/output/ubuntu_noble_amd64_sysroot.tar.xz \
s3://eagle0-windows/sysroot/${{ inputs.version }}/ubuntu_noble_amd64_sysroot.tar.xz \
--endpoint-url https://sfo3.digitaloceanspaces.com \
@@ -64,7 +54,7 @@ jobs:
--acl public-read
echo ""
echo "=== AMD64 Sysroot uploaded ==="
echo "=== Sysroot uploaded ==="
echo "URL: https://eagle0-windows.sfo3.digitaloceanspaces.com/sysroot/${{ inputs.version }}/ubuntu_noble_amd64_sysroot.tar.xz"
echo "SHA256: $(cat tools/sysroot/output/ubuntu_noble_amd64_sysroot.sha256)"
echo ""
@@ -74,64 +64,3 @@ jobs:
echo " sha256 = \"$(cat tools/sysroot/output/ubuntu_noble_amd64_sysroot.sha256)\","
echo " urls = [\"https://eagle0-windows.sfo3.digitaloceanspaces.com/sysroot/${{ inputs.version }}/ubuntu_noble_amd64_sysroot.tar.xz\"],"
echo ")"
build-sysroot-arm64:
if: ${{ inputs.architecture == 'arm64' || inputs.architecture == 'both' }}
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up QEMU for ARM64 emulation
uses: docker/setup-qemu-action@v3
with:
platforms: arm64
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build ARM64 sysroot
run: ./tools/sysroot/build_sysroot_arm64.sh
- name: Upload sysroot artifact
uses: actions/upload-artifact@v4
with:
name: ubuntu-noble-sysroot-arm64
path: tools/sysroot/output/
- name: Install AWS CLI
run: |
if ! command -v aws &> /dev/null; then
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip -q awscliv2.zip
sudo ./aws/install
fi
- name: Upload to DigitalOcean Spaces
env:
AWS_ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.SECRET_KEY }}
run: |
# Upload sysroot tarball to DO Spaces
aws s3 cp tools/sysroot/output/ubuntu_noble_arm64_sysroot.tar.xz \
s3://eagle0-windows/sysroot/${{ inputs.version }}/ubuntu_noble_arm64_sysroot.tar.xz \
--endpoint-url https://sfo3.digitaloceanspaces.com \
--acl public-read
# Upload sha256 file
aws s3 cp tools/sysroot/output/ubuntu_noble_arm64_sysroot.sha256 \
s3://eagle0-windows/sysroot/${{ inputs.version }}/ubuntu_noble_arm64_sysroot.sha256 \
--endpoint-url https://sfo3.digitaloceanspaces.com \
--acl public-read
echo ""
echo "=== ARM64 Sysroot uploaded ==="
echo "URL: https://eagle0-windows.sfo3.digitaloceanspaces.com/sysroot/${{ inputs.version }}/ubuntu_noble_arm64_sysroot.tar.xz"
echo "SHA256: $(cat tools/sysroot/output/ubuntu_noble_arm64_sysroot.sha256)"
echo ""
echo "Update MODULE.bazel with:"
echo "sysroot("
echo " name = \"linux_sysroot_arm64\","
echo " sha256 = \"$(cat tools/sysroot/output/ubuntu_noble_arm64_sysroot.sha256)\","
echo " urls = [\"https://eagle0-windows.sfo3.digitaloceanspaces.com/sysroot/${{ inputs.version }}/ubuntu_noble_arm64_sysroot.tar.xz\"],"
echo ")"
+6 -127
View File
@@ -11,8 +11,6 @@ on:
- 'src/main/resources/**'
- 'ci/BUILD.bazel'
- 'MODULE.bazel'
- 'docker-compose.prod.yml'
- 'nginx/**'
- '.github/workflows/docker_build.yml'
workflow_dispatch:
inputs:
@@ -350,99 +348,20 @@ jobs:
echo "Copying to :latest tag"
$CRANE copy "$IMAGE_TAG" "registry.digitalocean.com/eagle0/admin-server:latest"
build-jfr-sidecar:
runs-on: self-hosted
outputs:
image_tag: ${{ steps.push-jfr-sidecar.outputs.image_tag }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
lfs: false
- name: Build JFR Sidecar Docker image
id: build-jfr-sidecar
run: |
set -ex
# Build JFR sidecar image (Go binary has explicit goos/goarch in BUILD.bazel)
bazel build //ci:jfr_sidecar_image
# Save the resolved path before any other bazel command changes bazel-bin symlink
IMAGE_PATH=$(readlink -f bazel-bin/ci/jfr_sidecar_image)
echo "Image path: $IMAGE_PATH"
echo "image_path=$IMAGE_PATH" >> $GITHUB_OUTPUT
- name: Login to DigitalOcean Container Registry
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
env:
DO_TOKEN: ${{ secrets.DO_REGISTRY_TOKEN }}
run: |
mkdir -p ~/.docker
AUTH=$(echo -n "${DO_TOKEN}:${DO_TOKEN}" | base64)
echo "{\"auths\":{\"registry.digitalocean.com\":{\"auth\":\"${AUTH}\"}}}" > ~/.docker/config.json
mkdir -p .docker
cp ~/.docker/config.json .docker/
- name: Push JFR Sidecar image to DO registry
id: push-jfr-sidecar
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
env:
DOCKER_CONFIG: ${{ github.workspace }}/.docker
run: |
set -ex
JFR_IMAGE="${{ steps.build-jfr-sidecar.outputs.image_path }}"
echo "Using JFR Sidecar image: $JFR_IMAGE"
if [ -z "$JFR_IMAGE" ] || [ ! -d "$JFR_IMAGE" ]; then
echo "ERROR: JFR Sidecar image not found at: $JFR_IMAGE"
exit 1
fi
# Build the push target to get crane in runfiles
bazel build //ci:jfr_sidecar_push
# Use crane directly for push
CRANE="bazel-bin/ci/push_jfr_sidecar_push.sh.runfiles/rules_oci~~oci~oci_crane_darwin_arm64/crane"
echo "Using crane: $CRANE"
# Push with SHA tag
GIT_SHA=$(git rev-parse --short=8 HEAD)
IMAGE_TAG="registry.digitalocean.com/eagle0/jfr-sidecar:${GIT_SHA}"
echo "Pushing JFR sidecar image: $IMAGE_TAG"
$CRANE push "$JFR_IMAGE" "$IMAGE_TAG"
# Output the full image tag for deploy step
echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT
# Also update :latest for convenience
echo "Copying to :latest tag"
$CRANE copy "$IMAGE_TAG" "registry.digitalocean.com/eagle0/jfr-sidecar:latest"
deploy:
runs-on: ubuntu-latest
needs: [build-eagle, build-shardok, build-admin, build-jfr-sidecar]
needs: [build-eagle, build-shardok, build-admin]
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
environment: production
env:
EAGLE_IMAGE: ${{ needs.build-eagle.outputs.image_tag }}
SHARDOK_IMAGE: ${{ needs.build-shardok.outputs.image_tag }}
ADMIN_IMAGE: ${{ needs.build-admin.outputs.image_tag }}
JFR_SIDECAR_IMAGE: ${{ needs.build-jfr-sidecar.outputs.image_tag }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
GPT_MODEL_NAME: ${{ secrets.GPT_MODEL_NAME }}
EAGLE_ENABLE_S3: ${{ secrets.EAGLE_ENABLE_S3 }}
DO_SPACES_ACCESS_KEY: ${{ secrets.DO_SPACES_ACCESS_KEY }}
DO_SPACES_SECRET_KEY: ${{ secrets.DO_SPACES_SECRET_KEY }}
JWT_PRIVATE_KEY: ${{ secrets.JWT_PRIVATE_KEY }}
DISCORD_CLIENT_ID: ${{ secrets.DISCORD_CLIENT_ID }}
DISCORD_CLIENT_SECRET: ${{ secrets.DISCORD_CLIENT_SECRET }}
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
SHARDOK_ADDRESS: ${{ secrets.SHARDOK_ADDRESS }}
SHARDOK_AUTH_TOKEN: ${{ secrets.SHARDOK_AUTH_TOKEN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -463,45 +382,19 @@ jobs:
username: deploy
key: ${{ secrets.DO_SSH_KEY }}
script_stop: true
envs: EAGLE_IMAGE,SHARDOK_IMAGE,ADMIN_IMAGE,JFR_SIDECAR_IMAGE,OPENAI_API_KEY,GPT_MODEL_NAME,EAGLE_ENABLE_S3,DO_SPACES_ACCESS_KEY,DO_SPACES_SECRET_KEY,JWT_PRIVATE_KEY,DISCORD_CLIENT_ID,DISCORD_CLIENT_SECRET,GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET,SHARDOK_ADDRESS,SHARDOK_AUTH_TOKEN,SENTRY_DSN
envs: EAGLE_IMAGE,SHARDOK_IMAGE,ADMIN_IMAGE,OPENAI_API_KEY,GPT_MODEL_NAME,EAGLE_ENABLE_S3,DO_SPACES_ACCESS_KEY,DO_SPACES_SECRET_KEY
script: |
set -x
cd /opt/eagle0
# Check Docker has IPv6 support for connecting to Hetzner Shardok
# (One-time setup: sudo tee /etc/docker/daemon.json <<< '{"ipv6": true, "ip6tables": true, "experimental": true, "fixed-cidr-v6": "fd00::/80"}' && sudo systemctl restart docker)
if ! cat /etc/docker/daemon.json 2>/dev/null | grep -q '"ip6tables"'; then
echo "WARNING: Docker IPv6 not configured. Eagle may not reach Hetzner Shardok."
echo "Run: sudo tee /etc/docker/daemon.json <<< '{\"ipv6\": true, \"ip6tables\": true, \"experimental\": true, \"fixed-cidr-v6\": \"fd00::/80\"}' && sudo systemctl restart docker"
fi
# Write env vars to .env file for docker-compose
# Preserve AUTH_IMAGE if it exists (managed by auth_build.yml)
EXISTING_AUTH_IMAGE=""
if [ -f .env ]; then
EXISTING_AUTH_IMAGE=$(grep '^AUTH_IMAGE=' .env | cut -d= -f2- || true)
fi
rm -f .env 2>/dev/null || true
cat > .env << EOF
EAGLE_IMAGE=${EAGLE_IMAGE}
SHARDOK_IMAGE=${SHARDOK_IMAGE}
ADMIN_IMAGE=${ADMIN_IMAGE}
JFR_SIDECAR_IMAGE=${JFR_SIDECAR_IMAGE}
AUTH_IMAGE=${EXISTING_AUTH_IMAGE:-registry.digitalocean.com/eagle0/auth-server:latest}
OPENAI_API_KEY=${OPENAI_API_KEY:-}
GPT_MODEL_NAME=${GPT_MODEL_NAME:-gpt-4o}
EAGLE_ENABLE_S3=${EAGLE_ENABLE_S3:-false}
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:-}
GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID:-}
GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET:-}
SHARDOK_ADDRESS=${SHARDOK_ADDRESS:-shardok:40042}
SHARDOK_AUTH_TOKEN=${SHARDOK_AUTH_TOKEN:-}
SENTRY_DSN=${SENTRY_DSN:-}
EOF
chmod 600 .env
@@ -509,8 +402,7 @@ jobs:
echo "${{ secrets.DO_REGISTRY_TOKEN }}" | docker login registry.digitalocean.com -u "${{ secrets.DO_REGISTRY_TOKEN }}" --password-stdin
# Use exact image tags passed from build jobs (no :latest fallback)
# Note: AUTH_IMAGE is managed separately by auth_build.yml
echo "Using images: $EAGLE_IMAGE, $SHARDOK_IMAGE, $ADMIN_IMAGE, $JFR_SIDECAR_IMAGE"
echo "Using images: $EAGLE_IMAGE, $SHARDOK_IMAGE, $ADMIN_IMAGE"
# Use crane to pull images (handles OCI format correctly) then load into Docker
# This avoids digest mismatch from DO registry's OCI->Docker format conversion
@@ -537,12 +429,6 @@ jobs:
docker load -i admin.tar
rm admin.tar
echo "Pulling JFR Sidecar image with crane..."
./crane pull "${JFR_SIDECAR_IMAGE}" jfr-sidecar.tar || { echo "ERROR: Failed to pull jfr-sidecar image"; exit 1; }
echo "Loading JFR Sidecar image into Docker..."
docker load -i jfr-sidecar.tar
rm jfr-sidecar.tar
rm ./crane
# Also pull other compose images
@@ -551,19 +437,12 @@ jobs:
echo "All images pulled successfully"
# Recreate only the services we're updating (not auth - managed by auth_build.yml)
# This prevents restarting auth service during every Eagle deploy
docker compose -f docker-compose.prod.yml up -d --no-deps --force-recreate eagle shardok admin jfr-sidecar
# Ensure auth is running (but don't force-recreate it)
docker compose -f docker-compose.prod.yml up -d auth
# Force recreate containers to ensure new image is used
docker compose -f docker-compose.prod.yml up -d --force-recreate --remove-orphans
# Restart nginx to pick up new container IPs
# (nginx caches DNS at startup, so it needs restart after eagle/shardok)
docker compose -f docker-compose.prod.yml up -d --force-recreate nginx
# Cleanup orphaned containers
docker compose -f docker-compose.prod.yml up -d --remove-orphans
docker compose -f docker-compose.prod.yml restart nginx
# Wait for health checks
sleep 10
-90
View File
@@ -1,90 +0,0 @@
name: Cleanup Old Container Images
on:
schedule:
# Run daily at 3am UTC
- cron: '0 3 * * *'
workflow_dispatch:
inputs:
dry_run:
description: 'Dry run (show what would be deleted without deleting)'
required: true
default: 'true'
type: boolean
permissions:
contents: read
jobs:
cleanup:
runs-on: ubuntu-latest
steps:
- name: Install doctl
uses: digitalocean/action-doctl@v2
with:
token: ${{ secrets.DO_REGISTRY_TOKEN }}
- name: Cleanup old images
env:
DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.dry_run == 'true' }}
run: |
set -e
RETENTION_DAYS=5
CUTOFF_DATE=$(date -d "-${RETENTION_DAYS} days" +%s)
REGISTRY="eagle0"
echo "Cleaning up images older than ${RETENTION_DAYS} days"
echo "Cutoff date: $(date -d "@${CUTOFF_DATE}" -Iseconds)"
echo "Dry run: ${DRY_RUN}"
echo ""
# List of repositories to clean
REPOS=$(doctl registry repository list-v2 --format Name --no-header)
for REPO in $REPOS; do
echo "=== Processing repository: ${REPO} ==="
# Get all manifests with their tags and dates
MANIFESTS=$(doctl registry repository list-manifests "${REPO}" --format Digest,UpdatedAt,Tags --no-header 2>/dev/null || echo "")
if [ -z "$MANIFESTS" ]; then
echo " No manifests found"
continue
fi
echo "$MANIFESTS" | while read -r DIGEST UPDATED_AT TAGS; do
# Skip if no digest
if [ -z "$DIGEST" ]; then
continue
fi
# Parse the date
MANIFEST_DATE=$(date -d "$UPDATED_AT" +%s 2>/dev/null || echo "0")
# Skip protected tags (latest, arm64-latest)
if echo "$TAGS" | grep -qE '(^|,)(latest|arm64-latest)(,|$)'; then
echo " KEEP: ${DIGEST:0:20}... (protected tag: $TAGS)"
continue
fi
# Check if older than cutoff
if [ "$MANIFEST_DATE" -lt "$CUTOFF_DATE" ]; then
echo " DELETE: ${DIGEST:0:20}... (updated: $UPDATED_AT, tags: $TAGS)"
if [ "$DRY_RUN" != "true" ]; then
doctl registry repository delete-manifest "${REPO}" "$DIGEST" --force
fi
else
echo " KEEP: ${DIGEST:0:20}... (updated: $UPDATED_AT, tags: $TAGS)"
fi
done
echo ""
done
- name: Run garbage collection
if: github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.event.inputs.dry_run == 'false')
run: |
echo "Starting garbage collection..."
doctl registry garbage-collection start --force
echo "Garbage collection started. It may take a few minutes to complete."
-209
View File
@@ -1,209 +0,0 @@
name: Shardok ARM64 Build and Push
on:
push:
branches: [ "main" ]
paths:
- 'src/main/cpp/**'
- 'src/main/protobuf/net/eagle0/shardok/**'
- 'src/main/protobuf/net/eagle0/common/**'
- 'src/main/resources/net/eagle0/shardok/**'
- 'ci/BUILD.bazel'
- 'MODULE.bazel'
- '.github/workflows/shardok_arm64_build.yml'
workflow_dispatch:
inputs:
push_images:
description: 'Push images to container registry'
required: true
default: 'true'
type: boolean
permissions:
contents: read
jobs:
build-shardok-arm64:
runs-on: self-hosted
outputs:
image_tag: ${{ steps.push-shardok.outputs.image_tag }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
lfs: false
- name: Build Shardok ARM64 binary (cross-compile for Linux ARM64)
run: |
set -ex
echo "=== Building shardok-server binary for linux-aarch64 ==="
bazel build \
--platforms=//:linux_arm64 \
--extra_toolchains=@llvm_toolchain_linux_arm64//:all \
//src/main/cpp/net/eagle0/shardok:shardok-server
LINUX_BIN="bazel-bin/src/main/cpp/net/eagle0/shardok/shardok-server"
echo "=== Checking binary at: $LINUX_BIN ==="
if [ ! -f "$LINUX_BIN" ]; then
echo "ERROR: Binary not found at $LINUX_BIN"
exit 1
fi
# Verify it's ELF (Linux) not Mach-O (macOS)
echo "=== Verifying binary format ==="
MAGIC=$(head -c 4 "$LINUX_BIN" | xxd -p)
echo "Binary magic bytes: $MAGIC"
if [ "$MAGIC" = "7f454c46" ]; then
echo "SUCCESS: Binary is ELF format (Linux)"
# Check if it's ARM64 (e_machine = 0xB7 = 183 for aarch64)
E_MACHINE=$(od -An -j18 -N2 -tx2 "$LINUX_BIN" | tr -d ' ')
echo "ELF e_machine: $E_MACHINE"
if [ "$E_MACHINE" = "b700" ]; then
echo "SUCCESS: Binary is ARM64 (aarch64)"
else
echo "WARNING: Binary e_machine is $E_MACHINE (expected b700 for aarch64)"
fi
elif [ "$MAGIC" = "cfaeedfe" ] || [ "$MAGIC" = "cffaedfe" ]; then
echo "ERROR: Binary is Mach-O format (macOS) - cross-compilation failed!"
exit 1
else
echo "WARNING: Unknown binary format: $MAGIC"
file "$LINUX_BIN" || true
fi
- name: Build Shardok ARM64 Docker image
id: build-shardok
run: |
set -ex
bazel build \
--platforms=//:linux_arm64 \
--extra_toolchains=@llvm_toolchain_linux_arm64//:all \
//ci:shardok_server_image_arm64
IMAGE_PATH=$(readlink -f bazel-bin/ci/shardok_server_image_arm64)
echo "Image path: $IMAGE_PATH"
echo "image_path=$IMAGE_PATH" >> $GITHUB_OUTPUT
# Verify the binary inside the tar layer is ARM64 ELF
echo "=== Verifying binary in image tar ==="
BINARY_TAR="bazel-bin/ci/shardok_binary_layer_arm64.tar"
if [ -f "$BINARY_TAR" ]; then
echo "Checking binary in $BINARY_TAR"
MAGIC=$(tar -xOf "$BINARY_TAR" app/shardok-server 2>/dev/null | head -c 4 | xxd -p)
echo "Binary magic in tar: $MAGIC"
if [ "$MAGIC" = "7f454c46" ]; then
echo "SUCCESS: Binary in tar is ELF format (Linux)"
else
echo "ERROR: Binary in tar is NOT ELF format!"
exit 1
fi
else
echo "WARNING: Could not find $BINARY_TAR"
fi
- name: Login to DigitalOcean Container Registry
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
env:
DO_REGISTRY_TOKEN: ${{ secrets.DO_REGISTRY_TOKEN }}
run: |
AUTH=$(echo -n "${DO_REGISTRY_TOKEN}:${DO_REGISTRY_TOKEN}" | base64)
echo "{\"auths\":{\"registry.digitalocean.com\":{\"auth\":\"${AUTH}\"}}}" > ~/.docker/config.json
- name: Push Shardok ARM64 image to DigitalOcean
id: push-shardok
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
run: |
set -ex
CROSS_IMAGE="${{ steps.build-shardok.outputs.image_path }}"
echo "Using cross-compiled image: $CROSS_IMAGE"
if [ -z "$CROSS_IMAGE" ] || [ ! -d "$CROSS_IMAGE" ]; then
echo "ERROR: Cross-compiled image not found at: $CROSS_IMAGE"
exit 1
fi
# Build a push target to get crane in runfiles
bazel build //ci:eagle_server_push
# Find the Darwin crane binary
RUNFILES="bazel-bin/ci/push_eagle_server_push.sh.runfiles"
CRANE=$(find "$RUNFILES" -path "*darwin*" -name crane 2>/dev/null | head -1)
if [ -z "$CRANE" ]; then
CRANE=$(find "$RUNFILES" -name crane 2>/dev/null | head -1)
fi
if [ -z "$CRANE" ] || [ ! -e "$CRANE" ]; then
echo "ERROR: crane not found"
exit 1
fi
echo "Using crane: $CRANE"
# Push with arm64-prefixed SHA tag (same repo as x86, different tag)
GIT_SHA=$(git rev-parse --short=8 HEAD)
IMAGE_TAG="registry.digitalocean.com/eagle0/shardok-server:arm64-${GIT_SHA}"
echo "Pushing shardok ARM64 image: $IMAGE_TAG"
$CRANE push "$CROSS_IMAGE" "$IMAGE_TAG"
echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT
# Also update :arm64-latest tag for convenience
echo "Copying to :arm64-latest tag"
$CRANE copy "$IMAGE_TAG" "registry.digitalocean.com/eagle0/shardok-server:arm64-latest"
echo "=== Push complete ==="
echo "Image: $IMAGE_TAG"
echo "Also tagged as: registry.digitalocean.com/eagle0/shardok-server:arm64-latest"
deploy-hetzner:
runs-on: ubuntu-latest
needs: [build-shardok-arm64]
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
environment: production
env:
SHARDOK_IMAGE: ${{ needs.build-shardok-arm64.outputs.image_tag }}
steps:
- name: Deploy to Hetzner
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.HETZNER_IP }}
username: deploy
key: ${{ secrets.HETZNER_SSH_KEY }}
protocol: tcp6
script_stop: true
envs: SHARDOK_IMAGE
script: |
set -ex
cd /opt/eagle0
# Login to registry
echo "${{ secrets.DO_REGISTRY_TOKEN }}" | docker login registry.digitalocean.com -u "${{ secrets.DO_REGISTRY_TOKEN }}" --password-stdin
echo "Deploying Shardok ARM64: $SHARDOK_IMAGE"
# Pull the new image
docker pull "$SHARDOK_IMAGE"
# Stop and remove existing container
docker stop shardok-ai 2>/dev/null || true
docker rm shardok-ai 2>/dev/null || true
# Run new container
docker run -d \
--name shardok-ai \
--restart unless-stopped \
-p 40042:40042 \
-v /opt/eagle0/data:/data \
"$SHARDOK_IMAGE"
# Wait and verify
sleep 5
docker ps | grep shardok-ai
# Cleanup old images
docker image prune -f
echo "=== Hetzner deployment complete ==="
+7
View File
@@ -35,3 +35,10 @@ repos:
entry: ./scripts/pre-commit-gazelle.sh
files: '(\.go|\.proto|BUILD\.bazel|BUILD|WORKSPACE|WORKSPACE\.bazel|\.bzl)$'
pass_filenames: false
- repo: local
hooks:
- id: update-action-result-types
name: update-action-result-types
language: system
entry: ./scripts/updateActionResultTypes.sh
files: 'src/main/protobuf/net/eagle0/eagle/common/action_result_type.proto'
-9
View File
@@ -12,15 +12,6 @@ platform(
],
)
# Platform for cross-compiling to Linux ARM64
platform(
name = "linux_arm64",
constraint_values = [
"@platforms//os:linux",
"@platforms//cpu:aarch64",
],
)
gazelle(name = "gazelle")
# gazelle:proto file
+10 -135
View File
@@ -8,18 +8,6 @@ This document tracks the migration from protobuf types to native Scala models in
2. **Keep proto for Shardok communication**: Yes - protobuf is used for Eagle-Shardok gRPC communication
3. **Use Scala views inside the library**: Yes - use native Scala types like `ProvinceView`, `FactionView`, `HeroView`, etc. within the library code
## Recent Completed Work
### GameState Round-Trip Elimination (PRs #4913, #4914, #4915)
Eliminated wasteful Scala→proto→Scala conversions in the hot path:
1. **LLM Pipeline** (#4913): `LlmRequestWithGameState` now uses Scala `GameState` instead of proto. All ~38 prompt generators updated to use Scala model types (`FactionT`, `HeroT`, `ProvinceT`).
2. **ActionWithResultingState Caching** (#4914): Added `precomputedScalaState: Option[GameState]` to cache Scala state when available, avoiding `fromProto()` conversion in `stateAfter()`.
3. **PostResults Simplification** (#4915): Changed `PostResults.gameState` from proto to `Option[GameState]` (Scala), eliminating `toProto()` calls when creating PostResults.
## Migration Pattern
The codebase follows a **Legacy* pattern** for separating proto-dependent and protoless code:
@@ -48,7 +36,6 @@ When migrating a file:
- [x] `ExpandCommandSelector`
- [x] `HeroGiftCommandSelector`
- [x] `ImproveCommandSelector`
- [x] `MarchTowardProvinceCommandChooser` - in AI folder, uses native GameState (callers convert)
- [x] `OrganizeCommandSelector`
- [x] `RansomOfferHelpers`
- [x] `SeekMoreLeadersCommandChooser` - in AI folder, uses native GameState
@@ -69,127 +56,17 @@ When migrating a file:
### Fully Protoless
- [x] `AIClientUtils` - has protoless overloads (`takenHeroIdsForMarchTowardFocus`, `mostPowerfulHeroes`)
- [x] `AttackCommandChooser` - uses Scala `GameState` and `BattalionViewC` (Scala view type)
- [x] `BattalionPower` - has `estimatedPower(BattalionViewC)` for recon data with optional stats
- [x] `CommandChoiceHelpers` - fully protoless, uses Scala `GameState` throughout
- [x] `ProvinceGoldSurplusCalculator` - fully protoless (callers use converters)
- [x] `HeroSelector` - fully protoless (removed dead `minimallyFatiguedHeroesProto`)
### AI Layer ✅ COMPLETE
### Blocked (still uses proto GameState)
All AI and command chooser code is now fully protoless:
- [x] `AttackDecisionCommandChooser` - uses Scala GameState
- [x] `CommandChooser` - trait uses Scala GameState
- [x] `FulfillQuestsCommandSelector` - uses Scala GameState
- [x] `MidGameAIClient` - uses Scala GameState internally
### Still Using Proto GameState (Boundary Code)
These files use proto GameState because they're at system boundaries:
**View Filters (client projection):**
- `view_filters/GameStateViewFilter` - has Scala overload, uses Scala sub-filters
- `view_filters/ProvinceViewFilter` - has Scala overloads for some methods
- `view_filters/FactionViewFilter` - has Scala overload
- `view_filters/HeroViewFilter` - has Scala overload
- `view_filters/BattalionNameFilter` - has Scala overload
- `view_filters/BattleFilter` - has Scala overload
**Legacy Utilities (to be deprecated):**
- `LegacyProvinceDistances`, `LegacyFactionUtils`, `LegacyHeroUtils`, etc.
- Used by code that still needs proto GameState
**Persistence/Action System:**
- `ActionResultTApplier`, `ActionResultProtoApplier` - apply results to proto state
- `ActionWithResultingState` - caches both proto and Scala state
**Shardok Interface (gRPC boundary):**
- `ShardokInterfaceGrpcClient`, `ShardokInterfaceProxy` - must use proto for C++ communication
## Next Steps
### Phase 1-3: AI Layer ✅ COMPLETE
The entire AI decision-making layer is now protoless.
### Phase 4: View Filters ✅ COMPLETE
The view_filters package migration is complete:
**Completed:**
- [x] `GameStateViewFilter` - added Scala GameState overload (uses Scala sub-filters)
- [x] `ProvinceViewFilter` - already has Scala overloads for server-side views
- [x] `HumanPlayerClientConnectionState` - updated to pass Scala GameState directly
- [x] `HeroViewFilter` - added Scala overload
- [x] `FactionViewFilter` - added Scala overload
- [x] `Visibility` - added Scala overloads
**Still Using Proto:**
- [x] `BattalionNameFilter` - has Scala overload
- [x] `BattleFilter` - has Scala overload
- [ ] `ActionResultFilter` - uses proto internally (boundary code)
**Strategy:**
1. Add Scala GameState overloads to view filter methods
2. Update callers to pass Scala GameState where available
3. Eventually deprecate proto versions
### Phase 5: Legacy Utility Cleanup (IN PROGRESS)
Remove Legacy* utilities by migrating remaining callers:
1. Identify callers of each Legacy* util
2. Update callers to use protoless versions
3. Delete Legacy* files when no longer needed
**Deleted (no production callers):**
- [x] `LegacyProvinceDistances` - deleted (no callers)
- [x] `LegacyBattalionSuitability` - deleted (no callers)
- [x] `LegacyFoodConsumptionUtils` - deleted (no callers)
- [x] `LegacyHandleRiotUtils` - deleted (no callers)
**Refactored to Thin Wrappers (delegating to protoless versions):**
- [x] `LegacyRansomValidity` - already a thin wrapper delegating to `RansomValidity`
- [x] `LegacyRecruitmentOdds` - refactored to delegate to `RecruitmentOdds`
**Parallel Implementations (proto mirrors protoless):**
- [x] `FactionUtils` / `LegacyFactionUtils` - both have matching APIs; LegacyFactionUtils used by boundary code (24+ callers)
- [x] `HeroUtils` / `LegacyHeroUtils` - both have matching APIs; LegacyHeroUtils used by boundary code (10 callers)
- [x] `ProvinceUtils` / `LegacyProvinceUtils` - both have matching APIs; LegacyProvinceUtils used by boundary code (20 callers: availability factories, view filters)
**Parallel Implementations (awaiting migration of callers):**
- [x] `BattalionUtils` / `LegacyBattalionUtils` - both have matching core methods; LegacyBattalionUtils used by boundary code (4 callers)
- [x] `BattalionViewFilter` / `LegacyBattalionViewFilter` - protoless version exists; Legacy used by view filters, action appliers (3 callers)
- [x] `BattalionTypeFinder` / `LegacyBattalionTypeFinder` - protoless version exists; Legacy used by validators (1 caller: RuntimeValidator)
### Recent Caller Migration
**CheckForFulfilledQuestsAction** - migrated to use protoless `BattalionTypeFinder`:
- Changed `battalionTypes` parameter from proto `Vector[BattalionType]` to Scala `Vector[BattalionType]`
- Updated callers (EngineImpl, EndVassalCommandsPhaseAction) to pass Scala types directly
- Eliminated wasteful `BattalionTypeConverter.toProto()` conversions
**ExpandedUnaffiliatedHeroUtils** - added Scala overload:
- New overload takes Scala `GameState` and `UnaffiliatedHeroT` directly
- Added `UnaffiliatedHeroConverter.unaffiliatedHeroTypeToProto()` helper for efficient enum conversion
- Proto overload retained for backward compatibility
**AvailablePleaseRecruitMeCommandFactory** - eliminated wasteful proto conversions:
- Now uses `ExpandedUnaffiliatedHeroUtils.expandedUnaffiliatedHero(ScalaGameState, UnaffiliatedHeroT)` directly
- Removed `GameStateConverter.toProto()` and `UnaffiliatedHeroConverter.toProto()` calls
- Factory is now fully protoless internally (still returns proto types for API boundary)
**ProvinceViewFilter** - added Scala overload with faction filtering:
- New `filteredProvinceView(province: ProvinceT, gs: ScalaGameState, factionId: FactionId)` overload
- Uses protoless `FactionUtils.hasAlliance`, `Visibility.hasFullVisibility`, and `ProvinceUtils.incomingOthers`
- Handles reconned provinces directly from Scala `FactionT.reconnedProvinces` (already Scala type)
- Added helper methods: `fullProvinceInfoScala`, `maybeIncomingAttackersScala`, `unaffiliatedHeroInfoScala`
- Events still converted to proto at the end (ProvinceView.knownEvents uses proto events)
**GameStateViewFilter** - eliminated GameStateConverter.toProto() call:
- Scala overload now fully protoless internally
- Uses the new ProvinceViewFilter Scala overload with faction filtering
- Converts `battalionTypes` and `chronicleEntries` to proto only at output boundary
- [ ] `CommandChoiceHelpers` - main target, uses proto GameState extensively
- Depends on many Legacy* utils
- [ ] `AttackDecisionCommandChooser` - uses proto types
- [ ] `CommandChooser` - uses proto GameState
- [ ] `FulfillQuestsCommandSelector` - takes proto, converts to native immediately
- Called by `MidGameAIClient` which uses proto GameState
## Key Files
@@ -206,8 +83,6 @@ Remove Legacy* utilities by migrating remaining callers:
## Notes
- The AI client code (`src/main/scala/net/eagle0/eagle/ai/`) is now fully protoless
- Proto GameState is still needed at boundaries: persistence, gRPC to Shardok
- `PerformUnaffiliatedHeroesAction` and the LLM pipeline use protoless `GameState`
- `GameStateViewFilter` Scala overload is now fully protoless internally (converts to proto only at output)
- `ProvinceViewFilter` has Scala overloads for all three modes: no filtering, faction filtering, and withdrawn-from view
- The AI client code (`src/main/scala/net/eagle0/eagle/ai/`) currently uses proto types extensively
- `PerformUnaffiliatedHeroesAction` already uses protoless `GameState`
- Migration should proceed incrementally: utilities first, then higher-level selectors/choosers
+7 -46
View File
@@ -57,47 +57,25 @@ llvm.toolchain(
llvm_version = "20.1.2",
)
# Linux x86_64 sysroot for cross-compilation
# Linux sysroot for cross-compilation (Chromium's Debian sysroot)
llvm.sysroot(
name = "llvm_toolchain_linux",
label = "@linux_sysroot//sysroot",
targets = ["linux-x86_64"],
)
# Cross-compilation toolchain (macOS -> Linux ARM64)
llvm.toolchain(
name = "llvm_toolchain_linux_arm64",
llvm_version = "20.1.2",
)
use_repo(llvm, "llvm_toolchain", "llvm_toolchain_linux")
# Linux ARM64 sysroot for cross-compilation
llvm.sysroot(
name = "llvm_toolchain_linux_arm64",
label = "@linux_sysroot_arm64//sysroot",
targets = ["linux-aarch64"],
)
use_repo(llvm, "llvm_toolchain", "llvm_toolchain_linux", "llvm_toolchain_linux_arm64")
# Download the Linux sysroots (Ubuntu 24.04 Noble for C++23 support)
# Download the Linux sysroot (Ubuntu 24.04 Noble for C++23 support)
# Built by: .github/workflows/build_sysroot.yml
# To rebuild: Run the "Build Linux Sysroot" workflow with a new version, then update sha256 and URL
sysroot = use_repo_rule("@toolchains_llvm//toolchain:sysroot.bzl", "sysroot")
# x86_64 sysroot
sysroot(
name = "linux_sysroot",
sha256 = "a06475004fe8003ae7ccb4fe1d5511feb9b27cce4a8826eb1dfd686ed83f3dba",
urls = ["https://eagle0-windows.sfo3.digitaloceanspaces.com/sysroot/v3/ubuntu_noble_amd64_sysroot.tar.xz"],
)
# ARM64 sysroot
sysroot(
name = "linux_sysroot_arm64",
sha256 = "87469137737e09bc73855007dab835477eb10a7b3ce3f725f93f64e25747f3f9",
urls = ["https://eagle0-windows.sfo3.digitaloceanspaces.com/sysroot/v4/ubuntu_noble_arm64_sysroot.tar.xz"],
)
#
# Language Support - Go
#
@@ -116,8 +94,6 @@ use_repo(
"com_github_aws_aws_sdk_go_v2_config",
"com_github_aws_aws_sdk_go_v2_credentials",
"com_github_aws_aws_sdk_go_v2_service_s3",
"com_github_golang_jwt_jwt_v5",
"com_github_google_uuid",
"org_golang_google_grpc",
"org_golang_google_protobuf",
)
@@ -154,22 +130,19 @@ bazel_dep(name = "aspect_bazel_lib", version = "2.16.0")
oci = use_extension("@rules_oci//oci:extensions.bzl", "oci")
# Base image for Eagle (Java 17 JDK - includes jcmd for JFR dumps)
# Base image for Eagle (Java 17)
oci.pull(
name = "eclipse_temurin_17",
digest = "sha256:d286b5352d98777bbf727f54038b04f0145cd9b76ca83f38a67aa111d4303748",
image = "docker.io/library/eclipse-temurin",
platforms = ["linux/amd64"],
tag = "17-jdk",
)
# Base image for Shardok (Ubuntu 24.04 for C++ runtime)
oci.pull(
name = "ubuntu_24_04",
image = "docker.io/library/ubuntu",
platforms = [
"linux/amd64",
"linux/arm64/v8",
],
platforms = ["linux/amd64"],
tag = "24.04",
)
@@ -180,7 +153,7 @@ oci.pull(
platforms = ["linux/amd64"],
tag = "3.21",
)
use_repo(oci, "alpine_linux", "alpine_linux_linux_amd64", "eclipse_temurin_17", "eclipse_temurin_17_linux_amd64", "ubuntu_24_04", "ubuntu_24_04_linux_amd64", "ubuntu_24_04_linux_arm64_v8")
use_repo(oci, "alpine_linux", "alpine_linux_linux_amd64", "eclipse_temurin_17", "eclipse_temurin_17_linux_amd64", "ubuntu_24_04", "ubuntu_24_04_linux_amd64")
#
# Java/Scala Dependencies
@@ -246,9 +219,6 @@ maven.install(
# JWT (for OAuth token handling)
"com.nimbusds:nimbus-jose-jwt:9.37.3",
# Error tracking
"io.sentry:sentry:7.19.0",
],
duplicate_version_warning = "error",
fail_if_repin_required = True,
@@ -303,14 +273,6 @@ http_file(
executable = True,
)
http_file(
name = "busybox_aarch64",
sha256 = "141adb1b625a6f44c4b114f76b4387b4ea4f7ab802b88eb40e0d2f6adcccb1c3",
urls = ["https://busybox.net/downloads/binaries/1.35.0-aarch64-linux-musl/busybox"],
downloaded_file_path = "busybox",
executable = True,
)
#
# Toolchain Registration
#
@@ -324,6 +286,5 @@ register_toolchains(
register_toolchains(
"@llvm_toolchain//:all",
"@llvm_toolchain_linux//:all",
"@llvm_toolchain_linux_arm64//:all",
dev_dependency = True,
)
+4 -20
View File
@@ -1293,7 +1293,7 @@
"@@rules_oci~//oci:extensions.bzl%oci": {
"general": {
"bzlTransitiveDigest": "FaY+7xb13bB3hmxqwAWaGp3Tf3Q4Nfdlr+F38CP5mcg=",
"usagesDigest": "39yHQmifPoGf+JSYpnQSnJGugxbrFVge/+ENmUiqZ6M=",
"usagesDigest": "KXZUVR9ea29hTmhxC4+BG0pTXTijLpsFrDVraHG4OyU=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
@@ -1306,7 +1306,7 @@
"scheme": "https",
"registry": "index.docker.io",
"repository": "library/eclipse-temurin",
"identifier": "17-jdk",
"identifier": "sha256:d286b5352d98777bbf727f54038b04f0145cd9b76ca83f38a67aa111d4303748",
"platform": "linux/amd64",
"target_name": "eclipse_temurin_17_linux_amd64",
"bazel_tags": []
@@ -1321,7 +1321,7 @@
"scheme": "https",
"registry": "index.docker.io",
"repository": "library/eclipse-temurin",
"identifier": "17-jdk",
"identifier": "sha256:d286b5352d98777bbf727f54038b04f0145cd9b76ca83f38a67aa111d4303748",
"platforms": {
"@@platforms//cpu:x86_64": "@eclipse_temurin_17_linux_amd64"
},
@@ -1343,20 +1343,6 @@
"bazel_tags": []
}
},
"ubuntu_24_04_linux_arm64_v8": {
"bzlFile": "@@rules_oci~//oci/private:pull.bzl",
"ruleClassName": "oci_pull",
"attributes": {
"www_authenticate_challenges": {},
"scheme": "https",
"registry": "index.docker.io",
"repository": "library/ubuntu",
"identifier": "24.04",
"platform": "linux/arm64/v8",
"target_name": "ubuntu_24_04_linux_arm64_v8",
"bazel_tags": []
}
},
"ubuntu_24_04": {
"bzlFile": "@@rules_oci~//oci/private:pull.bzl",
"ruleClassName": "oci_alias",
@@ -1368,8 +1354,7 @@
"repository": "library/ubuntu",
"identifier": "24.04",
"platforms": {
"@@platforms//cpu:x86_64": "@ubuntu_24_04_linux_amd64",
"@@platforms//cpu:arm64": "@ubuntu_24_04_linux_arm64_v8"
"@@platforms//cpu:x86_64": "@ubuntu_24_04_linux_amd64"
},
"bzlmod_repository": "ubuntu_24_04",
"reproducible": true
@@ -1543,7 +1528,6 @@
"eclipse_temurin_17_linux_amd64",
"ubuntu_24_04",
"ubuntu_24_04_linux_amd64",
"ubuntu_24_04_linux_arm64_v8",
"alpine_linux",
"alpine_linux_linux_amd64"
],
+7 -155
View File
@@ -17,18 +17,6 @@ pkg_tar(
},
)
pkg_tar(
name = "busybox_layer_arm64",
srcs = ["@busybox_aarch64//file"],
package_dir = "/usr/local/bin",
remap_paths = {
"file/busybox": "busybox",
},
symlinks = {
"/usr/local/bin/nc": "busybox",
},
)
#
# Eagle Server Docker Image
#
@@ -63,17 +51,19 @@ oci_image(
base = "@eclipse_temurin_17_linux_amd64",
entrypoint = [
"java",
"-Xmx2g",
"-Xmx4g",
"-XX:+UseG1GC",
# JFR profiling support
"-XX:+UnlockDiagnosticVMOptions",
"-XX:+DebugNonSafepoints", # Required for JFR to see through inlined methods
# JFR: Continuous low-overhead profiling (~1% overhead).
# Keeps 1 hour of data in a 100MB circular buffer.
# Dump recording: docker exec eagle-server jcmd 1 JFR.dump filename=/app/jfr/profile.jfr
# Then copy out: docker cp eagle-server:/app/jfr/profile.jfr .
"-XX:StartFlightRecording=disk=true,maxage=1h,maxsize=100m,dumponexit=true,filename=/app/jfr/recording.jfr",
"-XX:FlightRecorderOptions=stackdepth=256",
"-jar",
"/app/eagle_server_deploy.jar",
],
env = {
"JAVA_OPTS": "-Xmx2g -XX:+UseG1GC",
"JAVA_OPTS": "-Xmx4g -XX:+UseG1GC",
},
exposed_ports = ["40032/tcp"],
tars = [
@@ -167,54 +157,6 @@ oci_push(
repository = "registry.digitalocean.com/eagle0/shardok-server",
)
#
# Shardok Server ARM64 Docker Image (for Hetzner on-demand compute)
#
# Build: bazel build //ci:shardok_server_image_arm64 --platforms=//:linux_arm64 --extra_toolchains=@llvm_toolchain_linux_arm64//:all
# Load: bazel run //ci:shardok_server_load_arm64
# Push: bazel run //ci:shardok_server_push_arm64
#
# Package the Shardok binary (ARM64 version - must be built with --platforms=//:linux_arm64)
pkg_tar(
name = "shardok_binary_layer_arm64",
srcs = ["//src/main/cpp/net/eagle0/shardok:shardok-server"],
package_dir = "/app",
)
oci_image(
name = "shardok_server_image_arm64",
base = "@ubuntu_24_04_linux_arm64_v8",
entrypoint = ["/app/shardok-server"],
exposed_ports = [
"40042/tcp",
"40052/tcp",
],
tars = [
# Note: busybox_layer_arm64 omitted - busybox.net has SSL issues
# Health checks can use the shardok-server binary itself or be added later
":shardok_binary_layer_arm64",
":shardok_resources_layer",
":shardok_maps_layer",
],
workdir = "/app",
)
# Load into Docker locally (ARM64): bazel run //ci:shardok_server_load_arm64
oci_load(
name = "shardok_server_load_arm64",
image = ":shardok_server_image_arm64",
repo_tags = ["eagle0/shardok-server:latest-arm64"],
)
# Push to DigitalOcean Container Registry (for Hetzner deployment)
# Uses same repository as x86 but with arm64- tag prefix
oci_push(
name = "shardok_server_push_arm64",
image = ":shardok_server_image_arm64",
repository = "registry.digitalocean.com/eagle0/shardok-server",
)
#
# Admin Server Docker Image (Go)
#
@@ -255,93 +197,3 @@ oci_push(
image = ":admin_server_image",
repository = "registry.digitalocean.com/eagle0/admin-server",
)
#
# JFR Sidecar Docker Image (Go + JDK for jcmd)
#
# This sidecar runs with shared PID namespace to access the Eagle JVM.
# Build: bazel build //ci:jfr_sidecar_image
# Load: bazel run //ci:jfr_sidecar_load
# Push: bazel run //ci:jfr_sidecar_push
#
# Package the Go JFR server binary
pkg_tar(
name = "jfr_sidecar_binary_layer",
srcs = ["//src/main/go/net/eagle0/jfr_server:jfr_server_linux_amd64"],
package_dir = "/app",
)
oci_image(
name = "jfr_sidecar_image",
# Use JDK base image - we need jcmd to dump JFR recordings
base = "@eclipse_temurin_17_linux_amd64",
entrypoint = ["/app/jfr_server_linux_amd64"],
exposed_ports = ["8081/tcp"],
tars = [
":jfr_sidecar_binary_layer",
],
workdir = "/app",
)
# Load into Docker locally: bazel run //ci:jfr_sidecar_load
oci_load(
name = "jfr_sidecar_load",
image = ":jfr_sidecar_image",
repo_tags = ["eagle0/jfr-sidecar:latest"],
)
# Push to DigitalOcean Container Registry
oci_push(
name = "jfr_sidecar_push",
image = ":jfr_sidecar_image",
repository = "registry.digitalocean.com/eagle0/jfr-sidecar",
)
#
# Auth Server Docker Image (Go)
#
# This is the external OAuth service that handles OAuth flows and JWT creation.
# Build: bazel build //ci:auth_server_image
# Load: bazel run //ci:auth_server_load
# Push: bazel run //ci:auth_server_push
#
# Package the Go auth binary (explicit Linux x86_64 target)
pkg_tar(
name = "auth_binary_layer",
srcs = [
"//src/main/go/net/eagle0/authcli:authcli_linux_amd64",
"//src/main/go/net/eagle0/authservice:authservice_linux_amd64",
],
package_dir = "/app",
)
oci_image(
name = "auth_server_image",
base = "@alpine_linux_linux_amd64",
entrypoint = ["/app/authservice_linux_amd64"],
exposed_ports = [
"40033/tcp", # gRPC
"8080/tcp", # HTTP OAuth callback
],
tars = [
":busybox_layer",
":auth_binary_layer",
],
workdir = "/app",
)
# Load into Docker locally: bazel run //ci:auth_server_load
oci_load(
name = "auth_server_load",
image = ":auth_server_image",
repo_tags = ["eagle0/auth-server:latest"],
)
# Push to DigitalOcean Container Registry
oci_push(
name = "auth_server_push",
image = ":auth_server_image",
repository = "registry.digitalocean.com/eagle0/auth-server",
)
+5 -107
View File
@@ -1,7 +1,7 @@
# Docker Compose for production deployment
#
# Local testing:
# Build images: bazel run //ci:eagle_server_load && bazel run //ci:shardok_server_load && bazel run //ci:auth_server_load
# Build images: bazel run //ci:eagle_server_load && bazel run //ci:shardok_server_load
# Run: docker compose -f docker-compose.prod.yml up
#
# Production deployment:
@@ -15,9 +15,7 @@ services:
- "--gpt-model-name"
- "${GPT_MODEL_NAME:-gpt-5.1}"
- "--shardok-interface-remote-address"
- "${SHARDOK_ADDRESS:-shardok:40042}"
- "--auth-service-url"
- "auth:40033"
- "shardok:40042"
ports:
- "40032:40032"
environment:
@@ -26,25 +24,11 @@ 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:-}"
# Auth token for remote Shardok on Hetzner (only used when shardok address contains .eagle0.net)
SHARDOK_AUTH_TOKEN: "${SHARDOK_AUTH_TOKEN:-}"
# Use persistent volume for save data (users, games, etc.)
EAGLE_SAVE_DIR: "/app/saves"
EAGLE_ARCHIVE_DIR: "/app/archived"
SENTRY_DSN: "${SENTRY_DSN:-}"
SENTRY_ENVIRONMENT: "production"
volumes:
- ./saves:/app/saves # Game saves and user database
- ./archived:/app/archived # Archived completed games
- ./saves:/app/saves
- ./jfr:/app/jfr # JFR recordings - dump with: docker exec eagle-server 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
depends_on:
- shardok
- auth
restart: unless-stopped
logging:
driver: "json-file"
@@ -58,51 +42,9 @@ services:
retries: 3
start_period: 30s
auth:
image: ${AUTH_IMAGE:-registry.digitalocean.com/eagle0/auth-server:latest}
container_name: auth-server
environment:
# gRPC port for Auth service
AUTH_GRPC_PORT: "40033"
# HTTP port for OAuth callbacks
AUTH_HTTP_PORT: "8080"
# User data persistence directory
AUTH_DATA_DIR: "/app/data"
# Legacy path for migrating users from Eagle (Phase 1 migration)
AUTH_LEGACY_DATA_DIR: "/app/saves/auth"
# OAuth provider credentials
DISCORD_CLIENT_ID: "${DISCORD_CLIENT_ID:-}"
DISCORD_CLIENT_SECRET: "${DISCORD_CLIENT_SECRET:-}"
GOOGLE_CLIENT_ID: "${GOOGLE_CLIENT_ID:-}"
GOOGLE_CLIENT_SECRET: "${GOOGLE_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
JWT_KEYS_PATH: "/etc/eagle0/keys"
JWT_PRIVATE_KEY: "${JWT_PRIVATE_KEY:-}"
# Note: port 40033 is exposed via nginx, not directly
volumes:
- jwt-keys:/etc/eagle0/keys # Shared JWT keys with Eagle
- ./auth-data:/app/data # User database persistence
- ./saves:/app/saves:ro # Read-only access to Eagle's saves for migration
restart: unless-stopped
logging:
driver: "json-file"
options:
max-size: "50m"
max-file: "3"
healthcheck:
test: ["CMD-SHELL", "nc -z localhost 40033 || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
shardok:
image: ${SHARDOK_IMAGE:-registry.digitalocean.com/eagle0/shardok-server:latest}
container_name: shardok-server
mem_limit: 1g
memswap_limit: 1g # Prevent swap, OOM-kill cleanly instead
ports:
- "40042:40042"
- "40052:40052"
@@ -129,7 +71,6 @@ services:
ports:
- "443:443"
- "80:80"
- "40033:40033" # Go Auth service gRPC (Phase 2 direct client connections)
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./certbot/conf:/etc/letsencrypt:ro
@@ -137,7 +78,6 @@ services:
- ./auth:/etc/nginx/auth:ro
depends_on:
- eagle
- admin
restart: unless-stopped
logging:
driver: "json-file"
@@ -151,17 +91,12 @@ services:
command:
- "--eagle-addr"
- "eagle:40032"
- "--auth-addr"
- "auth:40033"
- "--jfr-sidecar-addr"
- "jfr-sidecar:8081"
- "--http-port"
- "8080"
# No external port - accessed via nginx at admin.eagle0.net
ports:
- "8080:8080"
depends_on:
- eagle
- auth
- jfr-sidecar
restart: unless-stopped
logging:
driver: "json-file"
@@ -175,28 +110,6 @@ services:
retries: 3
start_period: 10s
jfr-sidecar:
image: ${JFR_SIDECAR_IMAGE:-registry.digitalocean.com/eagle0/jfr-sidecar:latest}
container_name: jfr-sidecar
# Share PID namespace with Eagle to access its JVM via jcmd
pid: "service:eagle"
volumes:
- jvm-tmp:/tmp # Shared with Eagle for JVM attach socket files
depends_on:
- eagle
restart: unless-stopped
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "2"
healthcheck:
test: ["CMD-SHELL", "wget -q --spider http://localhost:8081/health || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
certbot:
image: certbot/certbot
container_name: certbot
@@ -204,18 +117,3 @@ services:
- ./certbot/conf:/etc/letsencrypt
- ./certbot/www:/var/www/certbot
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"
volumes:
jvm-tmp:
# Shared /tmp for JVM attach socket files between Eagle and jfr-sidecar
jwt-keys:
# Shared JWT RSA keys between Eagle and auth service
networks:
default:
driver: bridge
enable_ipv6: true
ipam:
config:
- subnet: 172.28.0.0/16
- subnet: fd00:dead:beef::/48
+17 -60
View File
@@ -6,18 +6,10 @@ This document outlines enhancements to the Go admin server (`src/main/go/net/eag
### Current State
The admin server provides a full web UI with htmx interactivity:
- `GET /` - Redirect to games list
- `GET /games` - Game list page (HTML)
- `GET /games/{id}` - Game detail with action history
- `GET /games/{id}/history` - History rows (htmx partial, infinite scroll)
- `GET /games/{id}/action/{index}` - Action detail (htmx partial)
- `POST /games/{id}/rewind` - Rewind game to target action
- `GET /settings` - Settings list with live search
- `POST /settings/update` - Update setting value
- `GET /health` - Health check (JSON)
- `GET /api/games` - JSON API for programmatic access
- `GET /api/games/{id}/history` - JSON API for history
The admin server exposes REST endpoints that return JSON:
- `GET /health` - health check
- `GET /games` - list running games
- `GET /games/{id}/history` - paginated action history
### Goals
@@ -396,59 +388,24 @@ trait GameHistory {
**Deliverable**: View and edit settings via admin UI
### Phase 3: Game Rewind
### Phase 3: Game Rewind (2-3 days)
**Status: Complete**
1. ✅ Add `RewindGame` to `eagle.proto`
2. ✅ Implement `stateAt` and `truncateTo` in `GameHistory`
3. ✅ Implement rewind logic in `Engine` and `GameController`
4. ✅ Add rewind confirmation (htmx `hx-confirm` dialog)
5. ✅ Handle client disconnection gracefully
6. ✅ Add rewind button to history rows
7. ✅ Implement `rewindGame` in `GamesManager` and `EagleServiceImpl`
8. ✅ Add admin server `/games/{id}/rewind` POST handler
9. ✅ Add success/error feedback UI
1. Add `RewindGame` to `eagle.proto`
2. Implement `stateAt` and `truncateTo` in `GameHistory`
3. Implement rewind logic in `GameController`
4. Add rewind confirmation modal
5. Handle client disconnection gracefully
6. Add rewind button to history rows
**Deliverable**: Rewind games to any previous action
### Phase 4: Polish
### Phase 4: Polish (1-2 days)
**Status: Not Started**
#### High Priority
1. **Add tests for rewind functionality**
- `PersistedHistory.truncateTo` (handles complex persisted vs recent logic)
- `InMemoryHistory.truncateTo`
- `EngineImpl.rewindTo`
- `GameController.rewindTo`
- `GamesManager.rewindGame`
2. **Improve action history display**
- Human-readable action type names (e.g., "New Round" instead of "NewRoundAction")
- Show acting faction/province when available
- Action summaries from the `summary` field in `GameHistoryEntry`
#### Medium Priority
3. **Settings improvements**
- Group settings by category prefix (AI*, Combat*, Economy*, etc.)
- Show setting descriptions where available
- Pagination for large settings lists
4. **Error handling improvements**
- Better error messages on failed operations
- Retry logic for transient gRPC failures
#### Low Priority (Nice to Have)
5. **Basic auth** - HTTP Basic Auth or OAuth for production use
6. **Audit logging** - Log admin actions with timestamps
7. **Documentation** - Usage guide, deployment notes
#### Future Considerations
- Game creation from admin UI
- Player management (view connected players, force disconnect)
- Export game history to file
- Metrics/stats dashboard
1. Add loading states and error handling
2. Improve action history display (type icons, summaries)
3. Add settings categories and filtering
4. Add basic auth (optional)
5. Documentation
---
-240
View File
@@ -1,240 +0,0 @@
# Eagle0 Media Asset Audit
This document catalogs all media assets in the Unity project for licensing review.
**Total Assets:** ~12,500 files | **Size:** 1.4 GB
---
## Summary by Category
| Category | Count | Notes |
|----------|-------|-------|
| Images | 10,637 | Mostly PNG icons and UI sprites |
| Audio | 1,778 | 26 music tracks + 1,752 sound effects |
| 3D Models | 52 | Bridge pack only |
| Fonts | 16 | TTF files |
---
## 1. Purchased Asset Store Packages
These are commercial Unity Asset Store purchases tied to your account:
### 4000_Fantasy_Icons
- **Location:** `Assets/4000_Fantasy_Icons/`
- **Size:** 495 MB (5,621 PNG files)
- **Contents:** Icons for armor, weapons, skills, resources
- **License:** Unity Asset Store (check invoice/account)
### GUI Pro Kit Fantasy RPG
- **Location:** `Assets/GUI Pro Kit Fantasy RPG/`
- **Size:** 117 MB (3,755 PNG files)
- **Contents:** UI sprites, animations, prefabs
- **Includes fonts:** Alata-Regular.ttf, JosefinSans-Bold.ttf
- **License:** Unity Asset Store
### Modern UI Pack v4.2.0
- **Location:** `Assets/Modern UI Pack/`
- **Size:** 40 MB (191 PNG files)
- **Author:** Michsky (support@michsky.com)
- **Website:** https://www.michsky.com
- **Includes fonts:** Open Sans family (12 variants)
- **License:** Unity Asset Store
### Pixel Fonts Megapack
- **Location:** `Assets/Pixel Fonts Megapack/`
- **Publisher ID:** 17384
- **Author:** @pixelmush_ on Twitter
- **Asset Store Link:** http://u3d.as/w4v
- **License:** Unity Asset Store
### TileableBridgePack
- **Location:** `Assets/TileableBridgePack/`
- **Size:** 3.1 MB (52 FBX models)
- **Contents:** Bridge construction pieces
- **License:** Unity Asset Store
### Fantasy Interface Sounds
- **Location:** `Assets/Fantasy Interface Sounds/`
- **Count:** 320 WAV files
- **Contents:** UI sounds (bag, book, coins, dice, etc.)
- **License:** Unity Asset Store (verify)
### Medieval Combat Sounds
- **Location:** `Assets/Medieval Combat Sounds/`
- **Count:** 1,072 WAV files
- **Contents:** Footsteps, swings, shields, weapons, magic
- **License:** Unity Asset Store (verify)
### Magic Spells Sound Effects LITE
- **Location:** `Assets/Magic Spells Sound Effects LITE/`
- **Count:** 254 WAV files
- **Contents:** Spell casting, element effects
- **Note:** "LITE" version - check if restrictions apply
- **License:** Unity Asset Store (verify)
---
## 2. Creative Commons Music (Properly Licensed)
**Location:** `Assets/Resources/Music/`
**Documentation:** `Music Credits.txt` (attribution file exists)
All 26 tracks have CC licenses with proper attribution:
| Track | Artist | License |
|-------|--------|---------|
| A Robust Crew | Darren Curtis | CC BY 3.0 |
| Asian Graveyard | Darren Curtis | CC BY 3.0 |
| Fall From Grace | Darren Curtis | CC BY 3.0 |
| Samurai Sake Showdown | Darren Curtis | CC BY 3.0 |
| Deflector | Ghostrifter Official | CC BY-SA 3.0 |
| Chase | Alexander Nakarada | CC BY 4.0 |
| Wintersong | Alexander Nakarada | CC BY 4.0 |
| One Bard Band | Alexander Nakarada | CC BY 4.0 |
| Now We Ride | Alexander Nakarada | CC BY 4.0 |
| The Northern Path | Alexander Nakarada | CC BY 4.0 |
| Victory | MaxKoMusic | CC BY-SA 3.0 |
| Sakuya2 | PeriTune | CC BY 3.0 |
| Under The Sun | Keys of Moon | CC BY 4.0 |
| One Piece of Summer | Keys of Moon | CC BY 4.0 |
| Fluffing a Duck | Kevin MacLeod | CC BY 3.0 |
| Space Jazz | Kevin MacLeod | CC BY 3.0 |
| The Ice Giants | Kevin MacLeod | CC BY 4.0 |
| Epic Cinematic Trailer ELITE | Alex-Productions | CC BY 3.0 |
| Push | Alex-Productions | CC BY 3.0 |
| Virus | Alex-Productions | CC BY 3.0 |
| Duel | Makai Symphony | CC BY-SA 3.0 |
| Dragon Castle | Makai Symphony | CC BY-SA 3.0 |
| Durandal | Makai Symphony | CC BY-SA 3.0 |
**Tracks without specific license (verify):**
- Market Day
- Shopping List
- Medieval: Victory Theme
- Tracks by Dima Koltsov (AUDIUS): No Time for Greatness, Warriors of Demacia, Forest Queen Tale, Valor, Clouds
---
## 3. CC0 / Public Domain Assets
### SimpleFileBrowser Icons
- **Location:** `Assets/Plugins/SimpleFileBrowser/Sprites/FileIcons/`
- **License:** CC0 (documented in LICENSE.txt)
- **Source:** pngrepo.com
- **Items:** Archive, Audio, Default, Drive, Folder, Image, PDF, Text, Video icons
---
## 4. Potentially Problematic Assets (Review Needed)
### Stock Images (Possible License Issues)
These appear to be stock images that may have been used as placeholders:
| File | Concern |
|------|---------|
| ~~`Assets/Eagle/79066358-stock-illustration-raster-illustration-medieval-purse-bag...jpg`~~ | **DELETED** (2025-01-04) |
| ~~`Assets/Images/kisspng-hammer-hand-saws-tool-clip-art...jpg`~~ | **DELETED** (2025-01-04) |
| ~~`Assets/Images/lee-ermy-cropped.jpg`~~ | **DELETED** (2025-01-04) |
| ~~`Assets/Eagle/images.jpeg`~~ | **DELETED** (2025-01-04) |
### Clip Art (Unknown License)
| File | Concern |
|------|---------|
| `Assets/Shardok/commandImages/bridge.png` | Clip art style wooden bridge, unknown source - **needs replacement** |
| `Assets/Images/startFire.png` | Icon, unknown source - **needs verification or replacement** |
### Shardok Sound Effects
- **Location:** `Assets/Shardok/soundEffects/`
- **Count:** 56 MP3 files
- **Contents:** Spell effects, movement, combat sounds
- **Status:** Unknown origin - may be custom or need verification
### Free Icons
- **Location:** `Assets/free_icons/`
- **Count:** 8 PNG weather icons
- **Status:** Verify "free" means commercially usable
### Terrain Hexes
- **Location:** `Assets/Terrain Hexes/`
- **Count:** 85 PNG files
- **Status:** Unknown source - verify licensing
### StrategyGameIcons
- **Location:** `Assets/StrategyGameIcons/`
- **Count:** 138 PNG files
- **Status:** Unknown source - verify licensing
---
## 5. Fonts
| Font | Location | License |
|------|----------|---------|
| Open Sans (12 variants) | Modern UI Pack | Apache 2.0 (Google Font) |
| Alata-Regular | GUI Pro Kit | SIL OFL (Google Font) |
| JosefinSans-Bold | GUI Pro Kit | SIL OFL (Google Font) |
| LiberationSans | TextMesh Pro | SIL OFL |
| NotoColorEmoji | Assets root | SIL OFL (Google) |
| Stoke-Light, Stoke-Regular | Assets root | SIL OFL (Google Font) |
All fonts appear to be open-source Google Fonts or Liberation fonts - should be fine.
---
## 6. Third-Party Code Packages
NuGet packages in `Assets/Packages/` all include LICENSE.TXT files:
- Microsoft.Extensions.* - MIT License
- System.* - MIT License
- Grpc.* - Apache 2.0
---
## Action Items
### Must Verify Before Opening Public Access:
1. ~~**Stock images** - The JPG files with stock image filenames need review.~~ **DONE** - Deleted lee-ermy, kisspng, stock-illustration, Yosemite Sam, and images.jpeg (2025-01-04)
2. **Clip art images** - Unknown license, need replacement with properly licensed alternatives:
- `Assets/Shardok/commandImages/bridge.png` - wooden bridge icon
- `Assets/Images/startFire.png` - fire icon
3. **Shardok sound effects** - 56 MP3 files of unknown origin. Either:
- Document their source
- Replace with known-licensed alternatives
- Confirm they were custom-created
4. **Terrain Hexes** - 85 hex tiles of unknown source
- **TODO:** Confirm if this is a Unity Asset Store purchase (owner believes it is)
5. **StrategyGameIcons** - 138 icons of unknown source
- **TODO:** Investigate origin - check Unity Asset Store purchase history
6. **AUDIUS music tracks** - Verify Dima Koltsov tracks are licensed for commercial use
7. **Discord logo** (`Eagle/Discord-Logo-Blurple.png`) - Likely fine for "Login with Discord" button per Discord brand guidelines, but verify usage complies with their terms
### Already Safe:
- All Asset Store purchases (license tied to your account)
- CC-licensed music (attribution in Music Credits.txt)
- CC0 SimpleFileBrowser icons
- Google Fonts / Liberation fonts
- NuGet packages
---
## Recommendation
Before removing HTTP basic auth:
1. ~~Delete or replace the 4 suspicious JPG/JPEG files in `Assets/Eagle/` and `Assets/Images/`~~ **DONE** (2025-01-04)
2. Replace clip art images (`bridge.png`, `startFire.png`) with properly licensed alternatives (e.g., from [game-icons.net](https://game-icons.net) CC BY 3.0)
3. Verify source of `Assets/Shardok/soundEffects/` MP3s
4. Verify source of `Assets/Terrain Hexes/` and `Assets/StrategyGameIcons/`
5. If any are from early development with unclear licensing, replace them
The bulk of your assets (95%+) are properly licensed Asset Store purchases or CC content.
+31 -53
View File
@@ -190,24 +190,24 @@ Change APIs to vend Scala `GameState` and `ActionResultT` instead of proto versi
| `RandomStateSequencer.scala` | Threads Scala GameState | ✅ **Complete** |
| `VigorXPApplier.scala` | Has both proto and Scala methods | Scala method exists, delete proto method when unused |
| `PerformForcedTurnBackAction.scala` | Fully protoless | ✅ **Complete** |
| `ResolveBattleAction.scala` | ✅ Fully protoless | Complete (PR #5048) |
| `ResolveBattleAction.scala` | Heavy proto usage | Blocked by proto dependencies |
| `InMemoryHistory.scala` | Stores proto results | Pending - vend Scala types |
| `PersistedHistory.scala` | Stores proto results | Pending - vend Scala types, convert for disk |
| `GameController.scala` | Uses proto for client communication | Keep proto (gRPC boundary) |
### Remaining Proto Usage in Actions
**Progress: 52 of 52 action files (100%) are fully protoless.**
**Progress: 47 of 52 action files (90%) are fully protoless.**
All action files have been migrated to use Scala types:
The following 5 actions still have proto usage:
| Action | Status | Notes |
|--------|--------|-------|
| `ResolveBattleAction` | ✅ Complete | PR #5048 - uses Scala GameState and ActionResultApplier |
| `PerformVassalCommandsPhaseAction` | ✅ Complete | Uses Scala types via CommandChoiceHelpers |
| `EndHandleRiotsPhaseAction` | ✅ Complete | Uses Scala types via CommandChoiceHelpers |
| `PerformVassalDefenseDecisionsAction` | ✅ Complete | Uses Scala types via CommandChoiceHelpers |
| `EndVassalCommandsPhaseAction` | ✅ Complete | Uses Scala types via CommandChoiceHelpers |
| Action | Proto Usages | Blocker | Effort |
|--------|--------------|---------|--------|
| `ResolveBattleAction` | 24 | Shardok interface, complex battle logic | High |
| `PerformVassalCommandsPhaseAction` | 3 | `CommandChoiceHelpers` takes proto GameState | Medium |
| `EndHandleRiotsPhaseAction` | 2 | `CommandChoiceHelpers` takes proto GameState | Medium |
| `PerformVassalDefenseDecisionsAction` | 2 | `CommandChoiceHelpers` takes proto GameState | Medium |
| `EndVassalCommandsPhaseAction` | 1 | `CommandChoiceHelpers` takes proto GameState | Medium |
**Note:** `NewRoundAction` is now fully protoless after converting `ChronicleEventGenerator` to return Scala `ChronicleEvent` types directly.
@@ -223,28 +223,13 @@ All action files have been migrated to use Scala types:
| Component | Lines | Complexity | Blocks |
|-----------|-------|------------|--------|
| `CommandChoiceHelpers` to Scala | ~2000 | High | 4 vassal actions |
| `ResolveBattleAction` refactor | ~500 | High | 1 action (complex) |
| History API updates | ~100 | Low | - |
| **Total Remaining** | **~100** | | |
| **Total Remaining** | **~2600** | | |
**Completed:**
- `ChronicleEventGenerator` converted to return Scala `ChronicleEvent` types directly
-`CommandChoiceHelpers` migrated to Scala types
-`ResolveBattleAction` refactored to use Scala GameState and ActionResultApplier (PR #5048)
### Enum Type Migrations
Proto enums are being converted to Scala sealed traits with converters at boundaries:
| Enum | Scala Type | Status | Notes |
|------|------------|--------|-------|
| `DiplomacyOfferStatus` | `Status` sealed trait | ✅ **Complete** | PR #5093 - `EligibleDiplomacyStatuses` uses Scala types internally |
| `RoundPhase` | `RoundPhase` sealed trait | Partial | Some usages converted |
| `BattalionType` | `BattalionType` sealed trait | Partial | Some usages converted |
**DiplomacyOfferStatus Migration (PR #5093):**
- `EligibleDiplomacyStatuses.scala` now returns `Vector[Status]` instead of `Vector[DiplomacyOfferStatus]`
- Call sites in `AvailableResolve*CommandFactory` files convert to proto via `StatusConverter.toProto` at the boundary
- This pattern should be applied to other proto enums
- `ChronicleEventGenerator` converted to return Scala `ChronicleEvent` types directly
### CommandChoiceHelpers Migration Status
@@ -261,34 +246,28 @@ Several command selectors have already been converted to use Scala types:
| `ImproveCommandSelector.scala` | ✅ **Protoless** | Uses Scala `GameState`, `ProvinceT`, `HeroT` |
| `OrganizeCommandSelector.scala` | ✅ **Protoless** | Uses Scala `GameState`, `BattalionT`, `BattalionType` |
| `RansomOfferHelpers.scala` | ✅ **Protoless** | Uses Scala `GameState`, `FactionT` |
| `CommandChoiceHelpers.scala` | **Protoless** | Uses Scala `GameState` throughout |
| `ProvinceGoldSurplusCalculator.scala` | **Protoless** | Uses Scala types |
| `CommandChoiceHelpers.scala` | ❌ Proto | Main entry point, converts to Scala when calling converted selectors |
| `ProvinceGoldSurplusCalculator.scala` | **Partial** | Has both Scala and proto overloads |
| Other selectors | ❌ Proto | Various proto dependencies |
**All CommandChoiceHelpers selectors have been migrated to Scala types.**
**Pattern**: `CommandChoiceHelpers` currently uses `GameStateConverter.fromProto(gameState)` when calling already-converted selectors like `AlmsCommandSelector` and `AttackCommandChooser`. This allows incremental migration.
**Next Steps**:
1. ~~Convert `ExpandCommandSelector` to Scala types~~ ✅ Done
2. ~~Convert `ImproveCommandSelector` to Scala types~~ ✅ Done
3. ~~Convert `OrganizeCommandSelector` to Scala types~~ ✅ Done (PR #4812)
4. ~~Convert `RansomOfferHelpers` to Scala types~~ ✅ Done (PR #4821)
5. Convert remaining selectors one at a time
6. Update `CommandChoiceHelpers` to accept Scala `GameState` once all selectors are converted
### Progress Summary
| Metric | Value |
|--------|-------|
| Action files fully protoless | 52 / 52 (100%) |
| Proto usages in remaining actions | 0 |
| Next target | See "Next Candidates" section below |
### Next Candidates
Priority candidates for further deproto work:
1. **More Enum Migrations** - Apply the `DiplomacyOfferStatus` pattern to other proto enums:
- Files importing `net.eagle0.eagle.common.round_phase.RoundPhase` (proto) could use Scala `RoundPhase`
- Files importing `net.eagle0.eagle.common.battalion_type.BattalionType` (proto) could use Scala `BattalionType`
2. **AvailableCommandsFactory Files** - Many still use proto `GameState` internally:
- These files build proto `AvailableCommand` messages but could use Scala types for internal logic
- Convert to accept Scala `GameState`, only convert fields to proto when building the response
3. **History APIs** - `InMemoryHistory` and `PersistedHistory`:
- Change to vend Scala `GameState` and `ActionResultT` instead of proto versions
- `PersistedHistory` converts to proto internally for disk persistence
| Action files fully protoless | 47 / 52 (90%) |
| Proto usages in remaining actions | 32 total |
| Biggest blocker | `ResolveBattleAction` (24 usages) |
| Second biggest blocker | `CommandChoiceHelpers` (blocks 4 actions) |
### Validation
- [x] `ActionResultApplier` created and tested
@@ -297,9 +276,8 @@ Priority candidates for further deproto work:
- [x] `ProvinceViewFilter` has Scala overload for server-side use (PR #4752)
- [x] `FactionT.reconnedProvinces` and `ChangedFactionC.updatedReconnedProvinces` use Scala `ProvinceView`
- [x] `ProvinceViewFilter.withdrawnFromProvinceView` has Scala overload
- [x] `CommandChoiceHelpers` uses Scala types ✅
- [x] All action files (52/52) are fully protoless ✅
- [ ] `ProvinceViewFilter` faction-filtered views use Scala types
- [ ] `CommandChoiceHelpers` uses Scala types
- [ ] History APIs vend Scala types
- [ ] No `ActionResultProtoConverter.toProto()` calls except at persistence/gRPC boundaries
- [ ] All tests pass
-159
View File
@@ -1,159 +0,0 @@
# Hetzner Setup Guide
This guide walks through setting up Hetzner Cloud infrastructure for running Shardok on-demand compute.
## Prerequisites
- All code PRs merged (#4990, #4996, #4998, #5001, #5009)
- Access to DigitalOcean Container Registry (for pulling Shardok ARM64 image)
---
## Step 1: Create Hetzner Cloud Account
1. Go to https://console.hetzner.cloud/
2. Sign up and add payment method
3. Create a new project (e.g., "eagle0")
---
## Step 2: Generate Hetzner API Token
1. In Hetzner Console → Security → API Tokens
2. Click "Generate API Token"
3. Give it **Read & Write** permissions
4. Copy the token (you'll only see it once)
---
## Step 3: Generate Shardok Auth Token
Generate a 256-bit random token for Eagle-Shardok authentication:
```bash
openssl rand -hex 32
```
Save this output - it's the shared secret between Eagle and Shardok.
---
## Step 4: Store Secrets in GitHub Actions
Add these secrets in GitHub → Settings → Secrets and variables → Actions:
| Secret Name | Description |
|-------------|-------------|
| `HETZNER_API_TOKEN` | From Step 2 - for Hetzner API calls |
| `SHARDOK_AUTH_TOKEN` | From Step 3 - shared secret for gRPC auth |
Note: `DO_REGISTRY_TOKEN` already exists and will be used for Hetzner to pull container images.
These secrets will be passed to Eagle at runtime via `docker_build.yml`, similar to how `OPENAI_API_KEY` and other secrets are handled.
---
## Step 5: DNS Setup (for Let's Encrypt)
You need a domain pointing to the Shardok instance for TLS certificates.
### Option A: Floating IP (Recommended)
1. In Hetzner Console → Networking → Floating IPs
2. Create a **Floating IPv6** in **Hillsboro, Oregon (hil)** region
- IPv6 costs €1/month vs €3/month for IPv4
- Hillsboro has better latency to DigitalOcean SFO than Ashburn
- Server-to-server communication works fine with IPv6-only
3. Point `shardok.prod.eagle0.net` to this IP via AAAA record
4. The ShardokInstanceManager will attach this IP to instances on spin-up
**Location choice**: Hillsboro, OR (`hil`) is recommended for US West Coast. Same pricing as Ashburn (`ash`).
### Option B: Dynamic DNS
Update DNS programmatically when instance spins up. More complex but avoids floating IP cost.
---
## Step 6: Upload SSH Key to Hetzner
For debugging access to instances:
1. In Hetzner Console → Security → SSH Keys
2. Click "Add SSH Key"
3. Paste your public key (e.g., `~/.ssh/id_rsa.pub`)
4. Give it a name (e.g., "eagle-deploy")
---
## Step 7: Wire Security Config into Eagle
Update Eagle's startup code to use the security config when connecting to remote Shardok:
```scala
val securityConfig = ShardokSecurityConfig(
useTls = true,
authToken = Some(sys.env("SHARDOK_AUTH_TOKEN"))
)
val channel = ServerSetupHelpers.newChannel(
"shardok.prod.eagle0.net",
50051,
securityConfig
)
```
---
## Testing
### Manual Instance Spin-up
Test the Hetzner integration by triggering instance creation:
```scala
val manager = new ShardokInstanceManager(
hetznerApiToken = sys.env("HETZNER_API_TOKEN"),
// ... other config
)
manager.ensureInstanceRunning()
```
### Verify TLS and Auth
1. Instance spins up and gets Let's Encrypt certificate
2. Eagle connects via TLS
3. Auth token is validated on each request
---
## Cost Estimate
| Component | Cost |
|-----------|------|
| CAX41 (16 ARM cores) | ~$0.04/hour |
| Floating IP | ~$4/month |
| Typical usage (20 hrs/week) | ~$3.50/month compute |
**Total: ~$7-8/month** for typical usage.
---
## Troubleshooting
### Instance won't start
- Check Hetzner API token has Read & Write permissions
- Verify you're using the correct region (`hil` for Hillsboro OR, or `ash` for Ashburn VA)
### TLS certificate fails
- Ensure DNS points to the instance IP before certbot runs
- Check port 80 is open for Let's Encrypt HTTP-01 challenge
### Auth failures
- Verify `SHARDOK_AUTH_TOKEN` matches on both Eagle and Shardok
- Check the token file is readable by Shardok container
### Can't pull container image
- Ensure `DO_REGISTRY_TOKEN` is passed to cloud-init
- Verify the ARM64 image exists: `registry.digitalocean.com/eagle0/shardok-server:arm64-latest`
-383
View File
@@ -1,383 +0,0 @@
# Plan: Extract OAuth to Go Service
## Goal
Move OAuth authentication handling from the Eagle Scala server into a separate Go service. This simplifies Eagle (gRPC-only, no HTTP), reduces complexity, and sets up for potentially moving JWT validation outside Eagle too.
## Architecture Decision: Sidecar Service (Not DO Functions)
**Recommendation: Go sidecar service on the same droplet, in a separate container**
**Why not DO Functions:**
- OAuth requires **stateful sessions** (pendingOAuth/completedOAuth maps with 10-min TTL)
- Client polling pattern (every 2 seconds) would incur high function invocation costs
- Cold start latency problematic for auth flows
- State would require external store (Redis), adding complexity
**Why sidecar (separate container):**
- Simple process on same droplet, minimal network latency
- In-memory state management (like current Scala impl)
- Easy to monitor/debug alongside Eagle
- Can share filesystem for key files (RSA keys) via volume mounts
- **Independent deployment**: Deploying Eagle doesn't restart auth service (and vice versa)
- **Independent scaling**: Could move to separate droplet later if needed
## Current Architecture (What Exists)
```
Unity Client
├── GetOAuthUrl RPC → Eagle AuthServiceImpl → OAuthService.getAuthUrl()
├── [User browser auth] → HTTP callback → OAuthHttpHandler → OAuthService.handleCallback()
├── CheckOAuthStatus RPC (polling) → AuthServiceImpl → OAuthService.checkStatus()
└── All other RPCs include JWT → AuthorizationInterceptor validates
```
**Key files:**
- `src/main/scala/net/eagle0/eagle/auth/OAuthService.scala` - OAuth flow, state management
- `src/main/scala/net/eagle0/eagle/auth/JwtService.scala` - JWT creation/validation
- `src/main/scala/net/eagle0/eagle/auth/UserService.scala` - User CRUD (persisted)
- `src/main/scala/net/eagle0/eagle/service/AuthServiceImpl.scala` - gRPC Auth service
- `src/main/scala/net/eagle0/eagle/service/OAuthHttpHandler.scala` - HTTP callback handler
## Target Architecture (Phase 1)
```
Unity Client
├── GetOAuthUrl RPC ──────────────┐
├── CheckOAuthStatus RPC (polling)├──→ Eagle (port 40032) ──proxy──→ Go Auth Container (port 40033)
├── RefreshToken RPC ─────────────┘ │
├── [User browser] → HTTP callback ────────────────────────────────────────┤
│ ↓
│ (Internal gRPC: GetOrCreateUser, GetUser)
│ ↓
└── Game RPCs with JWT ─────────────────────→ Eagle (port 40032) ← JWT validation stays here
[Same Droplet]
┌─────────────────────────────────────────────────────────────────────────────┐
│ │
│ ┌──────────────────────┐ ┌────────────────────────────────────┐ │
│ │ Go Auth Container │◄────────►│ Eagle Container │ │
│ │ (eagle0-auth) │ internal │ (eagle0-server) │ │
│ │ │ gRPC │ │ │
│ │ - OAuth flow │ │ - JWT validation │ │
│ │ - JWT creation │ │ - UserService (persistence) │ │
│ │ - HTTP callback │ │ - Game logic │ │
│ └──────────────────────┘ └────────────────────────────────────┘ │
│ │ │ │
│ └────────────────┬───────────────────────┘ │
│ ▼ │
│ /etc/eagle0/keys/ (shared volume) │
│ - private.pem │
│ - public.pem │
└─────────────────────────────────────────────────────────────────────────────┘
```
## Component Responsibilities
### Go Auth Service (NEW - separate container)
- **OAuth flow**: getAuthUrl, handleCallback (HTTP), checkStatus
- **State management**: pendingOAuth, completedOAuth maps with TTL
- **JWT creation**: Issue access/refresh tokens (shares RSA private key with Eagle)
- **Token refresh**: Validate refresh token, issue new access token
- Calls Eagle's internal UserService gRPC to find/create users
### Eagle Server (SIMPLIFIED)
- **JWT validation**: AuthorizationInterceptor stays (validates tokens on game RPCs)
- **UserService**: Stays in Eagle (user persistence, display name logic)
- **New internal gRPC**: Expose GetOrCreateUser, GetUser for Go service to call
- **Proxy (Phase 1)**: Forward OAuth RPCs to Go service
- **Remove (Phase 2)**: OAuthService, OAuthHttpHandler, HTTP server setup
### Unity Client (NO CHANGES in Phase 1)
- Eagle proxies Auth RPCs to Go service
- Client still connects to Eagle on port 40032
## Implementation Phases
### Phase 1: Go Auth Service with Eagle Proxy (Zero Client Changes)
1. **Create Go service structure**
```
src/main/go/net/eagle0/authservice/
├── main.go # Entry point, starts gRPC + HTTP servers
├── oauth.go # OAuth state management, provider configs
├── jwt.go # JWT creation (copy logic from Scala)
├── handlers.go # gRPC handlers for Auth service
├── http_callback.go # HTTP handler for OAuth callback
└── BUILD.bazel
```
2. **Internal gRPC proto for Eagle UserService**
```protobuf
// src/main/protobuf/net/eagle0/eagle/internal/auth_internal.proto
service InternalUserService {
rpc GetOrCreateUser(GetOrCreateUserRequest) returns (GetOrCreateUserResponse);
rpc GetUser(GetUserRequest) returns (GetUserResponse);
}
message GetOrCreateUserRequest {
string provider = 1; // "discord" or "google"
string provider_user_id = 2;
string email = 3;
string avatar_url = 4;
}
message GetOrCreateUserResponse {
string user_id = 1;
string display_name = 2;
string avatar_url = 3;
bool is_admin = 4;
bool is_new_user = 5;
}
```
3. **Eagle: Expose InternalUserService**
- New `InternalUserServiceImpl.scala` wrapping UserService
- Bind to same port, different service name (internal only)
4. **Eagle: Proxy Auth RPCs to Go**
- AuthServiceImpl delegates GetOAuthUrl, CheckOAuthStatus, RefreshToken to Go service
- SetDisplayName, GetCurrentUser, Logout stay in Eagle
5. **Share RSA keys via volume mount**
- Go service reads same key files as Eagle
- Both can create valid JWTs
- Eagle continues to validate JWTs
6. **Docker/Container setup**
- New Dockerfile for Go auth service
- docker-compose or Kubernetes config for both containers
- Shared volume for /etc/eagle0/keys/
- Internal network for container-to-container gRPC
### Phase 2: Client Direct to Go Service (Future)
1. **Update Unity client**
- Connect to Go Auth service directly for OAuth RPCs
- Keep connecting to Eagle for game RPCs
2. **Remove Eagle proxy code**
- Delete AuthServiceImpl OAuth delegation
- AuthServiceImpl only handles SetDisplayName, GetCurrentUser, Logout
### Phase 3: Move JWT Validation to Go (Optional Future)
1. **Go service validates JWTs**
- Add ValidateToken RPC or use shared middleware pattern
2. **Eagle calls Go for validation**
- AuthorizationInterceptor calls Go to validate tokens
- OR: Use stateless validation (both share public key)
## Files to Create
### Go Service
- `src/main/go/net/eagle0/authservice/main.go`
- `src/main/go/net/eagle0/authservice/oauth.go`
- `src/main/go/net/eagle0/authservice/jwt.go`
- `src/main/go/net/eagle0/authservice/handlers.go`
- `src/main/go/net/eagle0/authservice/http_callback.go`
- `src/main/go/net/eagle0/authservice/BUILD.bazel`
### Protos
- `src/main/protobuf/net/eagle0/eagle/internal/auth_internal.proto`
### Scala
- `src/main/scala/net/eagle0/eagle/service/InternalUserServiceImpl.scala`
### Docker/Deployment
- `ci/auth_service.Dockerfile`
- Update `docker-compose.yml` (or equivalent)
## Files to Modify
### Scala (Phase 1)
- `src/main/scala/net/eagle0/eagle/service/AuthServiceImpl.scala` - Proxy OAuth RPCs to Go
- `src/main/scala/net/eagle0/eagle/Main.scala` - Start internal user service, add auth-service-url flag
### Scala (Phase 2 - Removal)
- Delete `src/main/scala/net/eagle0/eagle/auth/OAuthService.scala`
- Delete `src/main/scala/net/eagle0/eagle/service/OAuthHttpHandler.scala`
- Simplify `src/main/scala/net/eagle0/eagle/Main.scala` - Remove HTTP server
### Unity (Phase 2)
- `Assets/Auth/OAuthManager.cs` - Point OAuth RPCs to Go service port
- `Assets/EagleConnection.cs` - Add second channel for auth service
## Key Implementation Details
### State Management in Go
```go
type OAuthState struct {
Provider string
CreatedAt time.Time
}
type OAuthResult struct {
Success bool
UserInfo *ProviderUserInfo
Provider string
Error string
}
var pendingOAuth = sync.Map{} // state -> OAuthState
var completedOAuth = sync.Map{} // state -> OAuthResult
const stateExpiration = 10 * time.Minute
// Background goroutine cleans expired states every minute
func cleanupExpiredStates() {
ticker := time.NewTicker(1 * time.Minute)
for range ticker.C {
cutoff := time.Now().Add(-stateExpiration)
pendingOAuth.Range(func(key, value any) bool {
if value.(OAuthState).CreatedAt.Before(cutoff) {
pendingOAuth.Delete(key)
}
return true
})
// Similar for completedOAuth
}
}
```
### JWT Creation in Go
```go
import "github.com/golang-jwt/jwt/v5"
type EagleClaims struct {
jwt.RegisteredClaims
UserId string `json:"userId"`
DisplayName string `json:"displayName"`
IsAdmin bool `json:"isAdmin"`
}
func CreateAccessToken(userId, displayName string, isAdmin bool) (string, error) {
claims := EagleClaims{
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(7 * 24 * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
UserId: userId,
DisplayName: displayName,
IsAdmin: isAdmin,
}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
return token.SignedString(privateKey)
}
```
### OAuth Provider Configs
- Read from environment variables (same as current OAuthConfig.scala)
- DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET
- GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET
- OAUTH_CALLBACK_URL (e.g., https://eagle0.shardok.games/oauth/callback)
### Container Networking
```yaml
# docker-compose.yml example
services:
eagle0-auth:
build:
context: .
dockerfile: ci/auth_service.Dockerfile
ports:
- "40033:40033" # gRPC
- "8080:8080" # HTTP callback
volumes:
- ./keys:/etc/eagle0/keys:ro
environment:
- DISCORD_CLIENT_ID
- DISCORD_CLIENT_SECRET
- GOOGLE_CLIENT_ID
- GOOGLE_CLIENT_SECRET
- EAGLE_INTERNAL_URL=eagle0-server:40034
eagle0-server:
build:
context: .
dockerfile: ci/eagle_run.Dockerfile
ports:
- "40032:40032" # Public gRPC
expose:
- "40034" # Internal gRPC (container-to-container only)
volumes:
- ./keys:/etc/eagle0/keys:ro
- ./data:/var/lib/eagle0
environment:
- AUTH_SERVICE_URL=eagle0-auth:40033
```
## Deployment
### Development
```bash
# Terminal 1: Go Auth Service
bazel run //src/main/go/net/eagle0/authservice:authservice -- \
--grpc-port=40033 \
--http-port=8080 \
--eagle-internal-url=localhost:40034
# Terminal 2: Eagle Server
bazel run //src/main/scala/net/eagle0/eagle:eagle_server -- \
--eagle-grpc-port=40032 \
--internal-grpc-port=40034 \
--auth-service-url=localhost:40033
```
### Production
- Both containers on same droplet via docker-compose
- Shared volume for RSA keys at /etc/eagle0/keys/
- Internal Docker network for container-to-container communication
- External access: 40032 (Eagle gRPC), 8080 (OAuth HTTP callback)
## Testing Strategy
1. **Unit tests for Go service**
- OAuth state management (expiration, cleanup)
- JWT creation matches Scala output (test with same keys)
- HTTP callback parsing
2. **Integration tests**
- Go service ↔ Eagle internal gRPC
- Full OAuth flow with mock provider
3. **Existing tests continue to pass**
- All Scala tests (JWT validation, user service)
4. **End-to-end test**
- Spin up both containers
- Run OAuth flow through proxy
## Risks and Mitigations
| Risk | Mitigation |
|------|------------|
| Key file permissions | Shared volume with read-only mount |
| State loss on Go restart | Document this (same as current Scala behavior); consider Redis later |
| Clock skew affecting JWT | Both on same machine |
| OAuth callback race | HTTP callback completes before gRPC poll |
| Container networking | Use docker-compose for reliable internal DNS |
| Proxy adds latency | Minimal (same machine), remove in Phase 2 |
## Estimated Scope
- **Phase 1**: ~500-700 lines Go, ~100 lines Scala changes, ~50 lines Docker config
- **Phase 2**: ~50 lines Unity, deletion of ~300 lines Scala
- **Phase 3**: Optional, separate decision
## Alternative Considered: Move Everything to Go
Could move UserService to Go as well, but:
- UserService is tightly integrated with game persistence
- Would require duplicating persistence layer
- Not worth the complexity for now
Keep UserService in Eagle, expose via internal gRPC.
## Open Questions
1. **HTTP callback routing**: Does the OAuth callback URL need to change, or can we route traffic from the existing URL to the new Go service?
2. **Health checks**: Should we add health check endpoints for container orchestration?
3. **Logging**: Should Go service log to same format/destination as Eagle?
-350
View File
@@ -1,350 +0,0 @@
# OAuth Implementation: Next Steps and Design
## Executive Summary
The OAuth implementation is functional but has several gaps that need addressing before it's production-ready. This document outlines the known issues, proposes a comprehensive user identity model, and provides a prioritized implementation plan.
## Current State (Updated January 2026)
### What Works ✅
- Discord OAuth flow (server-mediated polling)
- Google OAuth flow
- JWT token generation and validation
- User creation and display name setting
- Auto-login with stored tokens
- Basic game creation and play with OAuth users
- Headshot fetching via public CDN (no auth required)
- Logout button in lobby (preserves tokens for quick reconnect)
- Environment (prod/qa) and user display in lobby
- Game identity with userName = displayName (PR #4964 merged)
### Known Issues
#### 1. Game Identity Model Fragility (Deferred)
**Status**: Accepted for now. PR #4964 merged with `userName = displayName`.
**Current behavior**:
- Games store `userNameToFactionId: Map[String, Int]`
- For JWT users, this maps displayName → factionId
- displayName is technically mutable (users could change it)
- No migration path when displayName changes
**Why this is acceptable**:
1. We don't currently have a "change display name" feature
2. The alternative (using userId) requires more extensive changes
3. Can migrate to userId-based identity later if needed
#### 2. In-Game Headshot Fetching ✅ FIXED
**Solution**: Made the `eagle0-headshots` S3 bucket public and enabled CDN.
- Client now fetches directly from `https://eagle0-headshots.sfo3.cdn.digitaloceanspaces.com/`
- No authentication required
- Works for both OAuth and Basic Auth users
- Simpler architecture, no dependency on home Mac server
#### 3. Logout from Lobby ✅ FIXED
**Solution**: Added logout button to lobby UI (PR #4967).
- Button disconnects from server and returns to connection screen
- Intentionally does NOT clear OAuth tokens
- Allows quick reconnect with same account without full OAuth flow
#### 4. Display Name Uniqueness Not Enforced (Medium) - OPEN
**Problem**: User was able to set displayName "nolen" when that name was already taken.
**Root cause**: Unknown - needs investigation. Either:
- The uniqueness check is buggy
- The displayNameIndex wasn't populated correctly during user creation
- Race condition during concurrent registrations
#### 5. Admin Server Crashes ✅ FIXED
**Solution**: PR #4964 sets `userName = displayName` for JWT users.
#### 6. Intermittent "Expired" Errors During Login (Medium) - INVESTIGATING
**Problem**: Users occasionally get "OAuth session expired" errors even when server logs show the callback succeeded.
**Status**: Added diagnostic logging in PR #4974 to trace:
- State creation in `getAuthUrl`
- State lookup in `handleCallback`
- Result lookup in `checkStatus`
**Possible causes**:
- State mismatch between client and server
- Race condition in polling
- Cleanup running at wrong time
#### 7. Token Expiry Field Bug ✅ FIXED
**Problem**: `CheckOAuthStatusResponse.expiresAt` was returning refresh token expiry (30 days) instead of access token expiry (7 days).
**Solution**: Fixed in PR #4974 to calculate correct access token expiry.
---
## Proposed User Identity Model
### Design Principles
1. **Stable Internal Identity**: `userId` (UUID) is the only key used for persistent associations
2. **Display Name is Cosmetic**: Can change without breaking game associations
3. **Backwards Compatibility**: Basic Auth continues to work for local development
4. **Multi-Provider Support**: Users can link Discord, Google, and future providers
5. **Avatar Flexibility**: Use OAuth avatar by default, support custom uploads later
### Data Model
```
User {
userId: String (UUID) // Primary key, immutable, used for all internal references
displayName: String // Unique, user-visible, mutable with migration
displayNameLower: String // Case-insensitive uniqueness
email: String // Primary email for account recovery/linking
avatarUrl: String // Current avatar URL
avatarData: bytes // Cached avatar for offline/fast access (future)
oauthIdentities: [OAuthIdentity]
createdAt: Timestamp
lastLoginAt: Timestamp
isAdmin: Boolean
}
OAuthIdentity {
provider: String // "discord", "google", etc.
providerUserId: String // Provider's user ID
providerEmail: String // Email from this provider
avatarUrl: String // Avatar from this provider
linkedAt: Timestamp
}
```
### Identity Resolution Strategy
The key question: **What should `AuthorizationUtils.userName` return?**
#### Option A: userName = displayName (Current PR #4964)
- **Pro**: Human-readable in logs, game saves, debugging
- **Con**: Breaks if displayName changes
- **Migration**: None needed now, complex later
#### Option B: userName = userId (Recommended)
- **Pro**: Stable identity, displayName changes are safe
- **Con**: UUIDs in logs are ugly, need display name lookup for UI
- **Migration**: Cleaner long-term, but breaking change for any existing OAuth games
#### Option C: Hybrid with Migration Support
- **userName** = userId for new games
- **Legacy lookup** for old games by displayName
- **Display layer** resolves userId → displayName for UI
**Recommendation**: Option B with a display name resolution layer. The ugliness in logs is acceptable for the stability it provides. Implement a `UserService.resolveDisplayName(identifier: String): String` that returns displayName for UUIDs or the identifier itself for legacy usernames.
### Account Linking Strategy
#### Automatic Linking (Future)
When a user logs in with a new OAuth provider:
1. Check if the provider email matches an existing user's email
2. If match found, prompt: "An account exists with this email. Link accounts?"
3. If confirmed, add new OAuthIdentity to existing user
4. If declined, create separate account (different email required)
#### Manual Linking (MVP)
1. User logs in with primary account
2. User goes to Settings → Linked Accounts
3. User clicks "Link Discord" or "Link Google"
4. OAuth flow adds new identity to current user
### Avatar/Headshot Strategy
#### Phase 1: OAuth Avatars (MVP)
- Store `avatarUrl` from OAuth provider during login
- Server proxies avatar requests to avoid CORS issues
- Cache avatars locally with TTL
#### Phase 2: Avatar Caching
- Download avatar to local storage on login
- Serve from local storage for reliability
- Refresh periodically or on login
#### Phase 3: Custom Avatars (Future)
- Allow users to upload custom avatar
- Store in S3/DO Spaces
- Custom avatar overrides OAuth avatar
---
## Implementation Plan
### Phase 1: Stabilization ✅ COMPLETE
#### 1.1 Fix Display Name Uniqueness Bug - OPEN
- [ ] Investigate why "nolen" was allowed when it existed
- [ ] Add logging to `setDisplayName` to trace the issue
- [ ] Ensure `displayNameIndex` is correctly maintained
- [ ] Add unit tests for uniqueness enforcement
#### 1.2 Add Logout Button to Lobby ✅ DONE
- [x] Add "Logout" button to lobby UI
- [x] Disconnect from server
- [x] Navigate to connection screen
- [x] Preserve OAuth tokens for quick reconnect (intentional change from original plan)
#### 1.3 Merge PR #4964 (userName = displayName) ✅ DONE
- [x] Merged - games work with OAuth users
- [x] Documented limitation (games break if displayName changes)
#### 1.4 Fix Headshot Fetching ✅ DONE
- [x] Made eagle0-headshots bucket public
- [x] Client fetches directly from CDN
- [x] No authentication required
#### 1.5 Add Lobby Status Display ✅ DONE
- [x] Show environment (prod/qa) in lobby
- [x] Show current user in lobby (OAuth displayName or classic username)
### Phase 2: Remaining Work (Priority Order)
#### 2.1 Diagnose Intermittent "Expired" Errors - IN PROGRESS
- [x] Add diagnostic logging (PR #4974)
- [ ] Deploy and reproduce the issue
- [ ] Analyze logs to identify root cause
- [ ] Implement fix based on findings
#### 2.2 Fix Display Name Uniqueness
- [ ] Investigate UserService.setDisplayName logic
- [ ] Check displayNameIndex population
- [ ] Add logging to trace the issue
- [ ] Fix the bug and add tests
#### 2.3 Wire Up Lobby UI in Unity
- [ ] Connect `lobbyEnvironmentText` to TextMeshProUGUI in scene
- [ ] Connect `lobbyUserText` to TextMeshProUGUI in scene
#### 2.4 Implement Token Refresh During Gameplay
- [ ] Implement `RefreshToken` RPC on server (currently throws UNIMPLEMENTED)
- [ ] Store refresh tokens server-side for validation
- [ ] Add proactive refresh in client before token expires
- [ ] Handle refresh during reconnection attempts
### Phase 3: Nice-to-Haves (Future)
#### 3.1 Proactive Token Refresh
- [ ] Monitor token expiry in client
- [ ] Refresh automatically when < 5 minutes remaining
- [ ] Update TokenStorage with new access token
#### 3.2 Better Error Messages
- [ ] Distinguish between network errors and auth errors
- [ ] Show user-friendly messages for OAuth failures
- [ ] Add retry suggestions
#### 3.3 Session Persistence Across Server Restarts
- [ ] Move pendingOAuth from in-memory TrieMap to Redis/database
- [ ] Move completedOAuth to Redis with TTL
- [ ] Server can restart without breaking in-flight OAuth flows
#### 3.4 Migrate to userId-based Game Identity (Deferred)
- [ ] Change `AuthorizationUtils.userName` to return `userId` for JWT users
- [ ] Add `UserService.resolveDisplayName(id: String): String` for UI display
- [ ] Update game UI to resolve userIds to displayNames
- [ ] Existing Basic Auth games continue to work (userName is literal)
#### 3.5 Display Name Change Support (Requires 3.4)
- [ ] Add `ChangeDisplayName` RPC
- [ ] Validate new name is unique
- [ ] Update user record
- [ ] No game migration needed (games use userId)
### Phase 3: Multi-Provider Support (Future)
#### 3.1 Account Linking UI
- [ ] Add Settings page with "Linked Accounts" section
- [ ] Show currently linked providers
- [ ] "Link Another Account" button triggers OAuth flow
- [ ] `LinkOAuthProvider` RPC adds identity to current user
#### 3.2 Login Provider Selection
- [ ] If user has multiple providers, any can be used to login
- [ ] All resolve to same userId
- [ ] Session shows which provider was used
#### 3.3 Account Merging (Complex)
- [ ] Handle case where user created separate accounts
- [ ] Merge game history, stats, etc.
- [ ] Delete duplicate user record
- [ ] This is complex - may defer or not implement
### Phase 4: Enhanced Avatars (Future)
#### 4.1 Avatar Caching
- [ ] Download avatars to S3/DO Spaces on login
- [ ] Serve from our CDN
- [ ] Refresh on login if changed
#### 4.2 Custom Avatar Upload
- [ ] Upload endpoint with size/format validation
- [ ] Store in S3/DO Spaces
- [ ] Custom avatar overrides OAuth avatar
---
## Technical Debt to Address
1. **Context Propagation in Futures**: PR #4960 fixed `setDisplayName` and `getCurrentUser`, but audit all `Future` blocks that access `AuthorizationUtils`
2. **Dual Auth Support**: The system supports both Basic Auth and JWT. Consider:
- Should Basic Auth be deprecated for production?
- Should it remain for local development only?
- How do Basic Auth users interact with OAuth users in the same game?
3. **Token Refresh**: `RefreshToken` RPC throws UNIMPLEMENTED. Need to:
- Implement refresh token storage and validation
- Handle token refresh in client
- Consider refresh token rotation for security
4. **Session Management**: No server-side session tracking. Consider:
- Track active sessions per user
- Allow "logout all devices"
- Detect concurrent logins
---
## Open Questions
1. **What happens when a Basic Auth user and OAuth user have the same name?**
- Currently possible - Basic Auth doesn't check UserService
- Could cause confusion in games
- Solution: Require OAuth for multiplayer? Or namespace Basic Auth names?
2. **Should displayName changes be allowed?**
- With userId-based identity, it's safe
- But could cause confusion ("who is this new player?")
- Consider: rate limit changes, show "formerly known as" temporarily
3. **How to handle OAuth provider account deletion?**
- User deletes their Discord account
- Their Eagle0 account still exists
- They can't login unless they linked another provider
- Solution: Encourage linking multiple providers, or add email/password fallback
4. **Admin impersonation with OAuth**
- Currently works via X-Impersonate-User header
- Should this use userId or displayName?
- Probably userId for stability
---
## Appendix: File Locations
### Server (Scala)
- `src/main/scala/net/eagle0/eagle/auth/UserService.scala` - User CRUD
- `src/main/scala/net/eagle0/eagle/auth/JwtService.scala` - Token generation/validation
- `src/main/scala/net/eagle0/eagle/auth/OAuthService.scala` - OAuth flow
- `src/main/scala/net/eagle0/eagle/service/AuthServiceImpl.scala` - gRPC Auth service
- `src/main/scala/net/eagle0/eagle/service/AuthorizationInterceptor.scala` - Auth middleware
- `src/main/scala/net/eagle0/eagle/service/AuthorizationUtils.scala` - Context accessors
### Client (C#)
- `Assets/Auth/AuthClient.cs` - gRPC client for Auth service
- `Assets/Auth/OAuthManager.cs` - OAuth flow orchestration
- `Assets/Auth/TokenStorage.cs` - Persistent token storage
- `Assets/Auth/JwtAuthInterceptor.cs` - Attaches JWT to requests
### Protos
- `src/main/protobuf/net/eagle0/eagle/api/auth.proto` - Auth service definition
- `src/main/protobuf/net/eagle0/eagle/internal/user/user.proto` - User data model
-216
View File
@@ -1,216 +0,0 @@
# Shardok Latency Hiding Strategies
## Problem Statement
With Shardok running on Hetzner (Helsinki) and Eagle on DigitalOcean (US), the round-trip latency for human commands is ~200-400ms:
```
Human posts command:
Unity → Eagle (DO) → Shardok (Hetzner) → Eagle (DO) → Unity
~10ms ~100ms ~100ms ~10ms
Total: ~220ms round-trip
```
This latency is acceptable for AI turns (users watch animations anyway), but creates noticeable lag when humans post commands.
---
## Strategy 1: Client-Side Animation Masking
### Concept
Start animations immediately when the user clicks, before server confirmation arrives. The animation duration masks the network latency.
### Implementation by Command Type
**Movement Commands** (deterministic):
- Client knows the destination hex and movement path
- Start movement animation immediately on click
- Server confirms the move (should always match)
- If server rejects (invalid state), snap unit back to origin
**Attack Commands** (RNG-dependent):
- Show attack animation immediately (unit swings sword, fires arrow)
- Wait for server to return dice roll result
- Show damage numbers / hit effects when server responds
- Animation typically takes 300-500ms, masking most of the latency
**End Turn**:
- Latency not noticeable (user expects transition delay)
### Unity Implementation Sketch
```csharp
// In CommandHandler.cs
public void OnCommandSelected(Command command) {
// Start animation immediately
if (command.Type == CommandType.Move) {
unitController.StartMoveAnimation(command.TargetHex);
} else if (command.Type == CommandType.Attack) {
unitController.StartAttackAnimation(command.TargetUnit);
}
// Send to server in parallel
connection.SendCommand(command, (response) => {
if (response.Success) {
// Animation continues, apply result
ApplyCommandResult(response);
} else {
// Rollback animation
unitController.CancelAnimation();
ShowError(response.ErrorMessage);
}
});
}
```
### Pros
- Simple implementation
- No server-side changes
- Works with existing architecture
### Cons
- Doesn't eliminate latency for attacks with RNG (must wait for dice roll)
- Rollback needed if server rejects command (rare but possible)
### Estimated Improvement
- Movement: ~200ms latency hidden (feels instant)
- Attacks: ~100-200ms hidden by animation, ~100ms visible wait for dice result
---
## Strategy 2: Split Shardok Architecture
### Concept
Run two Shardok instances:
- **Shardok-Primary (DigitalOcean)**: Handles command processing, source of truth
- **Shardok-AI (Hetzner)**: AI computation only
Human commands go to the nearby Primary for low latency. AI computation uses the powerful Hetzner instance.
### Architecture
```
Human commands (low latency ~20ms)
Unity ←→ Eagle ←→ Shardok-Primary (DigitalOcean)
↓ state sync (when AI turn starts)
Shardok-AI (Hetzner)
↑ AI command response
```
### How It Works
**Human Turn:**
1. Human posts command → Eagle → Shardok-Primary (DO)
2. Primary processes command immediately (~10ms local)
3. Primary streams result to client via Eagle (~10ms)
4. **Total latency: ~20ms** (vs ~220ms current)
**AI Turn:**
1. When AI's turn starts, Primary sends game state snapshot to Hetzner
2. Shardok-AI computes best command using full CPU power
3. Shardok-AI returns command index to Primary
4. Primary executes command locally and streams to client
5. Repeat until AI turn ends
### Protocol Changes
```protobuf
// New service for AI-only computation
service ShardokAIService {
// Send game state, receive AI's chosen command
rpc GetAICommand(AICommandRequest) returns (AICommandResponse);
}
message AICommandRequest {
bytes game_state = 1; // Serialized game state
int32 player_id = 2; // Which AI player
repeated bytes available_commands = 3; // Available command descriptors
}
message AICommandResponse {
int32 command_index = 1; // Index into available_commands
int32 search_depth = 2; // For debugging
double best_score = 3; // For debugging
}
```
### Shardok-Primary Requirements
Shardok-Primary on DigitalOcean needs to:
- Process all commands (human and AI)
- Maintain authoritative game state
- Serialize/deserialize game state for AI requests
- Run on minimal CPU (command processing is fast)
This is essentially the current Shardok, but without running the AI search.
### Shardok-AI Requirements
Shardok-AI on Hetzner needs to:
- Receive game state snapshots
- Run AI evaluation (IterativeDeepeningAI or MCTS)
- Return best command index
- No persistent state (stateless worker)
### AI Turn Latency
Each AI command has ~200ms network latency. This is acceptable because:
1. User is watching animations anyway
2. Natural pacing lets user observe AI decisions
3. AI computation is fast on Hetzner's 16 cores
For a typical AI turn with 5 commands: 5 × 200ms = 1 second network overhead, plus AI thinking time. With animations, this feels natural.
### Implementation Phases
**Phase 1: Add Shardok-Primary (minimal)**
- Deploy existing Shardok container to DigitalOcean
- Configure Eagle to use local Shardok for all commands
- Human latency immediately improves
**Phase 2: Add AI offload**
- Implement `ShardokAIService` RPC
- Shardok-Primary calls Hetzner for AI commands
- Shardok-AI processes requests statelessly
**Phase 3: Optimize**
- Batch multiple AI actions if possible
- Pre-warm Shardok-AI connection
- Add fallback if Hetzner unavailable
### Pros
- Human command latency drops from ~220ms to ~20ms
- AI still gets Hetzner's CPU power
- Clear separation of concerns
- Shardok-Primary can fall back to local AI if Hetzner unavailable
### Cons
- Two Shardok instances to maintain
- State serialization overhead for AI requests
- Each AI action has network round-trip (acceptable with animations)
---
## Comparison
| Approach | Human Latency | AI Throughput | Complexity | Changes Required |
|----------|---------------|---------------|------------|------------------|
| Current | ~220ms | High | - | - |
| Animation masking | ~220ms (perceived ~50ms) | High | Low | Unity only |
| Split architecture | ~20ms | High | Medium | New RPC, two deployments |
---
## Recommendation
**Phase 1 (now)**: Implement animation masking in Unity client
- Quick win, no server changes
- Improves perceived latency significantly for movement
- Attacks still show dice animation while waiting
**Phase 2 (future)**: Split architecture if animation masking insufficient
- Only needed if users complain about attack latency
- More complex but provides true low latency
- Natural evolution of current architecture
-2
View File
@@ -9,8 +9,6 @@ require (
github.com/aws/aws-sdk-go-v2/config v1.28.10
github.com/aws/aws-sdk-go-v2/credentials v1.17.51
github.com/aws/aws-sdk-go-v2/service/s3 v1.72.2
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/uuid v1.6.0
google.golang.org/grpc v1.68.0
google.golang.org/protobuf v1.36.3
)
-4
View File
@@ -34,13 +34,9 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.33.6 h1:VwhTrsTuVn52an4mXx29PqRzs2Dv
github.com/aws/aws-sdk-go-v2/service/sts v1.33.6/go.mod h1:+8h7PZb3yY5ftmVLD7ocEoE98hdc8PoKS0H3wfx1dlc=
github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro=
github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+2 -33
View File
@@ -1,7 +1,7 @@
{
"__AUTOGENERATED_FILE_DO_NOT_MODIFY_THIS_FILE_MANUALLY": "THERE_IS_NO_DATA_ONLY_ZUUL",
"__INPUT_ARTIFACTS_HASH": -2049857450,
"__RESOLVED_ARTIFACTS_HASH": -1728186926,
"__INPUT_ARTIFACTS_HASH": -1064460283,
"__RESOLVED_ARTIFACTS_HASH": -1574144850,
"conflict_resolution": {
"com.google.guava:failureaccess:1.0.1": "com.google.guava:failureaccess:1.0.2",
"com.squareup.okio:okio:2.10.0": "com.squareup.okio:okio:3.6.0",
@@ -413,12 +413,6 @@
},
"version": "0.27.0"
},
"io.sentry:sentry": {
"shasums": {
"jar": "740a118182fc089d307830f4e508372e01ad94639b00b4e1b1d83762298a5f35"
},
"version": "7.19.0"
},
"javax.activation:javax.activation-api": {
"shasums": {
"jar": "43fdef0b5b6ceb31b0424b208b930c74ab58fac2ceeb7b3f6fd3aeb8b5ca4393"
@@ -1791,30 +1785,6 @@
"io.perfmark:perfmark-api": [
"io.perfmark"
],
"io.sentry:sentry": [
"io.sentry",
"io.sentry.backpressure",
"io.sentry.cache",
"io.sentry.clientreport",
"io.sentry.config",
"io.sentry.exception",
"io.sentry.hints",
"io.sentry.instrumentation.file",
"io.sentry.internal.debugmeta",
"io.sentry.internal.gestures",
"io.sentry.internal.modules",
"io.sentry.internal.viewhierarchy",
"io.sentry.metrics",
"io.sentry.profilemeasurements",
"io.sentry.protocol",
"io.sentry.rrweb",
"io.sentry.transport",
"io.sentry.util",
"io.sentry.util.thread",
"io.sentry.vendor",
"io.sentry.vendor.gson.internal.bind.util",
"io.sentry.vendor.gson.stream"
],
"javax.activation:javax.activation-api": [
"javax.activation"
],
@@ -2482,7 +2452,6 @@
"io.opencensus:opencensus-contrib-grpc-metrics",
"io.opencensus:opencensus-contrib-http-util",
"io.perfmark:perfmark-api",
"io.sentry:sentry",
"javax.activation:javax.activation-api",
"javax.xml.bind:jaxb-api",
"joda-time:joda-time",
-138
View File
@@ -33,7 +33,6 @@ http {
# HTTP server for Let's Encrypt challenge and redirect
server {
listen 80;
listen [::]:80;
server_name prod.eagle0.net;
# Let's Encrypt challenge
@@ -50,7 +49,6 @@ http {
# HTTPS server for gRPC
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name prod.eagle0.net;
@@ -83,29 +81,6 @@ http {
error_page 502 = /error502grpc;
}
# gRPC proxy for Auth service
location /net.eagle0.eagle.api.auth.Auth {
# Rate limiting
limit_req zone=grpc_limit burst=50 nodelay;
# gRPC proxy
grpc_pass grpc://eagle_grpc;
# Timeouts
grpc_read_timeout 30s;
grpc_send_timeout 30s;
# Error handling
error_page 502 = /error502grpc;
}
# OAuth callback endpoint (proxied to Go auth service)
location /oauth/callback {
proxy_pass http://auth:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# Health check endpoint
location /health {
access_log off;
@@ -122,117 +97,4 @@ http {
return 204;
}
}
# HTTPS server for Go Auth service (port 40033)
# Clients connect here directly for OAuth RPCs in Phase 2
server {
listen 40033 ssl;
listen [::]:40033 ssl;
http2 on;
server_name prod.eagle0.net;
# SSL certificates (same as main server)
ssl_certificate /etc/letsencrypt/live/prod.eagle0.net/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/prod.eagle0.net/privkey.pem;
# SSL configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
# gRPC proxy for Auth service
# Uses variable-based resolution so nginx can start even if auth isn't ready yet
# DNS is cached by the resolver directive (valid=10s)
location /net.eagle0.eagle.api.auth.Auth {
# Rate limiting
limit_req zone=grpc_limit burst=50 nodelay;
# Dynamic upstream resolution (doesn't block nginx startup)
set $auth_backend "auth:40033";
grpc_pass grpc://$auth_backend;
# Timeouts
grpc_read_timeout 30s;
grpc_send_timeout 30s;
# Error handling
error_page 502 = /error502grpc;
}
# gRPC proxy for Admin service
location /net.eagle0.eagle.api.admin.Admin {
# Rate limiting
limit_req zone=grpc_limit burst=50 nodelay;
# Dynamic upstream resolution (doesn't block nginx startup)
set $auth_backend "auth:40033";
grpc_pass grpc://$auth_backend;
# Timeouts
grpc_read_timeout 30s;
grpc_send_timeout 30s;
# Error handling
error_page 502 = /error502grpc;
}
# Health check endpoint
location /health {
access_log off;
return 200 "OK\n";
add_header Content-Type text/plain;
}
# gRPC error handling
location = /error502grpc {
internal;
default_type application/grpc;
add_header grpc-status 14;
add_header grpc-message "unavailable";
return 204;
}
}
# HTTP server for Admin Console (Let's Encrypt + redirect)
server {
listen 80;
listen [::]:80;
server_name admin.prod.eagle0.net admin.eagle0.net;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
# HTTPS server for Admin Console
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name admin.prod.eagle0.net admin.eagle0.net;
ssl_certificate /etc/letsencrypt/live/admin.eagle0.net/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/admin.eagle0.net/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
location / {
proxy_pass http://admin:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
bazel run //src/main/go/net/eagle0/build/action_result_type_build_file_generator \
${PWD}/src/main/scala/net/eagle0/eagle/model/action_result/types/
@@ -14,7 +14,6 @@ cc_library(
"//src/main/cpp/net/eagle0/shardok/library:engine",
"//src/main/cpp/net/eagle0/shardok/server:eagle_interface_grpc_server",
"//src/main/cpp/net/eagle0/shardok/server:server_configuration",
"//src/main/cpp/net/eagle0/shardok/server:token_auth",
"//src/main/protobuf/net/eagle0/common:common_unit_cc_proto",
"@grpc//:grpc++",
],
@@ -222,6 +222,7 @@ double AIHeuristicWeighting::GetCommandWeight(
case CommandType::RELEASE_UNIT_COMMAND: return 0.0;
case CommandType::REINFORCE_COMMAND: return 10.0;
case CommandType::MANAGE_PRISONER: return 1.0;
// === ZERO WEIGHT - NEVER SELECT (0.0) ===
// Explicitly bad actions
@@ -25,8 +25,7 @@ auto CalculateTimeBudget(
const PlayerId playerId,
const GameSettingsSPtr &settings,
const GameStateW &state,
const size_t numCommands,
const bool isAllAiBattle) -> AITimeBudget {
const size_t numCommands) -> AITimeBudget {
const auto settingsGetter = settings->GetGetter();
const auto castleCoords = AllCastleCoords(state->hex_map());
@@ -35,10 +34,8 @@ auto CalculateTimeBudget(
net::eagle0::shardok::storage::fb::GameStatus_::State_SET_UP;
// Get maximum budget cap from settings (in seconds)
// For all-AI battles, use the faster budget limit
const double maxBudgetSeconds =
isAllAiBattle ? settingsGetter.Backing().all_ai_battle_time_budget_maximum()
: settingsGetter.Backing().lookahead_time_budget_maximum_seconds();
settingsGetter.Backing().lookahead_time_budget_maximum_seconds();
const double maxBudgetMs = maxBudgetSeconds * 1000.0;
// During setup, use the setup-specific time budget
@@ -38,13 +38,11 @@ struct AITimeBudget {
// Calculate time budget based on proximity to enemies and castles
// Time budget is calculated dynamically based on number of available commands:
// budget = msPerCommand × numCommands (clamped to 200-5000ms)
// If isAllAiBattle is true, uses allAiBattleTimeBudgetMaximum instead of the normal maximum.
auto CalculateTimeBudget(
PlayerId playerId,
const GameSettingsSPtr &settings,
const GameStateW &state,
size_t numCommands,
bool isAllAiBattle) -> AITimeBudget;
size_t numCommands) -> AITimeBudget;
} // namespace shardok
@@ -53,7 +53,6 @@ auto RoundsRemaining(const GameSettingsSPtr &settings, const GameStateView &gsv)
ShardokAIClient::ShardokAIClient(
const PlayerId playerId,
const bool isDefender,
const bool isAllAiBattle,
const HexMap *hexMap,
const SettingsGetter &settings,
const AIAlgorithmType aiAlgorithmType,
@@ -61,7 +60,6 @@ ShardokAIClient::ShardokAIClient(
const mcts::MCTSConfig &mctsConfig)
: playerId(playerId),
isDefender(isDefender),
isAllAiBattle(isAllAiBattle),
aiAlgorithmType(aiAlgorithmType),
scoringCalculatorType(scoringCalculatorType),
alCache(std::make_unique<AttackLocationsCache>(hexMap, settings)),
@@ -137,8 +135,7 @@ auto ShardokAIClient::StandardChooseCommandIndex(
const auto commandCount = guessedCommands->size();
// Calculate time budget based on game situation using new dynamic per-command settings
const auto timeBudget =
CalculateTimeBudget(playerId, settings, guessedState, commandCount, isAllAiBattle);
const auto timeBudget = CalculateTimeBudget(playerId, settings, guessedState, commandCount);
// Configure MCTS based on proximity to enemy
// When far from enemy: use AVERAGING with maxPlayerFlips=0 (single-player lookahead)
@@ -41,7 +41,6 @@ class ShardokAIClient {
private:
const PlayerId playerId;
const bool isDefender;
const bool isAllAiBattle; // Whether this battle has only AI players (for faster time budgets)
const AIAlgorithmType aiAlgorithmType;
const ScoringCalculatorType scoringCalculatorType;
@@ -70,7 +69,6 @@ public:
explicit ShardokAIClient(
PlayerId playerId,
bool isDefender,
bool isAllAiBattle,
const HexMap* hexMap,
const SettingsGetter& settings,
AIAlgorithmType aiAlgorithmType,
@@ -71,21 +71,15 @@ vector<shared_ptr<ShardokAIClient>> ShardokGameController::MakeAIClients(
mctsConfig.maxSimulationFlips = 1;
mctsConfig.simulationPolicy = mcts::MCTSSimulationPolicy::WEIGHTED_HEURISTIC;
// Check if all players are AI - if so, use faster time budgets
const auto &playerInfos = e->GetPlayerInfos();
const bool isAllAiBattle =
std::ranges::all_of(playerInfos, [](const auto &pi) { return pi.is_ai(); });
for (const auto &pi : playerInfos) {
for (const auto &pi : e->GetPlayerInfos()) {
if (pi.is_ai()) {
auto newClient = std::make_shared<ShardokAIClient>(
pi.player_id(),
pi.is_defender(),
isAllAiBattle,
e->GetCurrentGameState()->hex_map(),
e->GetGameSettings()->GetGetter(),
AIAlgorithmType::ITERATIVE_DEEPENING,
ScoringCalculatorType::STANDARD,
ScoringCalculatorType::MCTS_OPTIMIZED,
mctsConfig);
// MCTS config is dynamically adjusted in ShardokAIClient based on proximity:
@@ -34,17 +34,6 @@ cc_library(
],
)
cc_library(
name = "token_auth",
hdrs = ["TokenAuthInterceptor.hpp"],
copts = COPTS,
visibility = ["//src/main/cpp/net/eagle0/shardok:__subpackages__"],
deps = [
"//src/main/cpp/net/eagle0/common:protobuf_warning_suppression",
"@grpc//:grpc++",
],
)
cc_library(
name = "eagle_interface_grpc_server",
srcs = ["EagleInterfaceGrpcServer.cpp"],
@@ -53,7 +42,6 @@ cc_library(
visibility = ["//src/main/cpp/net/eagle0/shardok:__subpackages__"],
deps = [
":games_manager",
":token_auth",
"//src/main/cpp/net/eagle0/common:unit_conversions",
"//src/main/protobuf/net/eagle0/common:shardok_internal_interface_cc_grpc",
"//src/main/protobuf/net/eagle0/common:victory_condition_cc_proto",
@@ -81,17 +81,8 @@ static auto FromInternalStatus(
const std::string &kShardokGameRequestExtension = *(new string(".e0gr"));
EagleInterfaceImpl::EagleInterfaceImpl(
shared_ptr<ShardokGamesManager> manager,
std::string authToken)
: gamesManager(std::move(manager)),
tokenValidator_(std::move(authToken)) {
if (tokenValidator_.IsEnabled()) {
std::cout << "Token authentication enabled" << std::endl;
} else {
std::cout << "Token authentication disabled (no token configured)" << std::endl;
}
}
EagleInterfaceImpl::EagleInterfaceImpl(shared_ptr<ShardokGamesManager> manager)
: gamesManager(std::move(manager)) {}
auto ConvertPlayerInfo(
const google::protobuf::RepeatedPtrField<net::eagle0::common::PlayerSetupInfo> &allPis,
@@ -197,12 +188,9 @@ void EagleInterfaceImpl::StartGame(const NewGameRequest &request) {
}
auto EagleInterfaceImpl::PostCommand(
ServerContext *context,
ServerContext * /*context*/,
const PostCommandRequest *request,
GameStatusResponse *response) -> Status {
// Validate auth token
if (auto authStatus = tokenValidator_.ValidateOrStatus(context)) { return *authStatus; }
shared_ptr<ShardokGameController> controller;
try {
controller = ControllerForGame(request->game_id(), request->game_setup_info());
@@ -241,12 +229,9 @@ auto EagleInterfaceImpl::PostCommand(
}
auto EagleInterfaceImpl::PostPlacementCommands(
ServerContext *context,
ServerContext * /*context*/,
const PlacementCommandsRequest *request,
GameStatusResponse *response) -> Status {
// Validate auth token
if (auto authStatus = tokenValidator_.ValidateOrStatus(context)) { return *authStatus; }
shared_ptr<ShardokGameController> controller;
try {
controller = ControllerForGame(request->game_id(), request->game_setup_info());
@@ -338,12 +323,9 @@ void EagleInterfaceImpl::PopulateGameStatusResponse(
}
auto EagleInterfaceImpl::GetHexMap(
ServerContext *context,
ServerContext * /*context*/,
const HexMapRequest *request,
HexMapResponse *response) -> Status {
// Validate auth token
if (auto authStatus = tokenValidator_.ValidateOrStatus(context)) { return *authStatus; }
// TODO: return appropriate status code if a bad map name is sent
*response->mutable_map() = LoadMap(request->map_name());
@@ -351,12 +333,9 @@ auto EagleInterfaceImpl::GetHexMap(
}
auto EagleInterfaceImpl::GetHexMapNames(
ServerContext *context,
ServerContext * /*context*/,
const HexMapNamesRequest * /*request*/,
HexMapNamesResponse *response) -> Status {
// Validate auth token
if (auto authStatus = tokenValidator_.ValidateOrStatus(context)) { return *authStatus; }
for (const string &mapName : GetMapNames()) { response->add_map_names(mapName); }
return Status::OK;
@@ -537,9 +516,6 @@ auto EagleInterfaceImpl::SubscribeToGame(
ServerContext *context,
const GameSubscriptionRequest *request,
grpc::ServerWriter<GameStatusResponse> *writer) -> Status {
// Validate auth token
if (auto authStatus = tokenValidator_.ValidateOrStatus(context)) { return *authStatus; }
shared_ptr<ShardokGameController> controller;
try {
controller = ControllerForGame(request->game_id(), request->game_setup_info());
@@ -13,9 +13,8 @@
#include <string>
#include <thread>
#include "ShardokGamesManager.hpp"
#include "src/main/cpp/net/eagle0/common/ProtobufWarningSuppression.hpp"
#include "src/main/cpp/net/eagle0/shardok/server/ShardokGamesManager.hpp"
#include "src/main/cpp/net/eagle0/shardok/server/TokenAuthInterceptor.hpp"
#pragma GCC diagnostic push
SUPPRESS_PROTOBUF_WARNINGS
#include <grpc/grpc.h>
@@ -47,7 +46,6 @@ using std::shared_ptr;
class EagleInterfaceImpl final : public ShardokInternalInterface::Service {
private:
std::shared_ptr<ShardokGamesManager> gamesManager;
TokenValidator tokenValidator_;
auto ControllerForGame(const GameId& gameId, const GameSetupInfo& setupInfo)
-> std::shared_ptr<ShardokGameController>;
@@ -59,14 +57,7 @@ private:
GameStatusResponse* response);
public:
/**
* Create the service.
*
* @param manager Games manager
* @param authToken Optional auth token. If non-empty, all requests must include
* "authorization: Bearer <token>" metadata.
*/
EagleInterfaceImpl(std::shared_ptr<ShardokGamesManager> manager, std::string authToken = "");
explicit EagleInterfaceImpl(std::shared_ptr<ShardokGamesManager> manager);
auto PostCommand(
ServerContext* context,
@@ -24,7 +24,6 @@ const string ServerConfiguration::kEagleInterfaceGrpcAddress = "eagleInterfaceGr
const string ServerConfiguration::kEagleGrpcAddress = "eagleGrpcAddress";
const string ServerConfiguration::kSslCertPath = "sslCertPath";
const string ServerConfiguration::kSslPrivateKeyPath = "sslPrivateKeyPath";
const string ServerConfiguration::kAuthTokenPath = "authTokenPath";
#pragma GCC diagnostic pop
using std::unordered_map;
@@ -29,7 +29,6 @@ public:
const static string kEagleGrpcAddress;
const static string kSslCertPath;
const static string kSslPrivateKeyPath;
const static string kAuthTokenPath;
};
#endif /* ServerConfiguration_hpp */
@@ -1,119 +0,0 @@
//
// TokenAuthInterceptor.hpp
// eagle0
//
// Token-based authentication for gRPC.
// Validates Bearer tokens in the 'authorization' metadata header.
//
#ifndef TokenAuthInterceptor_hpp
#define TokenAuthInterceptor_hpp
#include <fstream>
#include <iostream>
#include <optional>
#include <sstream>
#include <string>
#include "src/main/cpp/net/eagle0/common/ProtobufWarningSuppression.hpp"
#pragma GCC diagnostic push
SUPPRESS_PROTOBUF_WARNINGS
#include <grpcpp/server_context.h>
#include <grpcpp/support/status.h>
#pragma GCC diagnostic pop
namespace shardok {
/**
* Validates Bearer tokens in gRPC request metadata.
*
* Usage:
* TokenValidator validator(expectedToken);
* if (!validator.Validate(context)) {
* return grpc::Status(grpc::UNAUTHENTICATED, "Invalid token");
* }
*/
class TokenValidator {
public:
/**
* Create a validator with the expected token.
* If expectedToken is empty, all requests are allowed (no auth required).
*/
explicit TokenValidator(std::string expectedToken) : expectedToken_(std::move(expectedToken)) {}
/**
* Validate the authorization header in the server context.
*
* Expects: "authorization" metadata with value "Bearer <token>"
*
* @return true if valid (or no auth configured), false otherwise
*/
[[nodiscard]] auto Validate(grpc::ServerContext* context) const -> bool {
// If no auth token is configured, allow all requests
if (expectedToken_.empty()) { return true; }
const auto& metadata = context->client_metadata();
auto it = metadata.find("authorization");
if (it == metadata.end()) {
std::cerr << "Auth failed: no authorization header" << std::endl;
return false;
}
std::string authHeader(it->second.begin(), it->second.end());
std::string expectedHeader = "Bearer " + expectedToken_;
if (authHeader != expectedHeader) {
std::cerr << "Auth failed: invalid token" << std::endl;
return false;
}
return true;
}
/**
* Validate and return appropriate Status.
*
* @return std::nullopt if valid, Status(UNAUTHENTICATED) if invalid
*/
[[nodiscard]] auto ValidateOrStatus(grpc::ServerContext* context) const
-> std::optional<grpc::Status> {
if (Validate(context)) { return std::nullopt; }
return grpc::Status(grpc::UNAUTHENTICATED, "Invalid or missing authentication token");
}
/**
* Check if authentication is enabled.
*/
[[nodiscard]] auto IsEnabled() const -> bool { return !expectedToken_.empty(); }
private:
std::string expectedToken_;
};
/**
* Read auth token from a file, stripping whitespace.
* Returns empty string if file doesn't exist or is empty.
*/
inline auto ReadAuthTokenFromFile(const std::string& filePath) -> std::string {
if (filePath.empty()) { return ""; }
std::ifstream file(filePath);
if (!file.is_open()) {
std::cerr << "Warning: Could not open auth token file: " << filePath << std::endl;
return "";
}
std::stringstream buffer;
buffer << file.rdbuf();
std::string token = buffer.str();
// Trim whitespace
auto start = token.find_first_not_of(" \t\n\r");
if (start == std::string::npos) { return ""; }
auto end = token.find_last_not_of(" \t\n\r");
return token.substr(start, end - start + 1);
}
} // namespace shardok
#endif /* TokenAuthInterceptor_hpp */
@@ -7,9 +7,7 @@
//
#include <cstddef>
#include <fstream>
#include <memory>
#include <sstream>
#include <thread>
#include "src/main/cpp/net/eagle0/common/ProtobufWarningSuppression.hpp"
@@ -19,16 +17,14 @@ SUPPRESS_PROTOBUF_WARNINGS
#include <execinfo.h>
#include <grpcpp/channel.h>
#include <grpcpp/security/server_credentials.h>
#include <signal.h>
#include <unistd.h>
#pragma GCC diagnostic pop
#include "src/main/cpp/net/eagle0/shardok/server/EagleInterfaceGrpcServer.hpp"
#include "src/main/cpp/net/eagle0/shardok/server/ServerConfiguration.hpp"
#include "src/main/cpp/net/eagle0/shardok/server/ShardokGamesManager.hpp"
#include "src/main/cpp/net/eagle0/shardok/server/TokenAuthInterceptor.hpp"
#include "server/EagleInterfaceGrpcServer.hpp"
#include "server/ServerConfiguration.hpp"
#include "server/ShardokGamesManager.hpp"
using ::GameStatePersister;
using shardok::EagleInterfaceImpl;
@@ -45,15 +41,12 @@ struct ServerThreadInfo {
auto StartInThread(grpc::ServerBuilder *serverBuilder) -> ServerThreadInfo;
auto CreateServerBuilder(const string &serverAddress, const string &certPath, const string &keyPath)
-> grpc::ServerBuilder *;
auto CreateServerBuilder(const string &serverAddress) -> grpc::ServerBuilder *;
auto CreateEagleInterfaceService(
const std::shared_ptr<ServerConfiguration> &config,
std::shared_ptr<ShardokGamesManager> shardokGamesManager) -> ServerThreadInfo;
auto ReadFileContents(const string &path) -> string;
void handler(const int sig) {
void *array[10];
size_t size;
@@ -89,46 +82,11 @@ auto main(const int argc, char **argv) -> int {
eagleInterfaceInfo.thread.join();
}
auto ReadFileContents(const string &path) -> string {
std::ifstream file(path);
if (!file.is_open()) { return ""; }
std::stringstream buffer;
buffer << file.rdbuf();
return buffer.str();
}
auto CreateServerBuilder(const string &serverAddress, const string &certPath, const string &keyPath)
-> grpc::ServerBuilder * {
auto CreateServerBuilder(const string &serverAddress) -> grpc::ServerBuilder * {
auto *serverBuilder = new grpc::ServerBuilder;
std::shared_ptr<grpc::ServerCredentials> credentials;
// Check if TLS is configured
if (!certPath.empty() && !keyPath.empty()) {
const string certContents = ReadFileContents(certPath);
const string keyContents = ReadFileContents(keyPath);
if (certContents.empty() || keyContents.empty()) {
std::cerr << "Error: Could not read TLS certificate or key file" << std::endl;
std::cerr << " cert path: " << certPath << std::endl;
std::cerr << " key path: " << keyPath << std::endl;
std::cerr << "Falling back to insecure connection" << std::endl;
credentials = grpc::InsecureServerCredentials();
} else {
grpc::SslServerCredentialsOptions::PemKeyCertPair keyCert;
keyCert.private_key = keyContents;
keyCert.cert_chain = certContents;
grpc::SslServerCredentialsOptions sslOpts;
sslOpts.pem_key_cert_pairs.push_back(keyCert);
credentials = grpc::SslServerCredentials(sslOpts);
std::cout << "TLS enabled with certificate from: " << certPath << std::endl;
}
} else {
credentials = grpc::InsecureServerCredentials();
std::cout << "Creating insecure connection (no TLS configured)" << std::endl;
}
const auto credentials = grpc::InsecureServerCredentials();
std::cout << "Creating insecure connection" << std::endl;
serverBuilder->AddListeningPort(serverAddress, credentials);
return serverBuilder;
@@ -140,19 +98,9 @@ auto CreateEagleInterfaceService(
const string eagleInterfaceServerAddress =
config->stringForKey(ServerConfiguration::kEagleInterfaceGrpcAddress);
// TLS configuration
const string certPath = config->stringForKey(ServerConfiguration::kSslCertPath);
const string keyPath = config->stringForKey(ServerConfiguration::kSslPrivateKeyPath);
auto *eagleInterfaceServerBuilder = CreateServerBuilder(eagleInterfaceServerAddress);
auto *eagleInterfaceServerBuilder =
CreateServerBuilder(eagleInterfaceServerAddress, certPath, keyPath);
// Auth token configuration
const string authTokenPath = config->stringForKey(ServerConfiguration::kAuthTokenPath);
const string authToken = shardok::ReadAuthTokenFromFile(authTokenPath);
const auto eagleInterfaceService =
std::make_shared<EagleInterfaceImpl>(shardokGamesManager, authToken);
const auto eagleInterfaceService = std::make_shared<EagleInterfaceImpl>(shardokGamesManager);
eagleInterfaceServerBuilder->RegisterService(eagleInterfaceService.get());
ServerThreadInfo eagleInterfaceThreadInfo = StartInThread(eagleInterfaceServerBuilder);
@@ -64,12 +64,10 @@
<Compile Include="Assets/common/CommonExtensions.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Modal Window/ModalWindowTabs.cs" />
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/CapturedHeroImprisonedDetailsNotificationGenerator.cs" />
<Compile Include="Assets/Shardok/MoveAnimator.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/SuppressBeastsCommandSelector.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/ResolveInvitationCommandSelector.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Slider/SliderManager.cs" />
<Compile Include="Assets/common/GUIUtils/TableRowHoverDetector.cs" />
<Compile Include="Assets/ConnectionHandler/DropGameConfirmationPanel.cs" />
<Compile Include="Assets/Eagle/Table Rows/DynamicHeroTextUpdater.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/util/HeroDropdownController.cs" />
<Compile Include="Assets/Eagle/Notifications/PrisonerReturnedNotificationGenerator.cs" />
@@ -93,7 +91,6 @@
<Compile Include="Assets/Eagle/ConnectionStatusUI.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Button/ButtonManagerIconEditor.cs" />
<Compile Include="Assets/Eagle/NotificationPanel.cs" />
<Compile Include="Assets/Shardok/FreezeAnimator.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Demo/DemoListShadow.cs" />
<Compile Include="Assets/HoveringTooltipTextProvider.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/util/KeyModifiedAmount.cs" />
@@ -111,14 +108,12 @@
<Compile Include="Assets/Modern UI Pack/Scripts/Context Menu/ContextMenuManager.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/UI Manager/UIManagerProgressBarLoopEditor.cs" />
<Compile Include="Assets/Eagle/BattalionUtils.cs" />
<Compile Include="Assets/Shardok/ArrowVolleyAnimator.cs" />
<Compile Include="Assets/Eagle/Table Rows/UnaffiliatedHeroRowController.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Tooltip/TooltipContent.cs" />
<Compile Include="Assets/Eagle/ConnectionCircuitBreaker.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/UI Manager/UIManagerHSelector.cs" />
<Compile Include="Assets/Shardok/Grid.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/ArmTroopsCommandSelector.cs" />
<Compile Include="Assets/Shardok/RaiseDeadAnimator.cs" />
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/PrisonerExiledDetailsNotificationGenerator.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/UI Manager/UIManagerProgressBar.cs" />
<Compile Include="Assets/Eagle/Table Rows/OrganizeTroopsComponentRow.cs" />
@@ -130,14 +125,11 @@
<Compile Include="Assets/Eagle/Notifications/Notification.cs" />
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/QuestFailedDetailsNotificationGenerator.cs" />
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/QuestFulfilledDetailsNotificationGenerator.cs" />
<Compile Include="Assets/Auth/OAuthManager.cs" />
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/ProfessionGainedDetailsNotificationGenerator.cs" />
<Compile Include="Assets/Eagle/TextureList.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Modal Window/ModalWindowManager.cs" />
<Compile Include="Assets/Eagle/Table Rows/BattalionRowController.cs" />
<Compile Include="Assets/Shardok/DismissAnimator.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Toggle/ToggleAnim.cs" />
<Compile Include="Assets/Shardok/HolyWaveAnimator.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/UI Manager/UIManagerInputField.cs" />
<Compile Include="Assets/Shardok/ActionResultTypeManager.cs" />
<Compile Include="Assets/MainQueue.cs" />
@@ -154,7 +146,6 @@
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/ProvinceHeldDetailsNotificationGenerator.cs" />
<Compile Include="Assets/Bluetooth/DieInfo.cs" />
<Compile Include="Assets/common/ResourceFetcher.cs" />
<Compile Include="Assets/Auth/AuthClient.cs" />
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/TruceRejectedDetailsNotificationGenerator.cs" />
<Compile Include="Assets/Eagle/Notifications/RiotSuppressedNotificationGenerator.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Modal Window/ModalWindowManagerEditor.cs" />
@@ -162,7 +153,6 @@
<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/Eagle/Notifications/ARNNotifications/ShatteredArmyDetailsNotificationGenerator.cs" />
<Compile Include="Assets/Shardok/TurnHistoryPanelController.cs" />
@@ -186,7 +176,6 @@
<Compile Include="Assets/Shardok/Table Rows/ReserveRowController.cs" />
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/OutlawSpottedDetailsNotificationGenerator.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Fixes/UIElementInFront.cs" />
<Compile Include="Assets/Shardok/MeleeAnimator.cs" />
<Compile Include="Assets/ConnectionHandler/WaitingGameItem.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Button/ButtonManagerBasicIcon.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/UI Manager/UIManagerSlider.cs" />
@@ -200,7 +189,6 @@
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/TruceAcceptedDetailsNotificationGenerator.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Progress Bar/ProgressBarEditor.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Dropdown/CustomDropdownEditor.cs" />
<Compile Include="Assets/Shardok/WaterEffectAnimator.cs" />
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/RansomRejectedDetailsNotificationGenerator.cs" />
<Compile Include="Assets/Eagle/FactionsTableController.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Slider/RangeSlider.cs" />
@@ -240,7 +228,6 @@
<Compile Include="Assets/ConnectionKiller.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Slider/RadialSliderEditor.cs" />
<Compile Include="Assets/Eagle/Table Rows/MovingArmyPopupRowController.cs" />
<Compile Include="Assets/Shardok/DuelAnimator.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Dropdown/CustomDropdown.cs" />
<Compile Include="Assets/Eagle/Notifications/NotificationDispatcher.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Demo/DemoTopButton.cs" />
@@ -257,11 +244,9 @@
<Compile Include="Assets/Eagle/CommandButtonPanelController.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Slider/RangeMinSlider.cs" />
<Compile Include="Assets/Eagle/Notifications/WeatherForcedSuppliesBackNotificationGenerator.cs" />
<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" />
<Compile Include="Assets/Modern UI Pack/Scripts/Progress Bar/ProgressBar.cs" />
@@ -269,12 +254,10 @@
<Compile Include="Assets/UI/Scripts/GameSceneManager.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/RansomCommandSelector.cs" />
<Compile Include="Assets/Eagle/Notifications/WeatherForcedSuppliesLostNotificationGenerator.cs" />
<Compile Include="Assets/Shardok/MeteorAnimator.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/ReturnCommandSelector.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/ResolveTruceCommandSelector.cs" />
<Compile Include="Assets/Eagle/ClientPregeneratedText.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/SwearBrotherhoodCommandSelector.cs" />
<Compile Include="Assets/Auth/TokenStorage.cs" />
<Compile Include="Assets/Eagle/Table Rows/TableRowController.cs" />
<Compile Include="Assets/Eagle/DisplayNames.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/UI Manager/UIManagerSliderEditor.cs" />
@@ -283,8 +266,6 @@
<Compile Include="Assets/Modern UI Pack/Scripts/Fixes/LayoutGroupPositionFix.cs" />
<Compile Include="Assets/common/Logger.cs" />
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/AllianceAcceptedNotificationGenerator.cs" />
<Compile Include="Assets/Shardok/ToolAnimator.cs" />
<Compile Include="Assets/Shardok/ScoutAnimator.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/UI Manager/UIManagerModalWindow.cs" />
<Compile Include="Assets/Eagle/HeroDetailsController.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Button/ButtonManagerEditor.cs" />
@@ -292,7 +273,6 @@
<Compile Include="Assets/Eagle/Table Rows/UnitSelectorHeroRowController.cs" />
<Compile Include="Assets/ConnectionHandler/CustomBattleHandler.cs" />
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/RansomPaidDetailsNotificationGenerator.cs" />
<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/Modern UI Pack/Scripts/Slider/SliderManagerEditor.cs" />
@@ -308,7 +288,6 @@
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/CapturedHeroReturnedDetailsNotificationGenerator.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/DivineCommandSelector.cs" />
<Compile Include="Assets/Shardok/Unit.cs" />
<Compile Include="Assets/Shardok/FearAnimator.cs" />
<Compile Include="Assets/Eagle/ProvinceInfoPanelController.cs" />
<Compile Include="Assets/Shardok/ShardokGameModel.cs" />
<Compile Include="Assets/common/GUIUtils/GeneralClickDetector.cs" />
@@ -317,7 +296,6 @@
<Compile Include="Assets/Modern UI Pack/Scripts/Button/ButtonManagerBasicWithIcon.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" />
<Compile Include="Assets/Eagle/Notifications/NotificationGenerator.cs" />
<Compile Include="Assets/Eagle/Notifications/DynamicTextNotification.cs" />
@@ -344,7 +322,6 @@
<Compile Include="Assets/Eagle/CommandSelectors/ControlWeatherCommandSelector.cs" />
<Compile Include="Assets/Shardok/ShardokGameController.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Rendering/UIGradientEditor.cs" />
<Compile Include="Assets/Shardok/LightningAnimator.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Progress Bar/PBFilled.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Rendering/UIGradient.cs" />
<Compile Include="Assets/Eagle/DominionPanelController.cs" />
@@ -353,7 +330,6 @@
<Compile Include="Assets/Eagle/IClientConnectionSubscriber.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/AlmsCommandSelector.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/ResolveDiplomacyCommandSelector.cs" />
<Compile Include="Assets/Shardok/FleeAnimator.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Button/ButtonManagerWithIcon.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Horizontal Selector/HorizontalSelectorEditor.cs" />
<Compile Include="Assets/Eagle/PopupPanelController.cs" />
@@ -377,7 +353,6 @@
<Compile Include="Assets/ConnectionHandler/CreateGameItem.cs" />
<Compile Include="Assets/Eagle/EagleGameController.cs" />
<Compile Include="Assets/Eagle/Notifications/FailedSwearBrotherhoodNotificationGenerator.cs" />
<Compile Include="Assets/Shardok/FireEffectAnimator.cs" />
<Compile Include="Assets/common/GUIUtils/EventBasedTable.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Window/WindowManagerEditor.cs" />
<Compile Include="Assets/Eagle/Notifications/FactionDestroyedNotificationGenerator.cs" />
@@ -388,10 +363,8 @@
<Compile Include="Assets/Eagle/Table Rows/AvailableHeroTableRow.cs" />
<Compile Include="Assets/Eagle/Table Rows/HeroRowController.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/UI Manager/UIManagerTooltip.cs" />
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/NewFactionHeadDetailsNotificationGenerator.cs" />
<Compile Include="Assets/Shardok/HexGrid.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Tooltip/TooltipManager.cs" />
<Compile Include="Assets/Shardok/CatapultAnimator.cs" />
<None Include="Assets/TextMesh Pro/Resources/Shaders/TMPro.cginc" />
<None Include="Assets/TextMesh Pro/Resources/Shaders/TMP_SDF-Mobile Overlay.shader" />
<None Include="Assets/Packages/System.IO.Pipelines.8.0.0/lib/netstandard2.0/System.IO.Pipelines.xml" />
@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 683e069fbc3074e35ac6fc3881a03f4a
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,192 +0,0 @@
using System;
using System.Threading.Tasks;
using Cysharp.Net.Http;
using Grpc.Core;
using Grpc.Core.Interceptors;
using Grpc.Net.Client;
using Net.Eagle0.Eagle.Api.Auth;
using UnityEngine;
using GrpcAuthClient = Net.Eagle0.Eagle.Api.Auth.Auth.AuthClient;
namespace Auth {
/// <summary>
/// gRPC client for the Auth service.
/// Routes OAuth requests (GetOAuthUrl, CheckOAuthStatus, RefreshToken) to the Go auth service.
/// Routes user requests (SetDisplayName, GetCurrentUser, Logout) to Eagle.
/// </summary>
public class AuthClient : IDisposable {
private const int PollIntervalMs = 2000; // Poll every 2 seconds
private const int PollTimeoutMs = 300000; // 5 minute timeout
private readonly GrpcAuthClient _authServiceClient; // Go auth service
private readonly GrpcAuthClient _eagleClient; // Eagle server
private readonly GrpcChannel _authServiceChannel;
private readonly GrpcChannel _eagleChannel;
/// <summary>
/// Create auth client with separate channels for auth service and Eagle.
/// </summary>
/// <param name="authServiceUrl">Go auth service URL, e.g.
/// "https://prod.eagle0.net:40033"</param> <param name="eagleUrl">Eagle server URL, e.g.
/// "https://prod.eagle0.net:40032"</param>
public AuthClient(string authServiceUrl, string eagleUrl) {
// Auth service channel (no JWT needed for OAuth flow)
_authServiceChannel = GrpcChannel.ForAddress(
authServiceUrl,
new GrpcChannelOptions {
HttpHandler = new YetAnotherHttpHandler { Http2Only = true },
DisposeHttpClient = true
});
_authServiceClient = new GrpcAuthClient(_authServiceChannel);
// Eagle channel with JWT interceptor for authenticated requests
_eagleChannel = GrpcChannel.ForAddress(
eagleUrl,
new GrpcChannelOptions {
HttpHandler = new YetAnotherHttpHandler { Http2Only = true },
DisposeHttpClient = true
});
CallInvoker eagleInvoker = _eagleChannel.Intercept(new JwtAuthInterceptor());
_eagleClient = new GrpcAuthClient(eagleInvoker);
Debug.Log($"[AuthClient] Configured: authService={authServiceUrl}, eagle={eagleUrl}");
}
/// <summary>
/// Get OAuth URL to open in system browser.
/// Returns both the URL and the state token for polling.
/// Routed to Go auth service.
/// </summary>
/// <param name="provider">Discord or Google</param>
/// <returns>Tuple of (URL to open in browser, state token for polling)</returns>
public async Task<(string authUrl, string state)> GetOAuthUrlAsync(OAuthProvider provider) {
var request = new GetOAuthUrlRequest { Provider = provider };
var response = await _authServiceClient.GetOAuthUrlAsync(request);
Debug.Log($"[AuthClient] Got OAuth URL for {provider}, state={response.State}");
return (response.AuthUrl, response.State);
}
/// <summary>
/// Poll for OAuth completion. Blocks until success, failure, or timeout.
/// The server handles the OAuth callback and token exchange.
/// Routed to Go auth service.
/// </summary>
/// <param name="state">State token from GetOAuthUrlAsync</param>
/// <returns>Response with tokens and user info on success</returns>
public async Task<CheckOAuthStatusResponse> PollForOAuthCompletionAsync(string state) {
var startTime = DateTime.UtcNow;
while (true) {
var request = new CheckOAuthStatusRequest { State = state };
var response = await _authServiceClient.CheckOAuthStatusAsync(request);
switch (response.Status) {
case OAuthStatus.Success:
Debug.Log(
$"[AuthClient] OAuth successful, user={response.User?.DisplayName}, isNew={response.IsNewUser}");
return response;
case OAuthStatus.Failed:
throw new Exception($"OAuth failed: {response.ErrorMessage}");
case OAuthStatus.Expired:
throw new Exception("OAuth session expired. Please try again.");
case OAuthStatus.Pending:
// Check timeout
if ((DateTime.UtcNow - startTime).TotalMilliseconds > PollTimeoutMs) {
throw new TimeoutException("OAuth login timed out. Please try again.");
}
// Wait before polling again
await Task.Delay(PollIntervalMs);
break;
default: throw new Exception($"Unknown OAuth status: {response.Status}");
}
}
}
/// <summary>
/// Refresh access token using stored refresh token.
/// Routed to Go auth service.
/// </summary>
/// <returns>New access token and expiry, or null if refresh failed</returns>
public async Task<RefreshTokenResponse> RefreshTokenAsync() {
var refreshToken = TokenStorage.RefreshToken;
if (string.IsNullOrEmpty(refreshToken)) {
throw new InvalidOperationException("No refresh token available");
}
var request = new RefreshTokenRequest { RefreshToken = refreshToken };
var response = await _authServiceClient.RefreshTokenAsync(request);
// Update stored access token
TokenStorage.UpdateAccessToken(response.AccessToken, response.ExpiresAt);
Debug.Log($"[AuthClient] Token refreshed, expires at {response.ExpiresAt}");
return response;
}
/// <summary>
/// Set display name for new users.
/// Requires valid JWT in interceptor.
/// Routed to Eagle.
/// </summary>
public async Task<SetDisplayNameResponse> SetDisplayNameAsync(string displayName) {
var request = new SetDisplayNameRequest { DisplayName = displayName };
var response = await _eagleClient.SetDisplayNameAsync(request);
if (response.Success) {
TokenStorage.UpdateDisplayName(displayName);
// Update the access token with the new one that has updated displayName claim
if (!string.IsNullOrEmpty(response.AccessToken)) {
// Token expires in 7 days from now
var expiresAt = DateTimeOffset.UtcNow.AddDays(7).ToUnixTimeSeconds();
TokenStorage.UpdateAccessToken(response.AccessToken, expiresAt);
Debug.Log($"[AuthClient] Display name set to: {displayName}, token updated");
} else {
Debug.Log($"[AuthClient] Display name set to: {displayName}");
}
} else {
Debug.LogWarning(
$"[AuthClient] Failed to set display name: {response.ErrorMessage}");
}
return response;
}
/// <summary>
/// Get current user info. Validates the stored JWT.
/// Routed to Eagle.
/// </summary>
public async Task<GetCurrentUserResponse> GetCurrentUserAsync() {
var response = await _eagleClient.GetCurrentUserAsync(new GetCurrentUserRequest());
Debug.Log($"[AuthClient] Current user: {response.User?.DisplayName}");
return response;
}
/// <summary>
/// Logout - invalidates refresh token on server.
/// Routed to Eagle.
/// </summary>
public async Task LogoutAsync() {
try {
await _eagleClient.LogoutAsync(new LogoutRequest());
} catch (Exception ex) {
Debug.LogWarning($"[AuthClient] Logout RPC failed (may be expected): {ex.Message}");
}
// Clear local tokens regardless of server response
TokenStorage.Clear();
Debug.Log("[AuthClient] Logged out, tokens cleared");
}
public void Dispose() {
_authServiceChannel?.Dispose();
_eagleChannel?.Dispose();
}
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 4ba94126c2d2145b8875612eaf5bc371
@@ -1,75 +0,0 @@
using System;
using Grpc.Core;
using Grpc.Core.Interceptors;
namespace Auth {
/// <summary>
/// gRPC interceptor that attaches JWT Bearer token to requests.
/// Reads token from TokenStorage on each request.
/// </summary>
public class JwtAuthInterceptor : Interceptor {
private const string HeaderName = "Authorization";
private string GetBearerHeader() {
var token = TokenStorage.AccessToken;
return string.IsNullOrEmpty(token) ? null : $"Bearer {token}";
}
public override TResponse BlockingUnaryCall<TRequest, TResponse>(
TRequest request,
ClientInterceptorContext<TRequest, TResponse> context,
BlockingUnaryCallContinuation<TRequest, TResponse> continuation) {
return continuation(request, ContextWithHeaders(context));
}
public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
TRequest request,
ClientInterceptorContext<TRequest, TResponse> context,
AsyncUnaryCallContinuation<TRequest, TResponse> continuation) {
return continuation(request, ContextWithHeaders(context));
}
public override AsyncServerStreamingCall<TResponse>
AsyncServerStreamingCall<TRequest, TResponse>(
TRequest request,
ClientInterceptorContext<TRequest, TResponse> context,
AsyncServerStreamingCallContinuation<TRequest, TResponse> continuation) {
return continuation(request, ContextWithHeaders(context));
}
public override AsyncClientStreamingCall<TRequest, TResponse>
AsyncClientStreamingCall<TRequest, TResponse>(
ClientInterceptorContext<TRequest, TResponse> context,
AsyncClientStreamingCallContinuation<TRequest, TResponse> continuation) {
return continuation(ContextWithHeaders(context));
}
public override AsyncDuplexStreamingCall<TRequest, TResponse>
AsyncDuplexStreamingCall<TRequest, TResponse>(
ClientInterceptorContext<TRequest, TResponse> context,
AsyncDuplexStreamingCallContinuation<TRequest, TResponse> continuation) {
return continuation(ContextWithHeaders(context));
}
private ClientInterceptorContext<TRequest, TResponse>
ContextWithHeaders<TRequest, TResponse>(
ClientInterceptorContext<TRequest, TResponse> original)
where TRequest : class
where TResponse : class {
var bearerHeader = GetBearerHeader();
if (string.IsNullOrEmpty(bearerHeader)) {
// No token available - return original context
// Auth service endpoints don't require token
return original;
}
var headers = new Metadata();
headers.Add(HeaderName, bearerHeader);
return new ClientInterceptorContext<TRequest, TResponse>(
original.Method,
original.Host,
original.Options.WithHeaders(headers));
}
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 8528d8ee967e042ecb2a5e8285c425e0
@@ -1,193 +0,0 @@
using System;
using System.Threading.Tasks;
using Net.Eagle0.Eagle.Api.Auth;
using UnityEngine;
namespace Auth {
/// <summary>
/// Manages OAuth authentication flow using server-mediated polling:
/// - Opens system browser for OAuth consent
/// - Polls server for OAuth completion (no deep links needed)
/// - Token storage and refresh
/// </summary>
public class OAuthManager : MonoBehaviour {
public static OAuthManager Instance { get; private set; }
private AuthClient _authClient;
private string _currentAuthServiceUrl;
private string _currentEagleUrl;
// Events for UI updates
public event Action<UserInfo> OnLoginSuccess;
public event Action<string> OnLoginFailed;
public event Action OnLogout;
public event Action<bool> OnNewUserNeedsDisplayName; // true if new user
public bool IsAuthenticated => TokenStorage.HasValidToken;
public string DisplayName => TokenStorage.DisplayName;
public string UserId => TokenStorage.UserId;
private void Awake() {
if (Instance != null && Instance != this) {
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
// Initialize TokenStorage cache from PlayerPrefs on main thread
// This allows background threads to access tokens safely
TokenStorage.InitializeCache();
// AuthClient is created lazily when SetServerUrls is called
}
/// <summary>
/// Set the server URLs for OAuth and game requests. Call this before any OAuth operations.
/// </summary>
/// <param name="authServiceUrl">Go auth service URL, e.g.
/// "https://prod.eagle0.net:40033"</param> <param name="eagleUrl">Eagle server URL, e.g.
/// "https://prod.eagle0.net:40032"</param>
public void SetServerUrls(string authServiceUrl, string eagleUrl) {
if (_currentAuthServiceUrl == authServiceUrl && _currentEagleUrl == eagleUrl &&
_authClient != null) {
return; // Already configured for these URLs
}
_authClient?.Dispose();
_currentAuthServiceUrl = authServiceUrl;
_currentEagleUrl = eagleUrl;
_authClient = new AuthClient(authServiceUrl, eagleUrl);
Debug.Log($"[OAuthManager] Configured: authService={authServiceUrl}, eagle={eagleUrl}");
}
private void OnDestroy() {
_authClient?.Dispose();
if (Instance == this) { Instance = null; }
}
private void EnsureConfigured() {
if (_authClient == null) {
throw new InvalidOperationException(
"OAuthManager not configured. Call SetServerUrls() first.");
}
}
/// <summary>
/// Start OAuth login with specified provider.
/// Opens system browser for user consent, then polls for completion.
/// </summary>
public async Task<CheckOAuthStatusResponse> LoginAsync(OAuthProvider provider) {
EnsureConfigured();
try {
// Get OAuth URL and state from server
var (authUrl, state) = await _authClient.GetOAuthUrlAsync(provider);
// Open system browser
Debug.Log($"[OAuthManager] Opening browser: {authUrl}");
Application.OpenURL(authUrl);
// Poll for OAuth completion (server handles the callback)
var response = await _authClient.PollForOAuthCompletionAsync(state);
// Store tokens
TokenStorage.StoreTokens(
response.AccessToken,
response.RefreshToken,
response.ExpiresAt,
response.User.UserId,
response.User.DisplayName ?? "");
// Notify listeners
if (response.IsNewUser) {
OnNewUserNeedsDisplayName?.Invoke(true);
} else {
OnLoginSuccess?.Invoke(response.User);
}
return response;
} catch (Exception ex) {
Debug.LogError($"[OAuthManager] Login failed: {ex.Message}");
OnLoginFailed?.Invoke(ex.Message);
throw;
}
}
/// <summary>
/// Set display name for new user after OAuth.
/// </summary>
public async Task<bool> SetDisplayNameAsync(string displayName) {
EnsureConfigured();
var response = await _authClient.SetDisplayNameAsync(displayName);
if (response.Success) { OnLoginSuccess?.Invoke(response.User); }
return response.Success;
}
/// <summary>
/// Try to restore session from stored tokens.
/// Returns true if valid session exists.
/// Must call SetServerUrls() before this method.
/// </summary>
public async Task<bool> TryRestoreSessionAsync() {
if (_authClient == null) {
Debug.Log("[OAuthManager] Not configured yet, skipping session restore");
return false;
}
if (!TokenStorage.HasValidToken && !TokenStorage.HasRefreshToken) {
Debug.Log("[OAuthManager] No stored session");
return false;
}
// If token is expired but we have refresh token, try to refresh
if (!TokenStorage.HasValidToken && TokenStorage.HasRefreshToken) {
try {
await _authClient.RefreshTokenAsync();
} catch (Exception ex) {
Debug.LogWarning($"[OAuthManager] Token refresh failed: {ex.Message}");
TokenStorage.Clear();
return false;
}
}
// Validate session with server
try {
var response = await _authClient.GetCurrentUserAsync();
Debug.Log($"[OAuthManager] Session restored for {response.User.DisplayName}");
OnLoginSuccess?.Invoke(response.User);
return true;
} catch (Exception ex) {
Debug.LogWarning($"[OAuthManager] Session validation failed: {ex.Message}");
TokenStorage.Clear();
return false;
}
}
/// <summary>
/// Refresh access token if needed.
/// Call this before making API requests.
/// </summary>
public async Task EnsureValidTokenAsync() {
EnsureConfigured();
if (TokenStorage.NeedsRefresh && TokenStorage.HasRefreshToken) {
await _authClient.RefreshTokenAsync();
}
}
/// <summary>
/// Logout and clear stored tokens.
/// </summary>
public async Task LogoutAsync() {
// LogoutAsync can work even without server connection - just clear local tokens
if (_authClient != null) {
await _authClient.LogoutAsync();
} else {
TokenStorage.Clear();
}
OnLogout?.Invoke();
}
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 0e9d5b0cb9f26477eaf26e8a09c41707
@@ -1,176 +0,0 @@
using System;
using UnityEngine;
namespace Auth {
/// <summary>
/// Secure storage for OAuth tokens using Unity's PlayerPrefs.
/// Values are cached in memory to allow access from background threads.
/// In production, consider using platform-specific secure storage
/// (Keychain on iOS, Keystore on Android).
/// </summary>
public static class TokenStorage {
private const string AccessTokenKey = "eagle0_access_token";
private const string RefreshTokenKey = "eagle0_refresh_token";
private const string ExpiresAtKey = "eagle0_expires_at";
private const string UserIdKey = "eagle0_user_id";
private const string DisplayNameKey = "eagle0_display_name";
// In-memory cache for thread-safe access from background threads
// PlayerPrefs can only be accessed from the main thread
private static string _cachedAccessToken;
private static string _cachedRefreshToken;
private static long _cachedExpiresAt;
private static string _cachedUserId;
private static string _cachedDisplayName;
private static bool _cacheInitialized = false;
/// <summary>
/// Initialize cache from PlayerPrefs. Must be called from main thread.
/// Call this early in app startup (e.g., in Awake or Start).
/// </summary>
public static void InitializeCache() {
_cachedAccessToken = PlayerPrefs.GetString(AccessTokenKey, null);
_cachedRefreshToken = PlayerPrefs.GetString(RefreshTokenKey, null);
_cachedExpiresAt =
long.TryParse(PlayerPrefs.GetString(ExpiresAtKey, "0"), out var val) ? val : 0;
_cachedUserId = PlayerPrefs.GetString(UserIdKey, null);
_cachedDisplayName = PlayerPrefs.GetString(DisplayNameKey, null);
_cacheInitialized = true;
Debug.Log(
$"[TokenStorage] Cache initialized, hasToken={!string.IsNullOrEmpty(_cachedAccessToken)}");
}
public static bool HasValidToken {
get {
var token = AccessToken;
var expiresAt = ExpiresAt;
return !string.IsNullOrEmpty(token) &&
expiresAt > DateTimeOffset.UtcNow.ToUnixTimeSeconds();
}
}
public static bool HasRefreshToken => !string.IsNullOrEmpty(RefreshToken);
public static string AccessToken {
get {
if (!_cacheInitialized) {
Debug.LogWarning(
"[TokenStorage] Cache not initialized, returning cached value anyway");
}
return _cachedAccessToken;
}
private
set {
_cachedAccessToken = value;
PlayerPrefs.SetString(AccessTokenKey, value ?? "");
}
}
public static string RefreshToken {
get => _cachedRefreshToken;
private
set {
_cachedRefreshToken = value;
PlayerPrefs.SetString(RefreshTokenKey, value ?? "");
}
}
public static long ExpiresAt {
get => _cachedExpiresAt;
private
set {
_cachedExpiresAt = value;
PlayerPrefs.SetString(ExpiresAtKey, value.ToString());
}
}
public static string UserId {
get => _cachedUserId;
private
set {
_cachedUserId = value;
PlayerPrefs.SetString(UserIdKey, value ?? "");
}
}
public static string DisplayName {
get => _cachedDisplayName;
private
set {
_cachedDisplayName = value;
PlayerPrefs.SetString(DisplayNameKey, value ?? "");
}
}
/// <summary>
/// Store tokens received from OAuth exchange.
/// </summary>
public static void StoreTokens(
string accessToken,
string refreshToken,
long expiresAt,
string userId,
string displayName) {
AccessToken = accessToken;
RefreshToken = refreshToken;
ExpiresAt = expiresAt;
UserId = userId;
DisplayName = displayName;
PlayerPrefs.Save();
Debug.Log(
$"[TokenStorage] Stored tokens for user {displayName} (expires at {expiresAt})");
}
/// <summary>
/// Update access token after refresh.
/// </summary>
public static void UpdateAccessToken(string accessToken, long expiresAt) {
AccessToken = accessToken;
ExpiresAt = expiresAt;
PlayerPrefs.Save();
Debug.Log($"[TokenStorage] Updated access token (expires at {expiresAt})");
}
/// <summary>
/// Update display name after user sets it.
/// </summary>
public static void UpdateDisplayName(string displayName) {
DisplayName = displayName;
PlayerPrefs.Save();
}
/// <summary>
/// Clear all stored tokens (logout).
/// </summary>
public static void Clear() {
_cachedAccessToken = null;
_cachedRefreshToken = null;
_cachedExpiresAt = 0;
_cachedUserId = null;
_cachedDisplayName = null;
PlayerPrefs.DeleteKey(AccessTokenKey);
PlayerPrefs.DeleteKey(RefreshTokenKey);
PlayerPrefs.DeleteKey(ExpiresAtKey);
PlayerPrefs.DeleteKey(UserIdKey);
PlayerPrefs.DeleteKey(DisplayNameKey);
PlayerPrefs.Save();
Debug.Log("[TokenStorage] Cleared all tokens");
}
/// <summary>
/// Check if token needs refresh (expires within 5 minutes).
/// </summary>
public static bool NeedsRefresh {
get {
var expiresAt = ExpiresAt;
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
return expiresAt > 0 && expiresAt - now < 300; // 5 minutes
}
}
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 4b1f130cec656466893fa04ba789d34f
@@ -29,9 +29,9 @@ RectTransform:
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: 616210323856016607}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
@@ -105,23 +105,20 @@ MonoBehaviour:
m_VerticalAlignment: 512
m_textAlignment: 65535
m_characterSpacing: 0
m_characterHorizontalScale: 1
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_TextWrappingMode: 1
m_enableWordWrapping: 1
m_wordWrappingRatios: 0.4
m_overflowMode: 0
m_linkedTextComponent: {fileID: 0}
parentLinkedComponent: {fileID: 0}
m_enableKerning: 1
m_ActiveFontFeatures: 6e72656b
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_EmojiFallbackSupport: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
m_isCullingEnabled: 0
@@ -188,9 +185,9 @@ RectTransform:
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: 616210323856016607}
m_RootOrder: 2
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
@@ -264,23 +261,20 @@ MonoBehaviour:
m_VerticalAlignment: 512
m_textAlignment: 65535
m_characterSpacing: 0
m_characterHorizontalScale: 1
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_TextWrappingMode: 1
m_enableWordWrapping: 1
m_wordWrappingRatios: 0.4
m_overflowMode: 0
m_linkedTextComponent: {fileID: 0}
parentLinkedComponent: {fileID: 0}
m_enableKerning: 1
m_ActiveFontFeatures: 6e72656b
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_EmojiFallbackSupport: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
m_isCullingEnabled: 0
@@ -350,10 +344,10 @@ RectTransform:
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: 1806945232979884877}
m_Father: {fileID: 616210323856016607}
m_RootOrder: 3
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
@@ -431,7 +425,6 @@ MonoBehaviour:
atime6: 0
atime7: 0
m_Mode: 0
m_ColorSpace: -1
m_NumColorKeys: 2
m_NumAlphaKeys: 2
_gradientType: 0
@@ -571,9 +564,9 @@ RectTransform:
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: 7057983938046569337}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
@@ -647,23 +640,20 @@ MonoBehaviour:
m_VerticalAlignment: 512
m_textAlignment: 65535
m_characterSpacing: 0
m_characterHorizontalScale: 1
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_TextWrappingMode: 1
m_enableWordWrapping: 1
m_wordWrappingRatios: 0.4
m_overflowMode: 0
m_linkedTextComponent: {fileID: 0}
parentLinkedComponent: {fileID: 0}
m_enableKerning: 1
m_ActiveFontFeatures: 6e72656b
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_EmojiFallbackSupport: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
m_isCullingEnabled: 0
@@ -711,15 +701,13 @@ RectTransform:
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: 2291298599107070085}
- {fileID: 8906549728612534620}
- {fileID: 6686534948942979506}
- {fileID: 6700597635350808166}
- {fileID: 8218751192785770547}
- {fileID: 7057983938046569337}
m_Father: {fileID: 0}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
@@ -808,295 +796,31 @@ MonoBehaviour:
playerCountField: {fileID: 8350902200036365361}
joinButton: {fileID: 437043893514773017}
buttonText: {fileID: 2170549136187147757}
dropButton: {fileID: 19903828017934214}
--- !u!1 &7127645211350815762
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 8218751192785770547}
- component: {fileID: 3533697289353260343}
m_Layer: 5
m_Name: spacer
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &8218751192785770547
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7127645211350815762}
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: 616210323856016607}
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 &3533697289353260343
--- !u!114 &2580770409874757745
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7127645211350815762}
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: 10
m_MinHeight: -1
m_PreferredWidth: 10
m_PreferredHeight: -1
m_FlexibleWidth: -1
m_FlexibleHeight: -1
m_LayoutPriority: 1
--- !u!1 &7934454793097069971
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 6700597635350808166}
- component: {fileID: 5023923008883954850}
- component: {fileID: 19903828017934214}
- component: {fileID: 2151248441631091217}
- component: {fileID: 7406323257372084547}
m_Layer: 5
m_Name: Drop Game Button
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &6700597635350808166
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7934454793097069971}
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: 1846732971937087380}
m_Father: {fileID: 616210323856016607}
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 &5023923008883954850
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7934454793097069971}
m_CullTransparentMesh: 0
--- !u!114 &19903828017934214
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7934454793097069971}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Navigation:
m_Mode: 0
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: 0.42745098, b: 0.42745098, a: 1}
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}
m_DisabledColor: {r: 1, g: 1, b: 1, a: 0.39215687}
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: Highlighted
m_DisabledTrigger: Disabled
m_Interactable: 1
m_TargetGraphic: {fileID: 2151248441631091217}
m_OnClick:
m_PersistentCalls:
m_Calls:
- m_Target: {fileID: 7332722509251528870}
m_TargetAssemblyTypeName: AvailableGameItem, Assembly-CSharp
m_MethodName: DropClicked
m_Mode: 1
m_Arguments:
m_ObjectArgument: {fileID: 0}
m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine
m_IntArgument: 0
m_FloatArgument: 0
m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
--- !u!114 &2151248441631091217
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7934454793097069971}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 21300000, guid: f03f625cc7127f244a2f4da835b92720, type: 3}
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 &7406323257372084547
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7934454793097069971}
m_GameObject: {fileID: 8906549728612534621}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
m_Name:
m_EditorClassIdentifier:
m_IgnoreLayout: 0
m_MinWidth: 50
m_MinWidth: 700
m_MinHeight: -1
m_PreferredWidth: 50
m_PreferredWidth: 700
m_PreferredHeight: -1
m_FlexibleWidth: -1
m_FlexibleWidth: 1
m_FlexibleHeight: -1
m_LayoutPriority: 1
--- !u!1 &8839297347726735035
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1846732971937087380}
- component: {fileID: 2464975641276684442}
- component: {fileID: 4051352560264867900}
m_Layer: 5
m_Name: Icon
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &1846732971937087380
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8839297347726735035}
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: 6700597635350808166}
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 &2464975641276684442
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8839297347726735035}
m_CullTransparentMesh: 1
--- !u!114 &4051352560264867900
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8839297347726735035}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 1344c3c82d62a2a41a3576d8abb8e3ea, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.RawImage
m_Material: {fileID: 0}
m_Color: {r: 0, 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_UVRect:
serializedVersion: 2
x: 0
y: 0
width: 1
height: 1
--- !u!1001 &3324269289066796519
PrefabInstance:
m_ObjectHideFlags: 0
serializedVersion: 2
m_Modification:
serializedVersion: 3
m_TransformParent: {fileID: 616210323856016607}
m_Modifications:
- target: {fileID: 6176764421848850767, guid: 59599ef87ad824f52ada88f126d58c5f,
@@ -1109,11 +833,6 @@ PrefabInstance:
propertyPath: m_fontSizeBase
value: 24
objectReference: {fileID: 0}
- target: {fileID: 6176764421848850767, guid: 59599ef87ad824f52ada88f126d58c5f,
type: 3}
propertyPath: 'm_ActiveFontFeatures.Array.data[0]'
value: 1801810542
objectReference: {fileID: 0}
- target: {fileID: 6176764421927073094, guid: 59599ef87ad824f52ada88f126d58c5f,
type: 3}
propertyPath: m_fontSize
@@ -1240,13 +959,6 @@ PrefabInstance:
value: 0
objectReference: {fileID: 0}
m_RemovedComponents: []
m_RemovedGameObjects: []
m_AddedGameObjects: []
m_AddedComponents:
- targetCorrespondingSourceObject: {fileID: 6176764422977772730, guid: 59599ef87ad824f52ada88f126d58c5f,
type: 3}
insertIndex: -1
addedObject: {fileID: 2580770409874757745}
m_SourcePrefab: {fileID: 100100000, guid: 59599ef87ad824f52ada88f126d58c5f, type: 3}
--- !u!114 &8906549728612534595 stripped
MonoBehaviour:
@@ -1272,23 +984,3 @@ GameObject:
type: 3}
m_PrefabInstance: {fileID: 3324269289066796519}
m_PrefabAsset: {fileID: 0}
--- !u!114 &2580770409874757745
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8906549728612534621}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
m_Name:
m_EditorClassIdentifier:
m_IgnoreLayout: 0
m_MinWidth: 700
m_MinHeight: -1
m_PreferredWidth: 700
m_PreferredHeight: -1
m_FlexibleWidth: 1
m_FlexibleHeight: -1
m_LayoutPriority: 1
@@ -16,10 +16,8 @@ public class AvailableGameItem : MonoBehaviour {
public TextMeshProUGUI playerCountField;
public Button joinButton;
public TextMeshProUGUI buttonText;
public Button dropButton;
public Action<long, string> JoinCallback;
public Action<long> DropCallback;
private List<AvailableLeader> availableLeaders;
private long gameId;
@@ -40,8 +38,6 @@ public class AvailableGameItem : MonoBehaviour {
public void JoinClicked() { JoinCallback(gameId, SelectedLeaderTextId); }
public void DropClicked() { DropCallback?.Invoke(gameId); }
public void
SetAvailableNewGame(long gameId, string playersString, List<AvailableLeader> leaders) {
availableLeaders = leaders;
@@ -7,18 +7,15 @@ using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Auth;
using common;
using common.GUIUtils;
using eagle;
using Grpc.Core;
using Grpc.Core.Interceptors;
using Net.Eagle0.Eagle.Api;
using Net.Eagle0.Eagle.Api.Auth;
using TMPro;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
using Object = System.Object;
public class AuthInterceptor : Interceptor {
@@ -85,23 +82,6 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
public TMP_InputField passwordField;
public TMP_Dropdown resolutionDropdown;
[Header("OAuth UI")]
public GameObject oauthPanel;
public Button discordLoginButton;
public Button googleLoginButton;
public TextMeshProUGUI oauthStatusText;
[Header("Display Name Setup")]
public GameObject displayNamePanel;
public TMP_InputField displayNameField;
public Button setDisplayNameButton;
public TextMeshProUGUI displayNameErrorText;
[Header("Legacy Auth (for testing)")]
public GameObject legacyAuthPanel;
public Button useLegacyAuthButton;
public TextMeshProUGUI useLegacyAuthButtonText;
[Header("Status Display")]
public TextMeshProUGUI connectionStatusText;
@@ -110,13 +90,6 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
public GameObject customBattlePanel;
public ErrorHandler errorHandler;
[Header("Lobby Controls")]
public Button logoutButton;
public Button customBattleButton;
public Button cancelCustomBattleButton;
public TextMeshProUGUI lobbyEnvironmentText;
public TextMeshProUGUI lobbyUserText;
public GameObject runningGamesListArea;
public GameObject runningGamesListItemPrefab;
@@ -125,8 +98,6 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
public GameObject waitingGamesListItemPrefab;
public GameObject createGameListItemPrefab;
public DropGameConfirmationPanel dropGameConfirmationPanel;
public Canvas connectionCanvas;
public Canvas eagleCanvas;
public Canvas shardokCanvas;
@@ -141,8 +112,6 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
private bool listen = false;
private CancellationTokenSource _cancellationTokenSource;
private readonly Object pendingReplyLock = new Object();
private int _connectedEnvironmentIndex = -1; // -1 means not connected
private bool _useOAuth = false; // Default to legacy until server-side OAuth is ready
public ClientPregeneratedText clientPregeneratedText;
@@ -156,33 +125,12 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
private static readonly List<string> EnvironmentDisplayNames =
new() { "prod.", "qa.", "(none)" };
/// <summary>
/// Returns the connected environment name ("prod." or "qa.") or null if not connected.
/// </summary>
public string ConnectedEnvironmentName =>
_connectedEnvironmentIndex >= 0 &&
_connectedEnvironmentIndex < EnvironmentDisplayNames.Count
? EnvironmentDisplayNames[_connectedEnvironmentIndex]
: null;
private string GetUrlFromEnvironment() {
int envIndex = environmentDropdown.value;
string prefix = EnvironmentOptions[envIndex];
return prefix + BaseDomain;
}
/// <summary>
/// Get full URL with scheme for the selected environment (Eagle server).
/// Remote servers use HTTPS, could be extended for local HTTP.
/// </summary>
private string GetEagleUrl() { return "https://" + GetUrlFromEnvironment(); }
/// <summary>
/// Get full URL with scheme for the Go auth service.
/// Always uses prod auth service - OAuth tokens work across all environments.
/// </summary>
private string GetAuthServiceUrl() { return "https://prod.eagle0.net:40033"; }
public void ReceiveLobbyUpdate(LobbyResponse lobbyResponse) {
_handleLobbyResponse(lobbyResponse);
}
@@ -218,218 +166,8 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
errorHandler.gameObject.SetActive(true);
// Initialize OAuth UI
SetupOAuthUI();
// Initialize Lobby UI (logout button, etc.)
SetupLobbyUI();
// Initialize status text
UpdateConnectionStatus();
// Try to restore existing OAuth session
TryRestoreSession();
}
private void SetupOAuthUI() {
// Set up OAuth button click handlers
if (discordLoginButton != null) {
discordLoginButton.onClick.AddListener(
() => OnOAuthLoginClicked(OAuthProvider.Discord));
}
if (googleLoginButton != null) {
googleLoginButton.onClick.AddListener(() => OnOAuthLoginClicked(OAuthProvider.Google));
}
if (setDisplayNameButton != null) {
setDisplayNameButton.onClick.AddListener(OnSetDisplayNameClicked);
}
if (useLegacyAuthButton != null) {
useLegacyAuthButton.onClick.AddListener(OnUseLegacyAuthClicked);
}
// Subscribe to OAuthManager events
if (OAuthManager.Instance != null) {
OAuthManager.Instance.OnLoginSuccess += OnOAuthLoginSuccess;
OAuthManager.Instance.OnLoginFailed += OnOAuthLoginFailed;
OAuthManager.Instance.OnNewUserNeedsDisplayName += OnNewUserNeedsDisplayName;
OAuthManager.Instance.OnLogout += OnOAuthLogout;
}
// Show appropriate panel based on auth state
ShowAuthPanel();
}
private void ShowAuthPanel() {
// Hide all auth panels first
if (oauthPanel != null) oauthPanel.SetActive(false);
if (displayNamePanel != null) displayNamePanel.SetActive(false);
if (legacyAuthPanel != null) legacyAuthPanel.SetActive(false);
// Show appropriate panel based on _useOAuth
if (_useOAuth) {
if (oauthPanel != null) oauthPanel.SetActive(true);
if (useLegacyAuthButtonText != null)
useLegacyAuthButtonText.text = "Use Classic Sign-in";
} else {
if (legacyAuthPanel != null) legacyAuthPanel.SetActive(true);
if (useLegacyAuthButtonText != null) useLegacyAuthButtonText.text = "Use OAuth Sign-in";
}
}
private void SetupLobbyUI() {
if (logoutButton != null) { logoutButton.onClick.AddListener(OnLogoutClicked); }
if (customBattleButton != null) { customBattleButton.onClick.AddListener(CustomBattle); }
if (cancelCustomBattleButton != null) {
cancelCustomBattleButton.onClick.AddListener(CancelCustomBattle);
}
}
private void UpdateLobbyStatusDisplays() {
// Show environment (prod/qa)
if (lobbyEnvironmentText != null) {
lobbyEnvironmentText.text = ConnectedEnvironmentName ?? "";
}
// Show current user - prefer OAuth display name, fall back to classic login name
if (lobbyUserText != null) {
if (_useOAuth && !string.IsNullOrEmpty(TokenStorage.DisplayName)) {
lobbyUserText.text = TokenStorage.DisplayName;
} else {
lobbyUserText.text = nameField.text;
}
}
}
private async void OnLogoutClicked() {
Debug.Log("[ConnectionHandler] Logout button clicked");
// Clear OAuth tokens
if (OAuthManager.Instance != null) { await OAuthManager.Instance.LogoutAsync(); }
// Disconnect from server
_persistentClientConnection?.Dispose();
eagleConnection?.Dispose();
_httpClient?.Dispose();
_cancellationTokenSource?.Dispose();
_persistentClientConnection = null;
eagleConnection = null;
_httpClient = null;
_cancellationTokenSource = null;
_connectedEnvironmentIndex = -1;
// Return to connection screen
gameSelectionPanel.SetActive(false);
connectionPanel.SetActive(true);
// Re-initialize the auth UI
ShowAuthPanel();
UpdateConnectionStatus();
}
private async void TryRestoreSession() {
if (OAuthManager.Instance == null) return;
// Configure OAuthManager with auth service and Eagle URLs
OAuthManager.Instance.SetServerUrls(GetAuthServiceUrl(), GetEagleUrl());
if (oauthStatusText != null) { oauthStatusText.text = "Checking for existing session..."; }
var restored = await OAuthManager.Instance.TryRestoreSessionAsync();
if (restored) {
// Session restored, connect to lobby
ConnectWithOAuth();
} else if (oauthStatusText != null) {
oauthStatusText.text = "";
}
}
private async void OnOAuthLoginClicked(OAuthProvider provider) {
// Configure OAuthManager with auth service and Eagle URLs (user may have changed dropdown)
OAuthManager.Instance.SetServerUrls(GetAuthServiceUrl(), GetEagleUrl());
if (oauthStatusText != null) {
oauthStatusText.text = $"Opening browser for {provider} login...";
}
try {
await OAuthManager.Instance.LoginAsync(provider);
// OnLoginSuccess or OnNewUserNeedsDisplayName will be called
} catch (Exception ex) {
Debug.LogError($"OAuth login failed: {ex.Message}");
if (oauthStatusText != null) { oauthStatusText.text = $"Login failed: {ex.Message}"; }
}
}
private void OnOAuthLoginSuccess(UserInfo user) {
MainQueue.Q.Enqueue(() => {
if (oauthStatusText != null) { oauthStatusText.text = $"Welcome, {user.DisplayName}!"; }
ConnectWithOAuth();
});
}
private void OnOAuthLoginFailed(string error) {
MainQueue.Q.Enqueue(() => {
if (oauthStatusText != null) { oauthStatusText.text = $"Login failed: {error}"; }
});
}
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();
if (oauthStatusText != null) { oauthStatusText.text = ""; }
});
}
private void OnUseLegacyAuthClicked() {
_useOAuth = !_useOAuth; // Toggle instead of just setting false
if (_useOAuth) {
// Show OAuth panel
if (oauthPanel != null) oauthPanel.SetActive(true);
if (legacyAuthPanel != null) legacyAuthPanel.SetActive(false);
if (useLegacyAuthButtonText != null)
useLegacyAuthButtonText.text = "Use Classic Sign-in";
} else {
// Show legacy panel
if (oauthPanel != null) oauthPanel.SetActive(false);
if (legacyAuthPanel != null) legacyAuthPanel.SetActive(true);
if (useLegacyAuthButtonText != null) useLegacyAuthButtonText.text = "Use OAuth Sign-in";
}
}
private void ConnectWithOAuth() {
_useOAuth = true;
_internalConnectEagle();
}
private float _statusUpdateTimer = 0f;
@@ -507,14 +245,6 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
public void CustomBattle() { _internalCustomBattle(); }
public void CancelCustomBattle() {
customBattlePanel.SetActive(false);
shardokCanvas.gameObject.SetActive(false);
connectionCanvas.gameObject.SetActive(true);
gameSelectionPanel.SetActive(true);
StartListeningForLobbyUpdates();
}
public void OnApplicationQuit() { Dispose(); }
public void Dispose() {
@@ -532,8 +262,6 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
_persistentClientConnection = null;
eagleConnection?.Dispose();
eagleConnection = null;
_connectedEnvironmentIndex = -1;
}
public void OnResolutionChanged(int dropdownIndex) {
@@ -561,9 +289,6 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
connectionPanel.gameObject.SetActive(false);
gameSelectionPanel.gameObject.SetActive(true);
// Update lobby status displays
UpdateLobbyStatusDisplays();
// Set up running games table
foreach (Transform row in runningGamesListArea.transform) { Destroy(row.gameObject); }
foreach (var runningGame in lobbyResponse.RunningGames) {
@@ -572,10 +297,10 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
runningGamesListArea.transform,
true);
listItem.transform.localScale = new Vector3 { x = 1.0f, y = 1.0f, z = 1.0f };
var runningGameItem = listItem.GetComponent<RunningGameItem>();
runningGameItem.SetRunningGame(runningGame.GameId, runningGame.Leader);
runningGameItem.GoCallback = this.SelectEagleGame;
runningGameItem.DropCallback = this.DropGame;
listItem.GetComponent<RunningGameItem>().SetRunningGame(
runningGame.GameId,
runningGame.Leader);
listItem.GetComponent<RunningGameItem>().GoCallback = this.SelectEagleGame;
}
// Set up available/waiting games table
@@ -611,16 +336,14 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
availableGamesListArea.transform,
true);
listItem.transform.localScale = new Vector3 { x = 1.0f, y = 1.0f, z = 1.0f };
var availableGameItem = listItem.GetComponent<AvailableGameItem>();
availableGameItem.SetAvailableNewGame(
listItem.GetComponent<AvailableGameItem>().SetAvailableNewGame(
availableGame.GameId,
string.Format(
"{0} / {1}",
availableGame.CurrentHumanPlayerCount,
availableGame.MaxHumanPlayerCount),
availableGame.AvailableLeaders.ToList());
availableGameItem.JoinCallback = this.JoinGame;
availableGameItem.DropCallback = this.DropGame;
listItem.GetComponent<AvailableGameItem>().JoinCallback = this.JoinGame;
}
});
}
@@ -634,8 +357,7 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
}
private void _createConnection() {
_connectedEnvironmentIndex = environmentDropdown.value;
PlayerPrefs.SetInt(EnvironmentKey, _connectedEnvironmentIndex);
PlayerPrefs.SetInt(EnvironmentKey, environmentDropdown.value);
PlayerPrefs.SetString(NameKey, nameField.text);
PlayerPrefs.SetString(PasswordKey, passwordField.text);
@@ -649,26 +371,7 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
_cancellationTokenSource = new CancellationTokenSource();
string url = GetUrlFromEnvironment();
if (_useOAuth && TokenStorage.HasValidToken) {
// Use JWT-based connection
eagleConnection = EagleConnection.CreateWithJwt(url);
_httpClient = new HttpClient();
_httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", TokenStorage.AccessToken);
} else {
// Legacy Basic Auth connection
PlayerPrefs.SetString(NameKey, nameField.text);
PlayerPrefs.SetString(PasswordKey, passwordField.text);
eagleConnection = new EagleConnection(nameField.text, passwordField.text, url);
_httpClient = new HttpClient();
_httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Basic", eagleConnection.authHeader);
}
eagleConnection = new EagleConnection(nameField.text, passwordField.text, url);
_persistentClientConnection = new PersistentClientConnection(
eagleConnection.EagleGrpcClient,
eagleConnection.credentials,
@@ -676,16 +379,20 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
_persistentClientConnection.Connect();
errorHandler.PersistentClientConnection = _persistentClientConnection;
_httpClient = new HttpClient();
_httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Basic", eagleConnection.authHeader);
ResourceFetcher.SetUpConnection(_httpClient);
}
private void _internalCustomBattle() {
// Connection already exists when called from lobby
_createConnection();
_persistentClientConnection.SetLobbySubscriber(this);
SetCustomBattleActive(true);
gameSelectionPanel.SetActive(false);
connectionPanel.SetActive(false);
customBattlePanel.SetActive(true);
}
@@ -733,18 +440,6 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
var game = _internalJoinEagleGame(gameId, leaderName);
}
private void DropGame(long gameId) {
if (dropGameConfirmationPanel != null) {
dropGameConfirmationPanel.OnConfirm = ConfirmDropGame;
dropGameConfirmationPanel.Show(gameId, "Are you sure you want to leave this game?");
} else {
// Fallback if no confirmation panel is configured
ConfirmDropGame(gameId);
}
}
private void ConfirmDropGame(long gameId) { _internalDropGame(gameId); }
private void CreateGame(string leaderNameTextId, int humanPlayerCount, int totalPlayerCount) {
var game = _internalCreateGame(leaderNameTextId, humanPlayerCount, totalPlayerCount);
}
@@ -821,18 +516,6 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
}
}
private async Task<bool> _internalDropGame(long gameId) {
try {
return await _persistentClientConnection.SendUpdateStreamRequestAsync(
new UpdateStreamRequest {
DropGameRequest = new DropGameRequest { GameId = gameId }
});
} catch (WebException e) {
Debug.Log(String.Format("Caught exception in DropGame {0}", e.Message));
return false;
}
}
private void SetLobbyActive(bool active) {
runningGamesListArea.SetActive(active);
availableGamesListArea.SetActive(active);
@@ -850,8 +533,7 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
eagleCanvas.GetComponent<EagleGameController>().SetUpGame(
gameInfo.GameId,
gameInfo.FactionId,
_persistentClientConnection,
ConnectedEnvironmentName);
_persistentClientConnection);
connectionCanvas.enabled = false;
connectionCanvas.gameObject.SetActive(false);
eagleCanvas.gameObject.SetActive(true);
@@ -1,24 +0,0 @@
using System;
using TMPro;
using UnityEngine;
public class DropGameConfirmationPanel : MonoBehaviour {
public TextMeshProUGUI messageText;
public Action<long> OnConfirm;
private long _pendingGameId;
public void Show(long gameId, string message) {
_pendingGameId = gameId;
if (messageText != null) { messageText.text = message; }
gameObject.SetActive(true);
}
public void ConfirmClicked() {
gameObject.SetActive(false);
OnConfirm?.Invoke(_pendingGameId);
}
public void CancelClicked() { gameObject.SetActive(false); }
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 558d4c7321daa4fe18dc4759a130d919
@@ -29,9 +29,9 @@ RectTransform:
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: 909271087739711220}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
@@ -105,23 +105,20 @@ MonoBehaviour:
m_VerticalAlignment: 512
m_textAlignment: 65535
m_characterSpacing: 0
m_characterHorizontalScale: 1
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_TextWrappingMode: 1
m_enableWordWrapping: 1
m_wordWrappingRatios: 0.4
m_overflowMode: 0
m_linkedTextComponent: {fileID: 0}
parentLinkedComponent: {fileID: 0}
m_enableKerning: 1
m_ActiveFontFeatures: 6e72656b
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_EmojiFallbackSupport: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
m_isCullingEnabled: 0
@@ -188,9 +185,9 @@ RectTransform:
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: 909271087739711220}
m_RootOrder: 1
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
@@ -264,23 +261,20 @@ MonoBehaviour:
m_VerticalAlignment: 512
m_textAlignment: 65535
m_characterSpacing: 0
m_characterHorizontalScale: 1
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_TextWrappingMode: 1
m_enableWordWrapping: 1
m_wordWrappingRatios: 0.4
m_overflowMode: 0
m_linkedTextComponent: {fileID: 0}
parentLinkedComponent: {fileID: 0}
m_enableKerning: 1
m_ActiveFontFeatures: 6e72656b
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_EmojiFallbackSupport: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
m_isCullingEnabled: 0
@@ -318,216 +312,6 @@ MonoBehaviour:
m_FlexibleWidth: 1
m_FlexibleHeight: -1
m_LayoutPriority: 1
--- !u!1 &4189605341159952945
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 7336928442537586311}
- component: {fileID: 3344684898730655831}
m_Layer: 5
m_Name: spacer
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &7336928442537586311
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4189605341159952945}
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: 909271087739711220}
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 &3344684898730655831
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4189605341159952945}
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: 10
m_MinHeight: -1
m_PreferredWidth: 10
m_PreferredHeight: -1
m_FlexibleWidth: -1
m_FlexibleHeight: -1
m_LayoutPriority: 1
--- !u!1 &4678275134637000934
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1348356180262758599}
- component: {fileID: 8617884038396701234}
- component: {fileID: 4081637582368108930}
- component: {fileID: 5167949463556339992}
- component: {fileID: 7482580409419131173}
m_Layer: 5
m_Name: Drop Game Button
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &1348356180262758599
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4678275134637000934}
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: 6454652926630703861}
m_Father: {fileID: 909271087739711220}
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 &8617884038396701234
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4678275134637000934}
m_CullTransparentMesh: 0
--- !u!114 &4081637582368108930
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4678275134637000934}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Navigation:
m_Mode: 0
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: 0.42745098, b: 0.42745098, a: 1}
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}
m_DisabledColor: {r: 1, g: 1, b: 1, a: 0.39215687}
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: Highlighted
m_DisabledTrigger: Disabled
m_Interactable: 1
m_TargetGraphic: {fileID: 5167949463556339992}
m_OnClick:
m_PersistentCalls:
m_Calls:
- m_Target: {fileID: 7788402836057688155}
m_TargetAssemblyTypeName: RunningGameItem, Assembly-CSharp
m_MethodName: DropClicked
m_Mode: 1
m_Arguments:
m_ObjectArgument: {fileID: 0}
m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine
m_IntArgument: 0
m_FloatArgument: 0
m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
--- !u!114 &5167949463556339992
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4678275134637000934}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 21300000, guid: f03f625cc7127f244a2f4da835b92720, type: 3}
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 &7482580409419131173
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4678275134637000934}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
m_Name:
m_EditorClassIdentifier:
m_IgnoreLayout: 0
m_MinWidth: 0
m_MinHeight: -1
m_PreferredWidth: 50
m_PreferredHeight: -1
m_FlexibleWidth: -1
m_FlexibleHeight: -1
m_LayoutPriority: 1
--- !u!1 &4703848583262435150
GameObject:
m_ObjectHideFlags: 0
@@ -556,9 +340,9 @@ RectTransform:
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: 7341333336313446738}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
@@ -632,23 +416,20 @@ MonoBehaviour:
m_VerticalAlignment: 512
m_textAlignment: 65535
m_characterSpacing: 0
m_characterHorizontalScale: 1
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_TextWrappingMode: 1
m_enableWordWrapping: 1
m_wordWrappingRatios: 0.4
m_overflowMode: 0
m_linkedTextComponent: {fileID: 0}
parentLinkedComponent: {fileID: 0}
m_enableKerning: 1
m_ActiveFontFeatures: 6e72656b
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_EmojiFallbackSupport: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
m_isCullingEnabled: 0
@@ -698,10 +479,10 @@ RectTransform:
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: 2089905691925287270}
m_Father: {fileID: 909271087739711220}
m_RootOrder: 2
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
@@ -779,7 +560,6 @@ MonoBehaviour:
atime6: 0
atime7: 0
m_Mode: 0
m_ColorSpace: -1
m_NumColorKeys: 2
m_NumAlphaKeys: 2
_gradientType: 0
@@ -922,14 +702,12 @@ RectTransform:
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: 2008335870574158510}
- {fileID: 6674238761151365000}
- {fileID: 1348356180262758599}
- {fileID: 7336928442537586311}
- {fileID: 7341333336313446738}
m_Father: {fileID: 0}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
@@ -1015,8 +793,7 @@ MonoBehaviour:
item: {fileID: 5643565463360785033}
gameIdField: {fileID: 8058295588038032232}
leaderField: {fileID: 3311450659768657245}
goButton: {fileID: 145214844678457394}
dropButton: {fileID: 4081637582368108930}
goButton: {fileID: 0}
--- !u!114 &2208043217657811503
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -1037,75 +814,3 @@ MonoBehaviour:
m_FlexibleWidth: 1
m_FlexibleHeight: 1
m_LayoutPriority: 1
--- !u!1 &7814378479006347708
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 6454652926630703861}
- component: {fileID: 374667974001408770}
- component: {fileID: 5621315126805048060}
m_Layer: 5
m_Name: Icon
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &6454652926630703861
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7814378479006347708}
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: 1348356180262758599}
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 &374667974001408770
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7814378479006347708}
m_CullTransparentMesh: 1
--- !u!114 &5621315126805048060
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7814378479006347708}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 1344c3c82d62a2a41a3576d8abb8e3ea, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.RawImage
m_Material: {fileID: 0}
m_Color: {r: 0, 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_UVRect:
serializedVersion: 2
x: 0
y: 0
width: 1
height: 1
@@ -10,10 +10,8 @@ public class RunningGameItem : MonoBehaviour {
public TextMeshProUGUI gameIdField;
public TextMeshProUGUI leaderField;
public Button goButton;
public Button dropButton;
public Action<long> GoCallback;
public Action<long> DropCallback;
private long gameId;
@@ -22,8 +20,6 @@ public class RunningGameItem : MonoBehaviour {
public void GoClicked() { GoCallback(gameId); }
public void DropClicked() { DropCallback?.Invoke(gameId); }
public void SetRunningGame(long gameId, AvailableLeader leader) {
this.gameId = gameId;
@@ -0,0 +1,88 @@
fileFormatVersion: 2
guid: b853582537dc740aabe996b607191fe8
TextureImporter:
fileIDToRecycleName: {}
externalObjects: {}
serializedVersion: 9
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
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: -1
aniso: -1
mipBias: -100
wrapU: -1
wrapV: -1
wrapW: -1
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
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
platformSettings:
- serializedVersion: 2
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID:
vertices: []
indices:
edges: []
weights: []
spritePackingTag:
pSDRemoveMatte: 0
pSDShowRemoveMatteOption: 0
userData:
assetBundleName:
assetBundleVariant:
@@ -148,9 +148,6 @@ namespace eagle {
toggle.gameObject.SetActive(true);
toggle.interactable = available;
// Turn off unavailable toggles so they don't appear selected
if (!available) { toggle.isOn = false; }
// Only add to toggle group if available
toggle.group = available ? improvementToggleGroup : null;
@@ -26,10 +26,6 @@ namespace eagle {
private TextMeshProUGUI _textComponent;
private PersistentClientConnection _connection;
private IGameStateProvider _gameStateProvider;
private string _environmentName;
[Tooltip("Optional text component to display connected environment (prod/qa)")]
public TextMeshProUGUI environmentIndicatorText;
[Tooltip("Optional button to force immediate reconnection when server is down")]
public Button retryButton;
@@ -68,15 +64,6 @@ namespace eagle {
_gameStateProvider = provider;
}
/// <summary>
/// Set the environment name to display (e.g., "prod." or "qa.").
/// Pass null to clear the indicator.
/// </summary>
public void SetEnvironment(string environmentName) {
_environmentName = environmentName;
UpdateEnvironmentIndicator();
}
void Update() {
if (_connection == null || _textComponent == null) { return; }
@@ -178,20 +165,5 @@ namespace eagle {
private void OnRetryClicked() {
if (_connection != null) { _connection.ForceReconnect(); }
}
private void UpdateEnvironmentIndicator() {
if (environmentIndicatorText == null) return;
if (string.IsNullOrEmpty(_environmentName)) {
environmentIndicatorText.text = "";
return;
}
// Color prod green, qa yellow
environmentIndicatorText.text =
_environmentName switch { "prod." => $"<color=green>{_environmentName}</color>",
"qa." => $"<color=yellow>{_environmentName}</color>",
_ => _environmentName };
}
}
}
@@ -243,8 +243,7 @@ namespace eagle {
public void SetUpGame(
long gameId,
int? playerId,
PersistentClientConnection persistentClientConnection,
string environmentName = null) {
PersistentClientConnection persistentClientConnection) {
ModelUpdater = new GameModelUpdater(
gameId,
playerId,
@@ -270,12 +269,9 @@ namespace eagle {
ModelUpdater.ErrorHandler = errorHandler;
// Set up game state provider and environment for connection status UI
// Set up game state provider for connection status UI
var statusUI = connectionStatusLabel.GetComponent<ConnectionStatusUI>();
if (statusUI != null) {
statusUI.SetGameStateProvider(ModelUpdater);
statusUI.SetEnvironment(environmentName);
}
if (statusUI != null) { statusUI.SetGameStateProvider(ModelUpdater); }
// Fire-and-forget - subscription is awaited internally and failures are logged
MainQueue.Q.EnqueueForNextUpdate(
@@ -514,22 +514,13 @@ namespace eagle {
_connectionLogger.LogLine(
$"[STATE_RESYNC] timestamp={timestamp} round={startingState.CurrentRoundId} factions={startingState.Factions.Count} heroes={startingState.Heroes.Count}");
// Clear all stale state before applying new starting state.
// This handles rewind/reset scenarios where the server sends an earlier state.
_currentModel.ShardokGameModels.Clear();
_shardokResultCounts.Clear();
_shardokNeedsResync.Clear();
_currentModel.CommandToken = null;
_currentModel.LastPostedToken = -1;
_currentModel.AvailableCommandsByProvince.Clear();
_commandSubmittedTime = null;
_currentModel.GsView = startingState;
_currentModel.BattalionTypes =
startingState.BattalionTypes.ToDictionary(bt => bt.TypeId, bt => bt);
_currentModel.ChronicleEntries = startingState.ChronicleEntries.ToList();
// For any outstanding battles in the new state, create models and mark for resync.
// For any outstanding battles not in ShardokGameModels, create models and mark for
// resync. This ensures fresh clients get Shardok state for ongoing battles.
bool needsResubscribe = false;
foreach (var battle in _currentModel.ShardokBattles) {
if (!_currentModel.ShardokGameModels.ContainsKey(battle.ShardokGameId)) {
@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: e661753a1e43a4f2da81b76457cf55c8
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -981,69 +981,54 @@ namespace eagle {
HandleGameUpdate(current.GameUpdate, receivedTime);
break;
case UpdateStreamResponse.ResponseDetailsOneofCase.LobbyResponse: {
// Capture subscriber reference to avoid race condition - if
// subscriber changes or is cleared during dispose, we deliver to
// whoever was subscribed when the message arrived
var lobbySubscriber = _lobbySubscriber;
if (lobbySubscriber != null) {
case UpdateStreamResponse.ResponseDetailsOneofCase.LobbyResponse:
if (_lobbySubscriber != null) {
MainQueue.Q.Enqueue(() => {
lobbySubscriber.ReceiveLobbyUpdate(current.LobbyResponse);
_lobbySubscriber.ReceiveLobbyUpdate(current.LobbyResponse);
});
}
break;
}
case UpdateStreamResponse.ResponseDetailsOneofCase
.PregeneratedTextResponse: {
var lobbySubscriber = _lobbySubscriber;
if (lobbySubscriber != null) {
.PregeneratedTextResponse:
if (_lobbySubscriber != null) {
MainQueue.Q.Enqueue(() => {
lobbySubscriber.ReceivePregeneratedTextUpdate(
_lobbySubscriber.ReceivePregeneratedTextUpdate(
current.PregeneratedTextResponse);
});
}
break;
}
case UpdateStreamResponse.ResponseDetailsOneofCase.JoinGameResponse: {
var lobbySubscriber = _lobbySubscriber;
if (lobbySubscriber != null) {
case UpdateStreamResponse.ResponseDetailsOneofCase.JoinGameResponse:
if (_lobbySubscriber != null) {
MainQueue.Q.Enqueue(() => {
lobbySubscriber.HandleJoinGameResponse(
_lobbySubscriber.HandleJoinGameResponse(
current.JoinGameResponse);
});
}
break;
}
case UpdateStreamResponse.ResponseDetailsOneofCase.CreateGameResponse: {
var lobbySubscriber = _lobbySubscriber;
if (lobbySubscriber != null) {
case UpdateStreamResponse.ResponseDetailsOneofCase.CreateGameResponse:
if (_lobbySubscriber != null) {
MainQueue.Q.Enqueue(() => {
lobbySubscriber.HandleCreateGameResponse(
_lobbySubscriber.HandleCreateGameResponse(
current.CreateGameResponse);
});
}
break;
}
case UpdateStreamResponse.ResponseDetailsOneofCase
.CustomBattleResponse: {
var lobbySubscriber = _lobbySubscriber;
if (lobbySubscriber != null) {
case UpdateStreamResponse.ResponseDetailsOneofCase.CustomBattleResponse:
if (_lobbySubscriber != null) {
MainQueue.Q.Enqueue(() => {
lobbySubscriber.HandleCustomBattleResponse(
_lobbySubscriber.HandleCustomBattleResponse(
current.CustomBattleResponse);
});
}
break;
}
case UpdateStreamResponse.ResponseDetailsOneofCase.HexMapResponse:
var hexMapResponse = current.HexMapResponse;
@@ -1087,21 +1072,7 @@ namespace eagle {
var tokenCancelled = _currentThreadToken.IsCancellationRequested;
LogFlow($"HandleStreamingCall ENDED normally: sc_null={scNull} token_cancelled={tokenCancelled}");
_remoteEagleClientLogger.LogLine(
$"Stream ended normally: sc_null={scNull} token_cancelled={tokenCancelled}");
// Only schedule reconnect if:
// 1. App is not shutting down (main token not cancelled)
// 2. Thread token was NOT cancelled (if it was, something else like
// SyncMismatch already disposed the call and scheduled reconnect)
if (!_cancellationToken.IsCancellationRequested && !tokenCancelled) {
lock (this) {
_lastDisconnect = DateTime.UtcNow;
MarkAllShardokGamesForResync();
CancelAllPendingSubscriptionAcks();
}
LogConnectionEvent("disconnect", "StreamEndedNormally");
ScheduleReconnect("StreamEndedNormally");
}
"How did we get here? This is not my beautiful wife!");
} catch (RpcException e) {
lock (this) {
_lastDisconnect = DateTime.UtcNow;
Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

@@ -1,9 +1,9 @@
fileFormatVersion: 2
guid: 1850a7332b6e84e1a9b69f5f5b4cf482
guid: aa2fcd8d8f12d4218aa9ee793adfe434
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
serializedVersion: 12
mipmaps:
mipMapMode: 0
enableMipMap: 1
@@ -20,12 +20,11 @@ TextureImporter:
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
ignoreMasterTextureLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
@@ -63,11 +62,10 @@ TextureImporter:
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
applyGammaDecoding: 1
cookieLightType: 1
platformSettings:
- serializedVersion: 4
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
@@ -77,10 +75,9 @@ TextureImporter:
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
- serializedVersion: 3
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
@@ -90,14 +87,24 @@ TextureImporter:
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Server
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID:
@@ -107,11 +114,10 @@ TextureImporter:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
spritePackingTag:
pSDRemoveMatte: 0
pSDShowRemoveMatteOption: 0
userData:
assetBundleName:
assetBundleVariant:
@@ -1,6 +1,5 @@
using System;
using System.Threading;
using Auth;
using Cysharp.Net.Http;
using eagle;
using Grpc.Core;
@@ -58,25 +57,6 @@ public class EagleConnection : IDisposable {
};
}
private CallInvoker MakeJwtInvoker(string url) {
var jwtInterceptor = new JwtAuthInterceptor();
_channel = GrpcChannel.ForAddress(
"https://" + url,
new GrpcChannelOptions {
HttpHandler = CreateHttpHandler(),
DisposeHttpClient = true,
LoggerFactory = _loggerFactory,
MaxReceiveMessageSize = null
});
var invoker = _channel.Intercept(jwtInterceptor);
return invoker;
}
/// <summary>
/// Create connection using Basic Auth (legacy).
/// </summary>
public EagleConnection(string playerName, string password, string url) {
this.playerName = playerName;
credentials = new Metadata { { "user", playerName } };
@@ -88,26 +68,6 @@ public class EagleConnection : IDisposable {
EagleGrpcClient = new Eagle.EagleClient(MakeInvoker(playerName, password, url));
}
/// <summary>
/// Create connection using JWT Bearer token auth.
/// Tokens are read from TokenStorage on each request.
/// </summary>
public static EagleConnection CreateWithJwt(string url) {
return new EagleConnection(url, useJwt: true);
}
private EagleConnection(string url, bool useJwt) {
// For JWT auth, get display name from TokenStorage
playerName = TokenStorage.DisplayName ?? "Unknown";
credentials = new Metadata { { "user", TokenStorage.UserId ?? "" } };
authHeader = null; // Not used for JWT
_loggerFactory = LoggerFactory.Create(
builder => { builder.AddProvider(new CustomFileLoggerProvider()); });
EagleGrpcClient = new Eagle.EagleClient(MakeJwtInvoker(url));
}
public void Dispose() {
try {
_channel?.Dispose();
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,123 @@
fileFormatVersion: 2
guid: bce4d1b8797fe3f4c924defe072627c4
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 10
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
streamingMipmaps: 0
streamingMipmapsPriority: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: -1
mipBias: -100
wrapU: 1
wrapV: 1
wrapW: 1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 2
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: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
- serializedVersion: 2
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
- serializedVersion: 2
buildTarget: iPhone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
- serializedVersion: 2
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID: be6d2762a8fff49759a30689bd447303
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spritePackingTag:
pSDRemoveMatte: 0
pSDShowRemoveMatteOption: 0
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,90 @@
fileFormatVersion: 2
guid: fc00df0d402d141999c4b8bf71901287
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 10
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
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: -1
aniso: -1
mipBias: -100
wrapU: -1
wrapV: -1
wrapW: -1
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
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
platformSettings:
- serializedVersion: 2
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spritePackingTag:
pSDRemoveMatte: 0
pSDShowRemoveMatteOption: 0
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,95 @@
fileFormatVersion: 2
guid: 5b958d07f08ec0045ab8997382e9ad8f
labels:
- Bow
- Weapon
- Item
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 10
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
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: -1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: -1
aniso: 16
mipBias: -100
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: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spritePackingTag:
pSDRemoveMatte: 0
pSDShowRemoveMatteOption: 0
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,88 @@
fileFormatVersion: 2
guid: 354f50a479c2a4ca3bf062e12f528f1c
TextureImporter:
fileIDToRecycleName: {}
externalObjects: {}
serializedVersion: 7
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
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: -1
aniso: -1
mipBias: -100
wrapU: -1
wrapV: -1
wrapW: -1
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
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
platformSettings:
- serializedVersion: 2
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID:
vertices: []
indices:
edges: []
weights: []
spritePackingTag:
pSDRemoveMatte: 0
pSDShowRemoveMatteOption: 0
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,91 @@
fileFormatVersion: 2
guid: 1c5eda65433424fb183c62967354168b
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 10
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
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: -1
aniso: -1
mipBias: -100
wrapU: -1
wrapV: -1
wrapW: -1
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
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
platformSettings:
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spritePackingTag:
pSDRemoveMatte: 0
pSDShowRemoveMatteOption: 0
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,92 @@
fileFormatVersion: 2
guid: e6b5b8b76515f4724bf8e3f75512c826
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 11
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
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: -1
aniso: -1
mipBias: -100
wrapU: -1
wrapV: -1
wrapW: -1
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
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
applyGammaDecoding: 0
platformSettings:
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spritePackingTag:
pSDRemoveMatte: 0
pSDShowRemoveMatteOption: 0
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,91 @@
fileFormatVersion: 2
guid: 920f658e9b2e44c0d84fc84ea483a6af
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 10
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
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: -1
aniso: -1
mipBias: -100
wrapU: -1
wrapV: -1
wrapW: -1
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
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
platformSettings:
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spritePackingTag:
pSDRemoveMatte: 0
pSDShowRemoveMatteOption: 0
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,91 @@
fileFormatVersion: 2
guid: 87492a2ffb6094da1a19b0fa5a2ace37
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 10
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
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: -1
aniso: -1
mipBias: -100
wrapU: -1
wrapV: -1
wrapW: -1
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
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
platformSettings:
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spritePackingTag:
pSDRemoveMatte: 0
pSDShowRemoveMatteOption: 0
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,92 @@
fileFormatVersion: 2
guid: ba9c4ac960a7f474497a6dcb931bc6ed
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 11
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
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: -1
aniso: -1
mipBias: -100
wrapU: -1
wrapV: -1
wrapW: -1
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
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
applyGammaDecoding: 0
platformSettings:
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spritePackingTag:
pSDRemoveMatte: 0
pSDShowRemoveMatteOption: 0
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,77 @@
fileFormatVersion: 2
guid: d8f6ee79830a5438db6f1473843bcd6c
timeCreated: 1529459554
licenseType: Free
TextureImporter:
fileIDToRecycleName: {}
externalObjects: {}
serializedVersion: 4
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
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: 1
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spritePixelsToUnits: 100
alphaUsage: 1
alphaIsTransparency: 0
spriteTessellationDetail: -1
textureType: 0
textureShape: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
platformSettings:
- buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
spritePackingTag:
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,117 @@
fileFormatVersion: 2
guid: 4565e6df1495b47e3be58c8cc369f629
timeCreated: 1522356074
licenseType: Free
TextureImporter:
fileIDToRecycleName: {}
externalObjects: {}
serializedVersion: 4
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
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: 1
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spritePixelsToUnits: 100
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 0
textureShape: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
platformSettings:
- buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
- buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
- buildTarget: iPhone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
- buildTarget: tvOS
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
- buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
spritePackingTag:
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,91 @@
fileFormatVersion: 2
guid: b6b1b3d2045fa49cea45fe4aa4b82ec8
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 10
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
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: -1
aniso: -1
mipBias: -100
wrapU: -1
wrapV: -1
wrapW: -1
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
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
platformSettings:
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spritePackingTag:
pSDRemoveMatte: 0
pSDShowRemoveMatteOption: 0
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,91 @@
fileFormatVersion: 2
guid: 71af2849c68844cff84a25a9dae65f15
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 10
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
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: -1
aniso: -1
mipBias: -100
wrapU: -1
wrapV: -1
wrapW: -1
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
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
platformSettings:
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spritePackingTag:
pSDRemoveMatte: 0
pSDShowRemoveMatteOption: 0
userData:
assetBundleName:
assetBundleVariant:
@@ -1,9 +1,9 @@
fileFormatVersion: 2
guid: 8d524cd6fa08d47c99b2462c2686b8d0
guid: c38f017a9228a44819a59ece3ccb2009
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
serializedVersion: 10
mipmaps:
mipMapMode: 0
enableMipMap: 1
@@ -20,12 +20,9 @@ TextureImporter:
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
@@ -34,12 +31,12 @@ TextureImporter:
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 0
wrapV: 0
wrapW: 0
filterMode: -1
aniso: -1
mipBias: -100
wrapU: -1
wrapV: -1
wrapW: -1
nPOTScale: 1
lightmap: 0
compressionQuality: 50
@@ -52,22 +49,16 @@ TextureImporter:
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 0
alphaIsTransparency: 1
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
- serializedVersion: 2
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
@@ -77,10 +68,8 @@ TextureImporter:
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
- serializedVersion: 2
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
@@ -90,14 +79,33 @@ TextureImporter:
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 2
buildTarget: iPhone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
- serializedVersion: 2
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID:
@@ -107,11 +115,9 @@ TextureImporter:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
spritePackingTag:
pSDRemoveMatte: 0
pSDShowRemoveMatteOption: 0
userData:
assetBundleName:
assetBundleVariant:

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