mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-29 09:15:42 +00:00
Compare commits
109
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ccc382736 | ||
|
|
cf5962f4e3 | ||
|
|
6e4ccf7189 | ||
|
|
ed4a9174de | ||
|
|
d7afc48202 | ||
|
|
54de9fe655 | ||
|
|
68254d011b | ||
|
|
37f099be96 | ||
|
|
9f47a9d01c | ||
|
|
9014874f0b | ||
|
|
93339c8fc3 | ||
|
|
b10f936dfd | ||
|
|
db5446ea37 | ||
|
|
3119b0c63c | ||
|
|
4b36870cc5 | ||
|
|
b3d0c1ce29 | ||
|
|
577c94f092 | ||
|
|
0c271aacea | ||
|
|
5a82353744 | ||
|
|
c9d6944b95 | ||
|
|
66efefc8d6 | ||
|
|
811de67c40 | ||
|
|
f4aa19119b | ||
|
|
80023d39a0 | ||
|
|
ecffeb0d2c | ||
|
|
1936d65bea | ||
|
|
ea46bb4a90 | ||
|
|
480ae0d0c6 | ||
|
|
14def8899d | ||
|
|
581ffb735e | ||
|
|
74383ae5d9 | ||
|
|
ba9c92222c | ||
|
|
f24c1209e0 | ||
|
|
e7c8df8b33 | ||
|
|
c753c4995a | ||
|
|
51e9e9b14a | ||
|
|
a2feaa6546 | ||
|
|
4d83978951 | ||
|
|
11de6872d4 | ||
|
|
ecd089de59 | ||
|
|
c1756cb36b | ||
|
|
78533a9ff0 | ||
|
|
8172a4e117 | ||
|
|
fbabe9cea4 | ||
|
|
7df24149f7 | ||
|
|
7de74539f0 | ||
|
|
e9dd53fdf8 | ||
|
|
35e8f85627 | ||
|
|
35bc242582 | ||
|
|
c5829b1aff | ||
|
|
6c78d9f733 | ||
|
|
1966dfbc24 | ||
|
|
7cd7edf9a0 | ||
|
|
104ee5429d | ||
|
|
57fd73571f | ||
|
|
9ea6e29752 | ||
|
|
8581232f70 | ||
|
|
053ac4a622 | ||
|
|
09041887c8 | ||
|
|
58fd3dc112 | ||
|
|
d62bb747dd | ||
|
|
f8646c7b50 | ||
|
|
affd0a8180 | ||
|
|
a92a878465 | ||
|
|
10696325e9 | ||
|
|
88273937da | ||
|
|
e472ab4df2 | ||
|
|
11110a5f88 | ||
|
|
d125394028 | ||
|
|
99037c7c3a | ||
|
|
c60d4b80f2 | ||
|
|
041514bd02 | ||
|
|
c410f790ff | ||
|
|
b4d19e7b65 | ||
|
|
9e8169df3e | ||
|
|
f92f400f28 | ||
|
|
28d7da9634 | ||
|
|
79aac7c9ce | ||
|
|
a530a219f1 | ||
|
|
335baf96dc | ||
|
|
584ccbadcb | ||
|
|
06f6ba8340 | ||
|
|
bfe5befff2 | ||
|
|
521dde9503 | ||
|
|
85eccb97f0 | ||
|
|
e270d27a4a | ||
|
|
d9893edcb1 | ||
|
|
5d4719ea19 | ||
|
|
e63a8489b6 | ||
|
|
9b190254a4 | ||
|
|
eb2677bd84 | ||
|
|
23d2cb3968 | ||
|
|
c66263cd56 | ||
|
|
0ba3e0e5ae | ||
|
|
860d0f3c5f | ||
|
|
13f79c37bc | ||
|
|
87cd58f7a4 | ||
|
|
e739e18d77 | ||
|
|
c03daf100c | ||
|
|
6519850ec3 | ||
|
|
df785f557b | ||
|
|
224dc3bc13 | ||
|
|
5f3095dbd3 | ||
|
|
fb12810105 | ||
|
|
fe2a5b0869 | ||
|
|
3f00ba4a03 | ||
|
|
3dd06c0aba | ||
|
|
d95f65d9b4 | ||
|
|
a7c1146c41 |
@@ -0,0 +1,177 @@
|
||||
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
|
||||
@@ -8,12 +8,22 @@ 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:
|
||||
build-sysroot-amd64:
|
||||
if: ${{ inputs.architecture == 'amd64' || inputs.architecture == 'both' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -25,7 +35,7 @@ jobs:
|
||||
- name: Upload sysroot artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ubuntu-noble-sysroot
|
||||
name: ubuntu-noble-sysroot-amd64
|
||||
path: tools/sysroot/output/
|
||||
|
||||
- name: Install AWS CLI
|
||||
@@ -41,7 +51,7 @@ jobs:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.SECRET_KEY }}
|
||||
run: |
|
||||
# Upload sysroot tarball to DO Spaces (using eagle0-windows bucket, same as other workflows)
|
||||
# Upload sysroot tarball to DO Spaces
|
||||
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 \
|
||||
@@ -54,7 +64,7 @@ jobs:
|
||||
--acl public-read
|
||||
|
||||
echo ""
|
||||
echo "=== Sysroot uploaded ==="
|
||||
echo "=== AMD64 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 ""
|
||||
@@ -64,3 +74,64 @@ 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 ")"
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
name: Deploy OAuth Relay Worker
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'cloudflare/oauth-relay/**'
|
||||
workflow_dispatch: # Allow manual trigger
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
name: Deploy to Cloudflare Workers
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Deploy Worker
|
||||
uses: cloudflare/wrangler-action@v3
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
workingDirectory: cloudflare/oauth-relay
|
||||
@@ -438,6 +438,11 @@ jobs:
|
||||
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
|
||||
@@ -458,18 +463,32 @@ 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
|
||||
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
|
||||
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}
|
||||
@@ -478,6 +497,11 @@ jobs:
|
||||
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
|
||||
|
||||
@@ -485,6 +509,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"
|
||||
|
||||
# Use crane to pull images (handles OCI format correctly) then load into Docker
|
||||
@@ -526,12 +551,19 @@ jobs:
|
||||
|
||||
echo "All images pulled successfully"
|
||||
|
||||
# Force recreate containers to ensure new image is used
|
||||
docker compose -f docker-compose.prod.yml up -d --force-recreate --remove-orphans
|
||||
# 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
|
||||
|
||||
# Restart nginx to pick up new container IPs
|
||||
# (nginx caches DNS at startup, so it needs restart after eagle/shardok)
|
||||
docker compose -f docker-compose.prod.yml restart nginx
|
||||
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
|
||||
|
||||
# Wait for health checks
|
||||
sleep 10
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
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."
|
||||
@@ -0,0 +1,159 @@
|
||||
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"
|
||||
@@ -12,6 +12,15 @@ 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
|
||||
|
||||
+110
-25
@@ -70,43 +70,126 @@ 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`)
|
||||
|
||||
### Blocked (still uses proto GameState)
|
||||
### AI Layer ✅ COMPLETE
|
||||
|
||||
- [ ] `CommandChoiceHelpers` - main target, uses proto GameState extensively
|
||||
- Depends on many Legacy* utils
|
||||
- Central hub called by many command selectors
|
||||
- [ ] `AttackDecisionCommandChooser` - uses proto GameState, converts internally
|
||||
- [ ] `CommandChooser` - trait uses proto GameState in signature
|
||||
- [ ] `FulfillQuestsCommandSelector` - takes proto, converts to native immediately
|
||||
- Called by `MidGameAIClient` which 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: CommandChoiceHelpers Migration
|
||||
### Phase 1-3: AI Layer ✅ COMPLETE
|
||||
|
||||
The main blocker is `CommandChoiceHelpers.scala` which uses proto `GameState` extensively. Strategy:
|
||||
The entire AI decision-making layer is now protoless.
|
||||
|
||||
1. **Add Scala overloads** to `CommandChoiceHelpers` methods that currently take proto GameState
|
||||
2. **Update internal helpers** to use Scala types where possible
|
||||
3. **Migrate callers incrementally** - command selectors that are already protoless can switch to Scala overloads
|
||||
### Phase 4: View Filters ✅ COMPLETE
|
||||
|
||||
### Phase 2: CommandChooser Trait
|
||||
The view_filters package migration is complete:
|
||||
|
||||
Once CommandChoiceHelpers is protoless:
|
||||
**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
|
||||
|
||||
1. Add Scala `GameState` overload to `CommandChooser.choose()` method
|
||||
2. Update implementations (`AttackDecisionCommandChooser`, etc.) to use Scala internally
|
||||
3. Eventually deprecate proto overloads
|
||||
**Still Using Proto:**
|
||||
- [x] `BattalionNameFilter` - has Scala overload
|
||||
- [x] `BattleFilter` - has Scala overload
|
||||
- [ ] `ActionResultFilter` - uses proto internally (boundary code)
|
||||
|
||||
### Phase 3: MidGameAIClient
|
||||
**Strategy:**
|
||||
1. Add Scala GameState overloads to view filter methods
|
||||
2. Update callers to pass Scala GameState where available
|
||||
3. Eventually deprecate proto versions
|
||||
|
||||
The top-level AI client still uses proto GameState. Once lower layers are protoless:
|
||||
### Phase 5: Legacy Utility Cleanup (IN PROGRESS)
|
||||
|
||||
1. Convert `MidGameAIClient` to use Scala GameState internally
|
||||
2. Only convert at the boundary when receiving from/sending to gRPC
|
||||
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
|
||||
|
||||
## Key Files
|
||||
|
||||
@@ -123,6 +206,8 @@ The top-level AI client still uses proto GameState. Once lower layers are protol
|
||||
|
||||
## Notes
|
||||
|
||||
- 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
|
||||
- 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
|
||||
|
||||
+44
-5
@@ -57,25 +57,47 @@ llvm.toolchain(
|
||||
llvm_version = "20.1.2",
|
||||
)
|
||||
|
||||
# Linux sysroot for cross-compilation (Chromium's Debian sysroot)
|
||||
# Linux x86_64 sysroot for cross-compilation
|
||||
llvm.sysroot(
|
||||
name = "llvm_toolchain_linux",
|
||||
label = "@linux_sysroot//sysroot",
|
||||
targets = ["linux-x86_64"],
|
||||
)
|
||||
|
||||
use_repo(llvm, "llvm_toolchain", "llvm_toolchain_linux")
|
||||
# Cross-compilation toolchain (macOS -> Linux ARM64)
|
||||
llvm.toolchain(
|
||||
name = "llvm_toolchain_linux_arm64",
|
||||
llvm_version = "20.1.2",
|
||||
)
|
||||
|
||||
# Download the Linux sysroot (Ubuntu 24.04 Noble for C++23 support)
|
||||
# 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)
|
||||
# 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
|
||||
#
|
||||
@@ -94,6 +116,8 @@ 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",
|
||||
)
|
||||
@@ -142,7 +166,10 @@ oci.pull(
|
||||
oci.pull(
|
||||
name = "ubuntu_24_04",
|
||||
image = "docker.io/library/ubuntu",
|
||||
platforms = ["linux/amd64"],
|
||||
platforms = [
|
||||
"linux/amd64",
|
||||
"linux/arm64/v8",
|
||||
],
|
||||
tag = "24.04",
|
||||
)
|
||||
|
||||
@@ -153,7 +180,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")
|
||||
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")
|
||||
|
||||
#
|
||||
# Java/Scala Dependencies
|
||||
@@ -219,6 +246,9 @@ 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,
|
||||
@@ -273,6 +303,14 @@ 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
|
||||
#
|
||||
@@ -286,5 +324,6 @@ register_toolchains(
|
||||
register_toolchains(
|
||||
"@llvm_toolchain//:all",
|
||||
"@llvm_toolchain_linux//:all",
|
||||
"@llvm_toolchain_linux_arm64//:all",
|
||||
dev_dependency = True,
|
||||
)
|
||||
|
||||
Generated
+18
-2
@@ -1293,7 +1293,7 @@
|
||||
"@@rules_oci~//oci:extensions.bzl%oci": {
|
||||
"general": {
|
||||
"bzlTransitiveDigest": "FaY+7xb13bB3hmxqwAWaGp3Tf3Q4Nfdlr+F38CP5mcg=",
|
||||
"usagesDigest": "uqsqMpE+yaVuTByR2rIA2v1Vkm1JwwuN6nbXOo1W9G0=",
|
||||
"usagesDigest": "39yHQmifPoGf+JSYpnQSnJGugxbrFVge/+ENmUiqZ6M=",
|
||||
"recordedFileInputs": {},
|
||||
"recordedDirentsInputs": {},
|
||||
"envVariables": {},
|
||||
@@ -1343,6 +1343,20 @@
|
||||
"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",
|
||||
@@ -1354,7 +1368,8 @@
|
||||
"repository": "library/ubuntu",
|
||||
"identifier": "24.04",
|
||||
"platforms": {
|
||||
"@@platforms//cpu:x86_64": "@ubuntu_24_04_linux_amd64"
|
||||
"@@platforms//cpu:x86_64": "@ubuntu_24_04_linux_amd64",
|
||||
"@@platforms//cpu:arm64": "@ubuntu_24_04_linux_arm64_v8"
|
||||
},
|
||||
"bzlmod_repository": "ubuntu_24_04",
|
||||
"reproducible": true
|
||||
@@ -1528,6 +1543,7 @@
|
||||
"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"
|
||||
],
|
||||
|
||||
+108
@@ -17,6 +17,18 @@ 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
|
||||
#
|
||||
@@ -155,6 +167,54 @@ 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)
|
||||
#
|
||||
@@ -237,3 +297,51 @@ oci_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",
|
||||
)
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
node_modules/
|
||||
@@ -1,46 +0,0 @@
|
||||
# OAuth Relay Worker
|
||||
|
||||
Cloudflare Worker that relays OAuth callbacks to the eagle0:// custom URL scheme.
|
||||
|
||||
## Why?
|
||||
|
||||
Discord (and some other OAuth providers) don't support custom URL schemes as redirect URIs. This worker acts as a relay:
|
||||
|
||||
1. Discord redirects to `https://eagle0-oauth-relay.<account>.workers.dev/oauth/callback?code=xxx&state=yyy`
|
||||
2. Worker responds with 302 redirect to `eagle0://auth/callback?code=xxx&state=yyy`
|
||||
3. OS opens the Eagle0 app via deep link
|
||||
|
||||
## Deploy
|
||||
|
||||
1. Install wrangler: `npm install -g wrangler`
|
||||
2. Login: `wrangler login`
|
||||
3. Deploy: `wrangler deploy`
|
||||
|
||||
The worker will be available at `https://eagle0-oauth-relay.<your-account>.workers.dev`
|
||||
|
||||
## Test Locally
|
||||
|
||||
```bash
|
||||
wrangler dev
|
||||
# Then in another terminal:
|
||||
curl -I "http://localhost:8787/oauth/callback?code=test&state=abc"
|
||||
```
|
||||
|
||||
## Test Production
|
||||
|
||||
```bash
|
||||
curl -I "https://eagle0-oauth-relay.<your-account>.workers.dev/oauth/callback?code=test&state=abc"
|
||||
```
|
||||
|
||||
Should return:
|
||||
```
|
||||
HTTP/2 302
|
||||
location: eagle0://auth/callback?code=test&state=abc
|
||||
```
|
||||
|
||||
## OAuth Provider Configuration
|
||||
|
||||
In Discord Developer Portal / Google Cloud Console, set the redirect URI to:
|
||||
```
|
||||
https://eagle0-oauth-relay.<your-account>.workers.dev/oauth/callback
|
||||
```
|
||||
@@ -1,38 +0,0 @@
|
||||
/**
|
||||
* OAuth Relay Worker
|
||||
*
|
||||
* Receives OAuth callbacks from providers (Discord, Google) and redirects
|
||||
* to the eagle0:// custom URL scheme for the native app to handle.
|
||||
*
|
||||
* Input: GET /oauth/callback?code=xxx&state=yyy
|
||||
* Output: 302 Redirect to eagle0://auth/callback?code=xxx&state=yyy
|
||||
*/
|
||||
|
||||
export default {
|
||||
async fetch(request) {
|
||||
const url = new URL(request.url);
|
||||
|
||||
// Only handle /oauth/callback path
|
||||
if (url.pathname !== '/oauth/callback') {
|
||||
return new Response('Not Found', { status: 404 });
|
||||
}
|
||||
|
||||
// Build the deep link URL
|
||||
const deepLink = new URL('eagle0://auth/callback');
|
||||
|
||||
// Forward all query parameters
|
||||
const code = url.searchParams.get('code');
|
||||
const state = url.searchParams.get('state');
|
||||
const error = url.searchParams.get('error');
|
||||
const errorDescription = url.searchParams.get('error_description');
|
||||
|
||||
if (code) deepLink.searchParams.set('code', code);
|
||||
if (state) deepLink.searchParams.set('state', state);
|
||||
if (error) deepLink.searchParams.set('error', error);
|
||||
if (errorDescription) deepLink.searchParams.set('error_description', errorDescription);
|
||||
|
||||
console.log(`OAuth relay: redirecting to ${deepLink.toString()}`);
|
||||
|
||||
return Response.redirect(deepLink.toString(), 302);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
name = "eagle0-oauth-relay"
|
||||
main = "worker.js"
|
||||
compatibility_date = "2024-01-01"
|
||||
workers_dev = true
|
||||
+72
-5
@@ -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
|
||||
# Build images: bazel run //ci:eagle_server_load && bazel run //ci:shardok_server_load && bazel run //ci:auth_server_load
|
||||
# Run: docker compose -f docker-compose.prod.yml up
|
||||
#
|
||||
# Production deployment:
|
||||
@@ -15,7 +15,9 @@ services:
|
||||
- "--gpt-model-name"
|
||||
- "${GPT_MODEL_NAME:-gpt-5.1}"
|
||||
- "--shardok-interface-remote-address"
|
||||
- "shardok:40042"
|
||||
- "${SHARDOK_ADDRESS:-shardok:40042}"
|
||||
- "--auth-service-url"
|
||||
- "auth:40033"
|
||||
ports:
|
||||
- "40032:40032"
|
||||
environment:
|
||||
@@ -27,12 +29,22 @@ services:
|
||||
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
|
||||
- ./saves:/app/saves # Game saves and user database
|
||||
- ./archived:/app/archived # Archived completed games
|
||||
- ./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"
|
||||
@@ -46,6 +58,46 @@ 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
|
||||
@@ -77,6 +129,7 @@ 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
|
||||
@@ -84,6 +137,7 @@ services:
|
||||
- ./auth:/etc/nginx/auth:ro
|
||||
depends_on:
|
||||
- eagle
|
||||
- admin
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
driver: "json-file"
|
||||
@@ -97,14 +151,16 @@ services:
|
||||
command:
|
||||
- "--eagle-addr"
|
||||
- "eagle:40032"
|
||||
- "--auth-addr"
|
||||
- "auth:40033"
|
||||
- "--jfr-sidecar-addr"
|
||||
- "jfr-sidecar:8081"
|
||||
- "--http-port"
|
||||
- "8080"
|
||||
ports:
|
||||
- "8080:8080"
|
||||
# No external port - accessed via nginx at admin.eagle0.net
|
||||
depends_on:
|
||||
- eagle
|
||||
- auth
|
||||
- jfr-sidecar
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
@@ -152,3 +208,14 @@ services:
|
||||
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
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
# 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.
|
||||
+22
-31
@@ -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` | Heavy proto usage | Blocked by proto dependencies |
|
||||
| `ResolveBattleAction.scala` | ✅ Fully protoless | Complete (PR #5048) |
|
||||
| `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: 47 of 52 action files (90%) are fully protoless.**
|
||||
**Progress: 52 of 52 action files (100%) are fully protoless.** ✅
|
||||
|
||||
The following 5 actions still have proto usage:
|
||||
All action files have been migrated to use Scala types:
|
||||
|
||||
| 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 |
|
||||
| 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 |
|
||||
|
||||
**Note:** `NewRoundAction` is now fully protoless after converting `ChronicleEventGenerator` to return Scala `ChronicleEvent` types directly.
|
||||
|
||||
@@ -223,13 +223,13 @@ The following 5 actions still have proto usage:
|
||||
|
||||
| Component | Lines | Complexity | Blocks |
|
||||
|-----------|-------|------------|--------|
|
||||
| `CommandChoiceHelpers` to Scala | ~2000 | High | 4 vassal actions |
|
||||
| `ResolveBattleAction` refactor | ~500 | High | 1 action (complex) |
|
||||
| History API updates | ~100 | Low | - |
|
||||
| **Total Remaining** | **~2600** | | |
|
||||
| **Total Remaining** | **~100** | | |
|
||||
|
||||
**Completed:**
|
||||
- `ChronicleEventGenerator` converted to return Scala `ChronicleEvent` types directly
|
||||
- ✅ `ChronicleEventGenerator` converted to return Scala `ChronicleEvent` types directly
|
||||
- ✅ `CommandChoiceHelpers` migrated to Scala types
|
||||
- ✅ `ResolveBattleAction` refactored to use Scala GameState and ActionResultApplier (PR #5048)
|
||||
|
||||
### CommandChoiceHelpers Migration Status
|
||||
|
||||
@@ -246,28 +246,18 @@ 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` | ❌ 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 |
|
||||
| `CommandChoiceHelpers.scala` | ✅ **Protoless** | Uses Scala `GameState` throughout |
|
||||
| `ProvinceGoldSurplusCalculator.scala` | ✅ **Protoless** | Uses 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
|
||||
**All CommandChoiceHelpers selectors have been migrated to Scala types.** ✅
|
||||
|
||||
### Progress Summary
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Action files fully protoless | 47 / 52 (90%) |
|
||||
| Proto usages in remaining actions | 32 total |
|
||||
| Biggest blocker | `ResolveBattleAction` (24 usages) |
|
||||
| Second biggest blocker | `CommandChoiceHelpers` (blocks 4 actions) |
|
||||
| Action files fully protoless | 52 / 52 (100%) ✅ |
|
||||
| Proto usages in remaining actions | 0 |
|
||||
| Next target | History APIs (InMemoryHistory, PersistedHistory) |
|
||||
|
||||
### Validation
|
||||
- [x] `ActionResultApplier` created and tested
|
||||
@@ -276,8 +266,9 @@ Several command selectors have already been converted to use Scala types:
|
||||
- [x] `ProvinceViewFilter` has Scala overload for server-side use (PR #4752)
|
||||
- [x] `FactionT.reconnedProvinces` and `ChangedFactionC.updatedReconnedProvinces` use Scala `ProvinceView`
|
||||
- [x] `ProvinceViewFilter.withdrawnFromProvinceView` has Scala overload
|
||||
- [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
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
# 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`
|
||||
@@ -0,0 +1,383 @@
|
||||
# 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?
|
||||
@@ -0,0 +1,350 @@
|
||||
# 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
|
||||
@@ -0,0 +1,216 @@
|
||||
# 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
|
||||
@@ -9,6 +9,8 @@ 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
|
||||
)
|
||||
|
||||
@@ -34,9 +34,13 @@ 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=
|
||||
|
||||
+33
-2
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"__AUTOGENERATED_FILE_DO_NOT_MODIFY_THIS_FILE_MANUALLY": "THERE_IS_NO_DATA_ONLY_ZUUL",
|
||||
"__INPUT_ARTIFACTS_HASH": -1064460283,
|
||||
"__RESOLVED_ARTIFACTS_HASH": -1574144850,
|
||||
"__INPUT_ARTIFACTS_HASH": -2049857450,
|
||||
"__RESOLVED_ARTIFACTS_HASH": -1728186926,
|
||||
"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,6 +413,12 @@
|
||||
},
|
||||
"version": "0.27.0"
|
||||
},
|
||||
"io.sentry:sentry": {
|
||||
"shasums": {
|
||||
"jar": "740a118182fc089d307830f4e508372e01ad94639b00b4e1b1d83762298a5f35"
|
||||
},
|
||||
"version": "7.19.0"
|
||||
},
|
||||
"javax.activation:javax.activation-api": {
|
||||
"shasums": {
|
||||
"jar": "43fdef0b5b6ceb31b0424b208b930c74ab58fac2ceeb7b3f6fd3aeb8b5ca4393"
|
||||
@@ -1785,6 +1791,30 @@
|
||||
"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"
|
||||
],
|
||||
@@ -2452,6 +2482,7 @@
|
||||
"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",
|
||||
|
||||
+127
-11
@@ -33,6 +33,7 @@ http {
|
||||
# HTTP server for Let's Encrypt challenge and redirect
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name prod.eagle0.net;
|
||||
|
||||
# Let's Encrypt challenge
|
||||
@@ -49,6 +50,7 @@ http {
|
||||
# HTTPS server for gRPC
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
http2 on;
|
||||
server_name prod.eagle0.net;
|
||||
|
||||
@@ -97,22 +99,13 @@ http {
|
||||
error_page 502 = /error502grpc;
|
||||
}
|
||||
|
||||
# OAuth callback endpoint (proxied to Eagle's HTTP handler)
|
||||
# OAuth callback endpoint (proxied to Go auth service)
|
||||
location /oauth/callback {
|
||||
proxy_pass http://eagle:8080;
|
||||
proxy_pass http://auth:8080;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
# Admin server proxy
|
||||
location /admin/ {
|
||||
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;
|
||||
}
|
||||
|
||||
# Health check endpoint
|
||||
location /health {
|
||||
access_log off;
|
||||
@@ -129,4 +122,127 @@ 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;
|
||||
}
|
||||
|
||||
# Temporarily serve directly until SSL certs are obtained
|
||||
# After running certbot, uncomment the HTTPS block below and change this to:
|
||||
# return 301 https://$host$request_uri;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
# HTTPS server for Admin Console
|
||||
# Uncomment after obtaining SSL certs with:
|
||||
# certbot certonly --webroot -w /var/www/certbot -d admin.eagle0.net -d admin.prod.eagle0.net
|
||||
#
|
||||
# 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;
|
||||
# }
|
||||
# }
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ 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++",
|
||||
],
|
||||
|
||||
@@ -34,6 +34,17 @@ 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"],
|
||||
@@ -42,6 +53,7 @@ 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,8 +81,17 @@ static auto FromInternalStatus(
|
||||
|
||||
const std::string &kShardokGameRequestExtension = *(new string(".e0gr"));
|
||||
|
||||
EagleInterfaceImpl::EagleInterfaceImpl(shared_ptr<ShardokGamesManager> manager)
|
||||
: gamesManager(std::move(manager)) {}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
auto ConvertPlayerInfo(
|
||||
const google::protobuf::RepeatedPtrField<net::eagle0::common::PlayerSetupInfo> &allPis,
|
||||
@@ -188,9 +197,12 @@ 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());
|
||||
@@ -229,9 +241,12 @@ 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());
|
||||
@@ -323,9 +338,12 @@ 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());
|
||||
|
||||
@@ -333,9 +351,12 @@ 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;
|
||||
@@ -516,6 +537,9 @@ 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,8 +13,9 @@
|
||||
#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>
|
||||
@@ -46,6 +47,7 @@ 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>;
|
||||
@@ -57,7 +59,14 @@ private:
|
||||
GameStatusResponse* response);
|
||||
|
||||
public:
|
||||
explicit EagleInterfaceImpl(std::shared_ptr<ShardokGamesManager> manager);
|
||||
/**
|
||||
* 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 = "");
|
||||
|
||||
auto PostCommand(
|
||||
ServerContext* context,
|
||||
|
||||
@@ -24,6 +24,7 @@ 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,6 +29,7 @@ public:
|
||||
const static string kEagleGrpcAddress;
|
||||
const static string kSslCertPath;
|
||||
const static string kSslPrivateKeyPath;
|
||||
const static string kAuthTokenPath;
|
||||
};
|
||||
|
||||
#endif /* ServerConfiguration_hpp */
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
//
|
||||
// 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,7 +7,9 @@
|
||||
//
|
||||
|
||||
#include <cstddef>
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
#include <thread>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/common/ProtobufWarningSuppression.hpp"
|
||||
@@ -17,14 +19,16 @@ 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 "server/EagleInterfaceGrpcServer.hpp"
|
||||
#include "server/ServerConfiguration.hpp"
|
||||
#include "server/ShardokGamesManager.hpp"
|
||||
#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"
|
||||
|
||||
using ::GameStatePersister;
|
||||
using shardok::EagleInterfaceImpl;
|
||||
@@ -41,12 +45,15 @@ struct ServerThreadInfo {
|
||||
|
||||
auto StartInThread(grpc::ServerBuilder *serverBuilder) -> ServerThreadInfo;
|
||||
|
||||
auto CreateServerBuilder(const string &serverAddress) -> grpc::ServerBuilder *;
|
||||
auto CreateServerBuilder(const string &serverAddress, const string &certPath, const string &keyPath)
|
||||
-> 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;
|
||||
@@ -82,11 +89,46 @@ auto main(const int argc, char **argv) -> int {
|
||||
eagleInterfaceInfo.thread.join();
|
||||
}
|
||||
|
||||
auto CreateServerBuilder(const string &serverAddress) -> grpc::ServerBuilder * {
|
||||
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 *serverBuilder = new grpc::ServerBuilder;
|
||||
|
||||
const auto credentials = grpc::InsecureServerCredentials();
|
||||
std::cout << "Creating insecure connection" << std::endl;
|
||||
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;
|
||||
}
|
||||
|
||||
serverBuilder->AddListeningPort(serverAddress, credentials);
|
||||
return serverBuilder;
|
||||
@@ -98,9 +140,19 @@ auto CreateEagleInterfaceService(
|
||||
const string eagleInterfaceServerAddress =
|
||||
config->stringForKey(ServerConfiguration::kEagleInterfaceGrpcAddress);
|
||||
|
||||
auto *eagleInterfaceServerBuilder = CreateServerBuilder(eagleInterfaceServerAddress);
|
||||
// TLS configuration
|
||||
const string certPath = config->stringForKey(ServerConfiguration::kSslCertPath);
|
||||
const string keyPath = config->stringForKey(ServerConfiguration::kSslPrivateKeyPath);
|
||||
|
||||
const auto eagleInterfaceService = std::make_shared<EagleInterfaceImpl>(shardokGamesManager);
|
||||
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);
|
||||
eagleInterfaceServerBuilder->RegisterService(eagleInterfaceService.get());
|
||||
|
||||
ServerThreadInfo eagleInterfaceThreadInfo = StartInThread(eagleInterfaceServerBuilder);
|
||||
|
||||
@@ -110,6 +110,7 @@
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/UI Manager/UIManagerProgressBarLoopEditor.cs" />
|
||||
<Compile Include="Assets/Eagle/BattalionUtils.cs" />
|
||||
<Compile Include="Assets/Eagle/Table Rows/UnaffiliatedHeroRowController.cs" />
|
||||
<Compile Include="Assets/Shardok/ArrowVolleyAnimator.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" />
|
||||
@@ -180,6 +181,7 @@
|
||||
<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" />
|
||||
|
||||
@@ -11,43 +11,58 @@ using GrpcAuthClient = Net.Eagle0.Eagle.Api.Auth.Auth.AuthClient;
|
||||
namespace Auth {
|
||||
/// <summary>
|
||||
/// gRPC client for the Auth service.
|
||||
/// Handles OAuth URL generation, status polling, and token refresh.
|
||||
/// 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 _client;
|
||||
private readonly GrpcChannel _channel;
|
||||
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 for the given server URL.
|
||||
/// Create auth client with separate channels for auth service and Eagle.
|
||||
/// </summary>
|
||||
/// <param name="serverUrl">Full URL with scheme, e.g. "http://localhost:40032" or
|
||||
/// "https://prod.eagle0.net"</param>
|
||||
public AuthClient(string serverUrl) {
|
||||
_channel = GrpcChannel.ForAddress(
|
||||
serverUrl,
|
||||
/// <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);
|
||||
|
||||
// Create invoker with JWT interceptor for authenticated requests
|
||||
CallInvoker invoker = _channel.Intercept(new JwtAuthInterceptor());
|
||||
_client = new GrpcAuthClient(invoker);
|
||||
// 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 _client.GetOAuthUrlAsync(request);
|
||||
var response = await _authServiceClient.GetOAuthUrlAsync(request);
|
||||
|
||||
Debug.Log($"[AuthClient] Got OAuth URL for {provider}, state={response.State}");
|
||||
return (response.AuthUrl, response.State);
|
||||
@@ -56,6 +71,7 @@ namespace Auth {
|
||||
/// <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>
|
||||
@@ -64,7 +80,7 @@ namespace Auth {
|
||||
|
||||
while (true) {
|
||||
var request = new CheckOAuthStatusRequest { State = state };
|
||||
var response = await _client.CheckOAuthStatusAsync(request);
|
||||
var response = await _authServiceClient.CheckOAuthStatusAsync(request);
|
||||
|
||||
switch (response.Status) {
|
||||
case OAuthStatus.Success:
|
||||
@@ -94,6 +110,7 @@ namespace Auth {
|
||||
|
||||
/// <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() {
|
||||
@@ -104,7 +121,7 @@ namespace Auth {
|
||||
|
||||
var request = new RefreshTokenRequest { RefreshToken = refreshToken };
|
||||
|
||||
var response = await _client.RefreshTokenAsync(request);
|
||||
var response = await _authServiceClient.RefreshTokenAsync(request);
|
||||
|
||||
// Update stored access token
|
||||
TokenStorage.UpdateAccessToken(response.AccessToken, response.ExpiresAt);
|
||||
@@ -116,14 +133,23 @@ namespace Auth {
|
||||
/// <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 _client.SetDisplayNameAsync(request);
|
||||
var response = await _eagleClient.SetDisplayNameAsync(request);
|
||||
|
||||
if (response.Success) {
|
||||
TokenStorage.UpdateDisplayName(displayName);
|
||||
Debug.Log($"[AuthClient] Display name set to: {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}");
|
||||
@@ -134,19 +160,21 @@ namespace Auth {
|
||||
|
||||
/// <summary>
|
||||
/// Get current user info. Validates the stored JWT.
|
||||
/// Routed to Eagle.
|
||||
/// </summary>
|
||||
public async Task<GetCurrentUserResponse> GetCurrentUserAsync() {
|
||||
var response = await _client.GetCurrentUserAsync(new GetCurrentUserRequest());
|
||||
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 _client.LogoutAsync(new LogoutRequest());
|
||||
await _eagleClient.LogoutAsync(new LogoutRequest());
|
||||
} catch (Exception ex) {
|
||||
Debug.LogWarning($"[AuthClient] Logout RPC failed (may be expected): {ex.Message}");
|
||||
}
|
||||
@@ -156,6 +184,9 @@ namespace Auth {
|
||||
Debug.Log("[AuthClient] Logged out, tokens cleared");
|
||||
}
|
||||
|
||||
public void Dispose() { _channel?.Dispose(); }
|
||||
public void Dispose() {
|
||||
_authServiceChannel?.Dispose();
|
||||
_eagleChannel?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@ namespace Auth {
|
||||
public static OAuthManager Instance { get; private set; }
|
||||
|
||||
private AuthClient _authClient;
|
||||
private string _currentServerUrl;
|
||||
private string _currentAuthServiceUrl;
|
||||
private string _currentEagleUrl;
|
||||
|
||||
// Events for UI updates
|
||||
public event Action<UserInfo> OnLoginSuccess;
|
||||
@@ -34,22 +35,31 @@ namespace Auth {
|
||||
|
||||
Instance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
// AuthClient is created lazily when SetServerUrl is called
|
||||
|
||||
// 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 URL for OAuth requests. Call this before any OAuth operations.
|
||||
/// Set the server URLs for OAuth and game requests. Call this before any OAuth operations.
|
||||
/// </summary>
|
||||
/// <param name="serverUrl">Full URL with scheme, e.g. "https://prod.eagle0.net"</param>
|
||||
public void SetServerUrl(string serverUrl) {
|
||||
if (_currentServerUrl == serverUrl && _authClient != null) {
|
||||
return; // Already configured for this URL
|
||||
/// <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();
|
||||
_currentServerUrl = serverUrl;
|
||||
_authClient = new AuthClient(serverUrl);
|
||||
Debug.Log($"[OAuthManager] Configured for server: {serverUrl}");
|
||||
_currentAuthServiceUrl = authServiceUrl;
|
||||
_currentEagleUrl = eagleUrl;
|
||||
_authClient = new AuthClient(authServiceUrl, eagleUrl);
|
||||
Debug.Log($"[OAuthManager] Configured: authService={authServiceUrl}, eagle={eagleUrl}");
|
||||
}
|
||||
|
||||
private void OnDestroy() {
|
||||
@@ -60,7 +70,7 @@ namespace Auth {
|
||||
private void EnsureConfigured() {
|
||||
if (_authClient == null) {
|
||||
throw new InvalidOperationException(
|
||||
"OAuthManager not configured. Call SetServerUrl() first.");
|
||||
"OAuthManager not configured. Call SetServerUrls() first.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +129,7 @@ namespace Auth {
|
||||
/// <summary>
|
||||
/// Try to restore session from stored tokens.
|
||||
/// Returns true if valid session exists.
|
||||
/// Must call SetServerUrl() before this method.
|
||||
/// Must call SetServerUrls() before this method.
|
||||
/// </summary>
|
||||
public async Task<bool> TryRestoreSessionAsync() {
|
||||
if (_authClient == null) {
|
||||
|
||||
@@ -4,6 +4,7 @@ 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>
|
||||
@@ -14,6 +15,32 @@ namespace Auth {
|
||||
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;
|
||||
@@ -26,33 +53,54 @@ namespace Auth {
|
||||
public static bool HasRefreshToken => !string.IsNullOrEmpty(RefreshToken);
|
||||
|
||||
public static string AccessToken {
|
||||
get => PlayerPrefs.GetString(AccessTokenKey, null);
|
||||
get {
|
||||
if (!_cacheInitialized) {
|
||||
Debug.LogWarning(
|
||||
"[TokenStorage] Cache not initialized, returning cached value anyway");
|
||||
}
|
||||
return _cachedAccessToken;
|
||||
}
|
||||
private
|
||||
set => PlayerPrefs.SetString(AccessTokenKey, value ?? "");
|
||||
set {
|
||||
_cachedAccessToken = value;
|
||||
PlayerPrefs.SetString(AccessTokenKey, value ?? "");
|
||||
}
|
||||
}
|
||||
|
||||
public static string RefreshToken {
|
||||
get => PlayerPrefs.GetString(RefreshTokenKey, null);
|
||||
get => _cachedRefreshToken;
|
||||
private
|
||||
set => PlayerPrefs.SetString(RefreshTokenKey, value ?? "");
|
||||
set {
|
||||
_cachedRefreshToken = value;
|
||||
PlayerPrefs.SetString(RefreshTokenKey, value ?? "");
|
||||
}
|
||||
}
|
||||
|
||||
public static long ExpiresAt {
|
||||
get => long.TryParse(PlayerPrefs.GetString(ExpiresAtKey, "0"), out var val) ? val : 0;
|
||||
get => _cachedExpiresAt;
|
||||
private
|
||||
set => PlayerPrefs.SetString(ExpiresAtKey, value.ToString());
|
||||
set {
|
||||
_cachedExpiresAt = value;
|
||||
PlayerPrefs.SetString(ExpiresAtKey, value.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
public static string UserId {
|
||||
get => PlayerPrefs.GetString(UserIdKey, null);
|
||||
get => _cachedUserId;
|
||||
private
|
||||
set => PlayerPrefs.SetString(UserIdKey, value ?? "");
|
||||
set {
|
||||
_cachedUserId = value;
|
||||
PlayerPrefs.SetString(UserIdKey, value ?? "");
|
||||
}
|
||||
}
|
||||
|
||||
public static string DisplayName {
|
||||
get => PlayerPrefs.GetString(DisplayNameKey, null);
|
||||
get => _cachedDisplayName;
|
||||
private
|
||||
set => PlayerPrefs.SetString(DisplayNameKey, value ?? "");
|
||||
set {
|
||||
_cachedDisplayName = value;
|
||||
PlayerPrefs.SetString(DisplayNameKey, value ?? "");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -98,6 +146,12 @@ namespace Auth {
|
||||
/// 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);
|
||||
|
||||
+86
-8
@@ -110,6 +110,13 @@ 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;
|
||||
|
||||
@@ -165,10 +172,16 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get full URL with scheme for the selected environment.
|
||||
/// Get full URL with scheme for the selected environment (Eagle server).
|
||||
/// Remote servers use HTTPS, could be extended for local HTTP.
|
||||
/// </summary>
|
||||
private string GetFullUrlFromEnvironment() { return "https://" + GetUrlFromEnvironment(); }
|
||||
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);
|
||||
@@ -208,6 +221,9 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
// Initialize OAuth UI
|
||||
SetupOAuthUI();
|
||||
|
||||
// Initialize Lobby UI (logout button, etc.)
|
||||
SetupLobbyUI();
|
||||
|
||||
// Initialize status text
|
||||
UpdateConnectionStatus();
|
||||
|
||||
@@ -260,11 +276,62 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
}
|
||||
}
|
||||
|
||||
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 current environment URL
|
||||
OAuthManager.Instance.SetServerUrl(GetFullUrlFromEnvironment());
|
||||
// Configure OAuthManager with auth service and Eagle URLs
|
||||
OAuthManager.Instance.SetServerUrls(GetAuthServiceUrl(), GetEagleUrl());
|
||||
|
||||
if (oauthStatusText != null) { oauthStatusText.text = "Checking for existing session..."; }
|
||||
|
||||
@@ -278,8 +345,8 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
}
|
||||
|
||||
private async void OnOAuthLoginClicked(OAuthProvider provider) {
|
||||
// Configure OAuthManager with current environment URL (user may have changed dropdown)
|
||||
OAuthManager.Instance.SetServerUrl(GetFullUrlFromEnvironment());
|
||||
// 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...";
|
||||
@@ -440,6 +507,14 @@ 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() {
|
||||
@@ -486,6 +561,9 @@ 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) {
|
||||
@@ -602,12 +680,12 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
}
|
||||
|
||||
private void _internalCustomBattle() {
|
||||
_createConnection();
|
||||
// Connection already exists when called from lobby
|
||||
_persistentClientConnection.SetLobbySubscriber(this);
|
||||
|
||||
SetCustomBattleActive(true);
|
||||
|
||||
connectionPanel.SetActive(false);
|
||||
gameSelectionPanel.SetActive(false);
|
||||
customBattlePanel.SetActive(true);
|
||||
}
|
||||
|
||||
|
||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 11 KiB |
-88
@@ -1,88 +0,0 @@
|
||||
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:
|
||||
+45
-16
@@ -981,54 +981,69 @@ namespace eagle {
|
||||
HandleGameUpdate(current.GameUpdate, receivedTime);
|
||||
break;
|
||||
|
||||
case UpdateStreamResponse.ResponseDetailsOneofCase.LobbyResponse:
|
||||
if (_lobbySubscriber != null) {
|
||||
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) {
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
_lobbySubscriber.ReceiveLobbyUpdate(current.LobbyResponse);
|
||||
lobbySubscriber.ReceiveLobbyUpdate(current.LobbyResponse);
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case UpdateStreamResponse.ResponseDetailsOneofCase
|
||||
.PregeneratedTextResponse:
|
||||
if (_lobbySubscriber != null) {
|
||||
.PregeneratedTextResponse: {
|
||||
var lobbySubscriber = _lobbySubscriber;
|
||||
if (lobbySubscriber != null) {
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
_lobbySubscriber.ReceivePregeneratedTextUpdate(
|
||||
lobbySubscriber.ReceivePregeneratedTextUpdate(
|
||||
current.PregeneratedTextResponse);
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case UpdateStreamResponse.ResponseDetailsOneofCase.JoinGameResponse:
|
||||
if (_lobbySubscriber != null) {
|
||||
case UpdateStreamResponse.ResponseDetailsOneofCase.JoinGameResponse: {
|
||||
var lobbySubscriber = _lobbySubscriber;
|
||||
if (lobbySubscriber != null) {
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
_lobbySubscriber.HandleJoinGameResponse(
|
||||
lobbySubscriber.HandleJoinGameResponse(
|
||||
current.JoinGameResponse);
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case UpdateStreamResponse.ResponseDetailsOneofCase.CreateGameResponse:
|
||||
if (_lobbySubscriber != null) {
|
||||
case UpdateStreamResponse.ResponseDetailsOneofCase.CreateGameResponse: {
|
||||
var lobbySubscriber = _lobbySubscriber;
|
||||
if (lobbySubscriber != null) {
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
_lobbySubscriber.HandleCreateGameResponse(
|
||||
lobbySubscriber.HandleCreateGameResponse(
|
||||
current.CreateGameResponse);
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
case UpdateStreamResponse.ResponseDetailsOneofCase.CustomBattleResponse:
|
||||
if (_lobbySubscriber != null) {
|
||||
}
|
||||
|
||||
case UpdateStreamResponse.ResponseDetailsOneofCase
|
||||
.CustomBattleResponse: {
|
||||
var lobbySubscriber = _lobbySubscriber;
|
||||
if (lobbySubscriber != null) {
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
_lobbySubscriber.HandleCustomBattleResponse(
|
||||
lobbySubscriber.HandleCustomBattleResponse(
|
||||
current.CustomBattleResponse);
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case UpdateStreamResponse.ResponseDetailsOneofCase.HexMapResponse:
|
||||
var hexMapResponse = current.HexMapResponse;
|
||||
@@ -1072,7 +1087,21 @@ namespace eagle {
|
||||
var tokenCancelled = _currentThreadToken.IsCancellationRequested;
|
||||
LogFlow($"HandleStreamingCall ENDED normally: sc_null={scNull} token_cancelled={tokenCancelled}");
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
"How did we get here? This is not my beautiful wife!");
|
||||
$"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");
|
||||
}
|
||||
} catch (RpcException e) {
|
||||
lock (this) {
|
||||
_lastDisconnect = DateTime.UtcNow;
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 10 KiB |
@@ -1,123 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aa2fcd8d8f12d4218aa9ee793adfe434
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 12
|
||||
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
|
||||
vTOnly: 0
|
||||
ignoreMasterTextureLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 0
|
||||
wrapV: 0
|
||||
wrapW: 0
|
||||
nPOTScale: 1
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 0
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 0
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 1
|
||||
cookieLightType: 1
|
||||
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
|
||||
- serializedVersion: 3
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 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: []
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
nameFileIdTable: {}
|
||||
spritePackingTag:
|
||||
pSDRemoveMatte: 0
|
||||
pSDShowRemoveMatteOption: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -1,123 +0,0 @@
|
||||
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:
|
||||
BIN
Binary file not shown.
-90
@@ -1,90 +0,0 @@
|
||||
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:
|
||||
Binary file not shown.
@@ -1,95 +0,0 @@
|
||||
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:
|
||||
Binary file not shown.
@@ -1,88 +0,0 @@
|
||||
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:
|
||||
Binary file not shown.
@@ -1,91 +0,0 @@
|
||||
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:
|
||||
Binary file not shown.
@@ -1,92 +0,0 @@
|
||||
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:
|
||||
Binary file not shown.
@@ -1,91 +0,0 @@
|
||||
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:
|
||||
BIN
Binary file not shown.
-91
@@ -1,91 +0,0 @@
|
||||
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:
|
||||
Binary file not shown.
@@ -1,92 +0,0 @@
|
||||
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:
|
||||
Binary file not shown.
@@ -1,77 +0,0 @@
|
||||
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:
|
||||
Binary file not shown.
@@ -1,117 +0,0 @@
|
||||
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:
|
||||
Binary file not shown.
@@ -1,91 +0,0 @@
|
||||
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:
|
||||
Binary file not shown.
@@ -1,91 +0,0 @@
|
||||
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:
|
||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 87 KiB |
-123
@@ -1,123 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c38f017a9228a44819a59ece3ccb2009
|
||||
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: 1
|
||||
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
|
||||
- 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:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spritePackingTag:
|
||||
pSDRemoveMatte: 0
|
||||
pSDShowRemoveMatteOption: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 34 KiB |
-123
@@ -1,123 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a98ea09ca5b31411688f7dd794365af8
|
||||
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: 2
|
||||
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
|
||||
- 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:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spritePackingTag:
|
||||
pSDRemoveMatte: 0
|
||||
pSDShowRemoveMatteOption: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -1,117 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3b060e6a4d5fa416c8441e956ea66151
|
||||
timeCreated: 1523744563
|
||||
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:
|
||||
Binary file not shown.
@@ -1,127 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c3d3ab677e7a149d29c24ca2f6f32663
|
||||
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: 1
|
||||
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
|
||||
- serializedVersion: 3
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: iPhone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: Android
|
||||
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:
|
||||
Binary file not shown.
@@ -1,92 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9af5847f9737a450a8cf1ca346d08e51
|
||||
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:
|
||||
Binary file not shown.
@@ -1,117 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 13416e9d94e484f6c90efa07acf5c6f8
|
||||
timeCreated: 1522355331
|
||||
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:
|
||||
Binary file not shown.
@@ -1,91 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dc10947b75b89484c95585e7b7fb6835
|
||||
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:
|
||||
Binary file not shown.
@@ -1,90 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0dba71510faab4a00b60dda67dc5551a
|
||||
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:
|
||||
@@ -13,9 +13,6 @@ public class MainQueue : MonoBehaviour {
|
||||
// 8ms leaves room for rendering within a 16ms (60fps) frame budget
|
||||
private const long MaxMillisecondsPerFrame = 8;
|
||||
|
||||
// Track queue depth for logging
|
||||
private int _lastLoggedQueueDepth = 0;
|
||||
|
||||
private MainQueue() {}
|
||||
|
||||
void Awake() {
|
||||
@@ -38,14 +35,6 @@ public class MainQueue : MonoBehaviour {
|
||||
return;
|
||||
}
|
||||
|
||||
// Log when queue has built up (e.g., after resuming from background)
|
||||
if (queueDepthBefore > 100 && queueDepthBefore != _lastLoggedQueueDepth) {
|
||||
Debug.Log($"[MainQueue] Processing backlog: {queueDepthBefore} actions queued");
|
||||
_lastLoggedQueueDepth = queueDepthBefore;
|
||||
} else if (queueDepthBefore <= 100) {
|
||||
_lastLoggedQueueDepth = 0;
|
||||
}
|
||||
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
Action possibleAction;
|
||||
do {
|
||||
|
||||
@@ -47428,7 +47428,7 @@ MonoBehaviour:
|
||||
type: 3}
|
||||
fireEffectPrefab: {fileID: 198663536934198432, guid: bb98861c48ad2024ca93498c7dd93a6c,
|
||||
type: 3}
|
||||
bridgeImage: {fileID: 2800000, guid: 354f50a479c2a4ca3bf062e12f528f1c, type: 3}
|
||||
bridgeImage: {fileID: 2800000, guid: 2fde9377fcc9e4f62a3635c7b5894a09, type: 3}
|
||||
tileInfoText: {fileID: 258076458}
|
||||
workingFilePath:
|
||||
workingFileName:
|
||||
@@ -51019,7 +51019,7 @@ MonoBehaviour:
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Texture: {fileID: 2800000, guid: 354f50a479c2a4ca3bf062e12f528f1c, type: 3}
|
||||
m_Texture: {fileID: 2800000, guid: 2fde9377fcc9e4f62a3635c7b5894a09, type: 3}
|
||||
m_UVRect:
|
||||
serializedVersion: 2
|
||||
x: 0
|
||||
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Shardok {
|
||||
/// <summary>
|
||||
/// Animates a volley of arrows flying from a source hex to a target hex.
|
||||
/// Uses SpriteRenderer for world-space rendering (like particle effects)
|
||||
/// rather than UI Image, which has issues with the rotated Grid Canvas.
|
||||
/// </summary>
|
||||
public class ArrowVolleyAnimator : MonoBehaviour {
|
||||
[Header("Arrow Settings")]
|
||||
[Tooltip("Sprite to use for individual arrows")]
|
||||
public Sprite arrowSprite;
|
||||
|
||||
[Tooltip("Number of arrows in the volley")]
|
||||
public int arrowCount = 12;
|
||||
|
||||
[Tooltip("Scale of each arrow sprite")]
|
||||
public float arrowScale = 5.0f;
|
||||
|
||||
[Tooltip("Rotation offset to adjust for sprite orientation (degrees)")]
|
||||
public float spriteRotationOffset = 45f;
|
||||
|
||||
[Header("Animation Settings")]
|
||||
[Tooltip("Duration of the arrow flight in seconds")]
|
||||
public float flightDuration = 0.4f;
|
||||
|
||||
[Tooltip("Arc height as a fraction of the distance")]
|
||||
public float arcHeight = 0.15f;
|
||||
|
||||
[Tooltip("Spread radius at the source (in local units)")]
|
||||
public float sourceSpread = 15f;
|
||||
|
||||
[Tooltip("Spread radius at the target (in local units)")]
|
||||
public float targetSpread = 25f;
|
||||
|
||||
[Tooltip("Time spread for staggered launch")]
|
||||
public float launchSpread = 0.1f;
|
||||
|
||||
[Header("References")]
|
||||
[Tooltip("HexGrid to get canvas reference from (auto-found if not set)")]
|
||||
public HexGrid hexGrid;
|
||||
|
||||
private readonly List<Coroutine> _activeCoroutines = new List<Coroutine>();
|
||||
|
||||
private void Start() {
|
||||
if (hexGrid == null) { hexGrid = FindObjectOfType<HexGrid>(); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts an arrow volley animation between two hex cells.
|
||||
/// </summary>
|
||||
/// <param name="sourceCellIndex">Grid index of the attacking hex</param>
|
||||
/// <param name="targetCellIndex">Grid index of the target hex</param>
|
||||
public void AnimateVolley(int sourceCellIndex, int targetCellIndex) {
|
||||
if (arrowSprite == null) {
|
||||
Debug.LogWarning("ArrowVolleyAnimator: No arrow sprite assigned");
|
||||
return;
|
||||
}
|
||||
|
||||
if (hexGrid == null || hexGrid.gridCanvas == null) {
|
||||
Debug.LogWarning("ArrowVolleyAnimator: No hexGrid or gridCanvas available");
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2? sourcePos = hexGrid.GetCellLocalPosition(sourceCellIndex);
|
||||
Vector2? targetPos = hexGrid.GetCellLocalPosition(targetCellIndex);
|
||||
|
||||
if (sourcePos == null || targetPos == null) {
|
||||
Debug.LogWarning(
|
||||
$"ArrowVolleyAnimator: Invalid cell indices {sourceCellIndex} or {targetCellIndex}");
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Log(
|
||||
$"ArrowVolleyAnimator: Starting volley from cell {sourceCellIndex} to {targetCellIndex}");
|
||||
StartCoroutine(SpawnArrowVolley(sourcePos.Value, targetPos.Value));
|
||||
}
|
||||
|
||||
private IEnumerator SpawnArrowVolley(Vector2 sourcePosition, Vector2 targetPosition) {
|
||||
Debug.Log(
|
||||
$"ArrowVolleyAnimator: Spawning {arrowCount} arrows from {sourcePosition} to {targetPosition}");
|
||||
var arrows = new List<GameObject>();
|
||||
|
||||
for (int i = 0; i < arrowCount; i++) {
|
||||
// Random offset at source
|
||||
Vector2 sourceOffset = Random.insideUnitCircle * sourceSpread;
|
||||
Vector3 arrowSource = new Vector3(
|
||||
sourcePosition.x + sourceOffset.x,
|
||||
sourcePosition.y + sourceOffset.y,
|
||||
0);
|
||||
|
||||
// Random offset at target
|
||||
Vector2 targetOffset = Random.insideUnitCircle * targetSpread;
|
||||
Vector3 arrowTarget = new Vector3(
|
||||
targetPosition.x + targetOffset.x,
|
||||
targetPosition.y + targetOffset.y,
|
||||
0);
|
||||
|
||||
GameObject arrow = CreateArrow(arrowSource);
|
||||
arrows.Add(arrow);
|
||||
|
||||
float delay = Random.Range(0, launchSpread);
|
||||
var coroutine =
|
||||
StartCoroutine(AnimateArrow(arrow, arrowSource, arrowTarget, delay));
|
||||
_activeCoroutines.Add(coroutine);
|
||||
}
|
||||
|
||||
// Wait for all arrows to complete
|
||||
yield return new WaitForSeconds(flightDuration + launchSpread + 0.1f);
|
||||
|
||||
// Cleanup any remaining arrows
|
||||
foreach (var arrow in arrows) {
|
||||
if (arrow != null) { Destroy(arrow); }
|
||||
}
|
||||
|
||||
_activeCoroutines.Clear();
|
||||
}
|
||||
|
||||
private GameObject CreateArrow(Vector3 localPosition) {
|
||||
// Create arrow as child of gridCanvas, using SpriteRenderer for world-space rendering
|
||||
// This matches how particle effects work on the rotated canvas
|
||||
GameObject arrow = new GameObject("Arrow");
|
||||
arrow.transform.SetParent(hexGrid.gridCanvas.transform, false);
|
||||
|
||||
// Use SpriteRenderer instead of UI Image
|
||||
SpriteRenderer renderer = arrow.AddComponent<SpriteRenderer>();
|
||||
renderer.sprite = arrowSprite;
|
||||
renderer.sortingLayerName = "Effects";
|
||||
|
||||
// Set position using localPosition (like particle effects do)
|
||||
arrow.transform.localPosition = localPosition;
|
||||
arrow.transform.localScale = new Vector3(arrowScale, arrowScale, arrowScale);
|
||||
|
||||
return arrow;
|
||||
}
|
||||
|
||||
private IEnumerator
|
||||
AnimateArrow(GameObject arrow, Vector3 start, Vector3 end, float delay) {
|
||||
if (delay > 0) { yield return new WaitForSeconds(delay); }
|
||||
|
||||
if (arrow == null) { yield break; }
|
||||
|
||||
// Calculate direction for rotation in the canvas plane
|
||||
Vector3 direction = (end - start).normalized;
|
||||
// Rotate arrow to point in travel direction, adjusted for sprite orientation
|
||||
float angle =
|
||||
Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg + spriteRotationOffset;
|
||||
arrow.transform.localRotation = Quaternion.Euler(0, 0, angle);
|
||||
|
||||
float elapsed = 0;
|
||||
float distance = Vector3.Distance(start, end);
|
||||
float peakHeight = distance * arcHeight;
|
||||
|
||||
while (elapsed < flightDuration) {
|
||||
if (arrow == null) { yield break; }
|
||||
|
||||
float t = elapsed / flightDuration;
|
||||
|
||||
// Linear interpolation for x/y
|
||||
Vector3 currentPos = Vector3.Lerp(start, end, t);
|
||||
|
||||
// Add parabolic arc (using Z for height since canvas is rotated)
|
||||
// Positive Z moves toward camera (camera is at Z=70, canvas at Z≈0)
|
||||
float arcOffset = 4 * peakHeight * t * (1 - t);
|
||||
currentPos.z =
|
||||
arcOffset + 5; // Base offset to ensure arrows render in front of hex cells
|
||||
|
||||
arrow.transform.localPosition = currentPos;
|
||||
|
||||
elapsed += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
if (arrow != null) {
|
||||
arrow.transform.localPosition = end;
|
||||
Destroy(arrow);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels all active arrow animations.
|
||||
/// </summary>
|
||||
public void CancelAllAnimations() {
|
||||
foreach (var coroutine in _activeCoroutines) {
|
||||
if (coroutine != null) { StopCoroutine(coroutine); }
|
||||
}
|
||||
_activeCoroutines.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9d77c813a5a84ffb84b02d80a7272167
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -354,6 +354,15 @@ public class HexGrid : MonoBehaviour {
|
||||
return x + y * width;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the local position of a cell for use with effects (arrows, particles).
|
||||
/// Returns the anchoredPosition which maps to localPosition in the rotated canvas.
|
||||
/// </summary>
|
||||
public Vector2? GetCellLocalPosition(int cellIndex) {
|
||||
if (cells == null || cellIndex < 0 || cellIndex >= cells.Length) { return null; }
|
||||
return cells[cellIndex].TerrainImage.rectTransform.anchoredPosition;
|
||||
}
|
||||
|
||||
public void OverlayCell(
|
||||
int cellIndex,
|
||||
Color mainColor,
|
||||
@@ -413,6 +422,7 @@ public class HexGrid : MonoBehaviour {
|
||||
}
|
||||
|
||||
public void SetUnitInfoLabels(int cellIndex, string mainValue, string secondaryValue = null) {
|
||||
if (cells == null) return;
|
||||
cells[cellIndex].UpperUnitInfoLabel.text = mainValue;
|
||||
cells[cellIndex].LowerUnitInfoLabel.text = secondaryValue;
|
||||
|
||||
|
||||
@@ -0,0 +1,382 @@
|
||||
using System.Collections;
|
||||
using Net.Eagle0.Shardok.Api;
|
||||
using UnityEngine;
|
||||
|
||||
using static Net.Eagle0.Shardok.Api.BattalionView.Types;
|
||||
|
||||
namespace Shardok {
|
||||
/// <summary>
|
||||
/// Animates melee combat between two adjacent hexes.
|
||||
/// Swords swing in arcs, spears thrust. Both have shaky, violent motion.
|
||||
/// Uses SpriteRenderer for world-space rendering (like ArrowVolleyAnimator).
|
||||
/// </summary>
|
||||
public class MeleeAnimator : MonoBehaviour {
|
||||
[Header("Weapon Sprites")]
|
||||
[Tooltip("Sword sprite for light infantry")]
|
||||
public Sprite swordSprite;
|
||||
|
||||
[Tooltip("Mace/hammer sprite for heavy infantry")]
|
||||
public Sprite maceSprite;
|
||||
|
||||
[Tooltip("Small spear sprite for light cavalry")]
|
||||
public Sprite smallSpearSprite;
|
||||
|
||||
[Tooltip("Large spear/lance sprite for heavy cavalry")]
|
||||
public Sprite largeSpearSprite;
|
||||
|
||||
[Tooltip("Dagger sprite for longbowmen")]
|
||||
public Sprite daggerSprite;
|
||||
|
||||
[Tooltip("Bone/fist sprite for undead")]
|
||||
public Sprite boneSprite;
|
||||
|
||||
[Header("Animation Settings")]
|
||||
[Tooltip("Base scale of weapon sprites")]
|
||||
public float weaponScale = 20.0f;
|
||||
|
||||
[Tooltip("Duration of a single clash cycle in seconds")]
|
||||
public float clashDuration = 0.12f;
|
||||
|
||||
[Tooltip("Number of clash cycles")]
|
||||
public int clashCount = 3;
|
||||
|
||||
[Tooltip("Distance weapons travel toward each other (in local units)")]
|
||||
public float thrustDistance = 25f;
|
||||
|
||||
[Tooltip("Offset from center point along attack axis (in local units)")]
|
||||
public float weaponOffset = 40f;
|
||||
|
||||
[Tooltip("Base rotation offset for diagonal sprites (degrees)")]
|
||||
public float baseRotationOffset = -45f;
|
||||
|
||||
[Tooltip("Swing arc angle for swinging weapons (degrees)")]
|
||||
public float swingArc = 60f;
|
||||
|
||||
[Tooltip("Amount of random shake (in local units)")]
|
||||
public float shakeIntensity = 8f;
|
||||
|
||||
[Header("References")]
|
||||
[Tooltip("HexGrid to get canvas reference from (auto-found if not set)")]
|
||||
public HexGrid hexGrid;
|
||||
|
||||
private Coroutine _activeAnimation;
|
||||
|
||||
private void Start() {
|
||||
if (hexGrid == null) { hexGrid = FindObjectOfType<HexGrid>(); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts a melee animation between two adjacent hex cells.
|
||||
/// </summary>
|
||||
/// <param name="attackerCellIndex">Grid index of the attacking hex</param>
|
||||
/// <param name="defenderCellIndex">Grid index of the defending hex</param>
|
||||
/// <param name="attackerType">Battalion type of the attacker</param>
|
||||
/// <param name="defenderType">Battalion type of the defender</param>
|
||||
public void AnimateMelee(
|
||||
int attackerCellIndex,
|
||||
int defenderCellIndex,
|
||||
BattalionTypeId attackerType,
|
||||
BattalionTypeId defenderType) {
|
||||
if (swordSprite == null && maceSprite == null && smallSpearSprite == null &&
|
||||
largeSpearSprite == null && daggerSprite == null && boneSprite == null) {
|
||||
Debug.LogWarning("MeleeAnimator: No weapon sprites assigned");
|
||||
return;
|
||||
}
|
||||
|
||||
if (hexGrid == null || hexGrid.gridCanvas == null) {
|
||||
Debug.LogWarning("MeleeAnimator: No hexGrid or gridCanvas available");
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2? attackerPos = hexGrid.GetCellLocalPosition(attackerCellIndex);
|
||||
Vector2? defenderPos = hexGrid.GetCellLocalPosition(defenderCellIndex);
|
||||
|
||||
if (attackerPos == null || defenderPos == null) {
|
||||
Debug.LogWarning(
|
||||
$"MeleeAnimator: Invalid cell indices {attackerCellIndex} or {defenderCellIndex}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_activeAnimation != null) { StopCoroutine(_activeAnimation); }
|
||||
|
||||
Debug.Log(
|
||||
$"MeleeAnimator: Starting melee from cell {attackerCellIndex} ({attackerType}) to {defenderCellIndex} ({defenderType})");
|
||||
_activeAnimation = StartCoroutine(
|
||||
AnimateClash(attackerPos.Value, defenderPos.Value, attackerType, defenderType));
|
||||
}
|
||||
|
||||
private struct WeaponConfig {
|
||||
public Sprite sprite;
|
||||
public bool swings; // true = swing arc, false = thrust
|
||||
public float scaleMultiplier;
|
||||
public float violenceMultiplier; // shake intensity multiplier
|
||||
}
|
||||
|
||||
private WeaponConfig GetWeaponConfig(BattalionTypeId type) {
|
||||
switch (type) {
|
||||
case BattalionTypeId.LightInfantry:
|
||||
return new WeaponConfig {
|
||||
sprite = swordSprite,
|
||||
swings = true,
|
||||
scaleMultiplier = 1.0f,
|
||||
violenceMultiplier = 0.3f
|
||||
};
|
||||
case BattalionTypeId.HeavyInfantry:
|
||||
return new WeaponConfig {
|
||||
sprite = maceSprite ?? swordSprite,
|
||||
swings = true,
|
||||
scaleMultiplier = 1.2f,
|
||||
violenceMultiplier = 0.4f
|
||||
};
|
||||
case BattalionTypeId.LightCavalry:
|
||||
return new WeaponConfig {
|
||||
sprite = smallSpearSprite ?? largeSpearSprite ?? swordSprite,
|
||||
swings = false,
|
||||
scaleMultiplier = 0.9f,
|
||||
violenceMultiplier = 1.0f
|
||||
};
|
||||
case BattalionTypeId.HeavyCavalry:
|
||||
return new WeaponConfig {
|
||||
sprite = largeSpearSprite ?? smallSpearSprite ?? swordSprite,
|
||||
swings = false,
|
||||
scaleMultiplier = 1.4f,
|
||||
violenceMultiplier = 1.8f
|
||||
};
|
||||
case BattalionTypeId.Longbowmen:
|
||||
return new WeaponConfig {
|
||||
sprite = daggerSprite ?? swordSprite,
|
||||
swings = false,
|
||||
scaleMultiplier = 0.7f,
|
||||
violenceMultiplier = 0.8f
|
||||
};
|
||||
case BattalionTypeId.Undead:
|
||||
return new WeaponConfig {
|
||||
sprite = boneSprite ?? swordSprite,
|
||||
swings = true,
|
||||
scaleMultiplier = 1.0f,
|
||||
violenceMultiplier = 0.5f
|
||||
};
|
||||
default:
|
||||
return new WeaponConfig {
|
||||
sprite = swordSprite,
|
||||
swings = true,
|
||||
scaleMultiplier = 1.0f,
|
||||
violenceMultiplier = 0.3f
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator AnimateClash(
|
||||
Vector2 attackerPos,
|
||||
Vector2 defenderPos,
|
||||
BattalionTypeId attackerType,
|
||||
BattalionTypeId defenderType) {
|
||||
// Calculate midpoint and direction
|
||||
Vector2 midpoint = (attackerPos + defenderPos) / 2f;
|
||||
Vector2 direction = (defenderPos - attackerPos).normalized;
|
||||
float baseAngle = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg;
|
||||
|
||||
// Get weapon configs
|
||||
WeaponConfig attackerConfig = GetWeaponConfig(attackerType);
|
||||
WeaponConfig defenderConfig = GetWeaponConfig(defenderType);
|
||||
|
||||
// Create weapons with type-specific scaling
|
||||
GameObject attackerWeapon = CreateWeapon(attackerConfig, baseAngle);
|
||||
GameObject defenderWeapon = CreateWeapon(defenderConfig, baseAngle + 180f);
|
||||
|
||||
bool attackerSwings = attackerConfig.swings;
|
||||
bool defenderSwings = defenderConfig.swings;
|
||||
float attackerViolence = shakeIntensity * attackerConfig.violenceMultiplier;
|
||||
float defenderViolence = shakeIntensity * defenderConfig.violenceMultiplier;
|
||||
|
||||
// Starting positions offset from midpoint
|
||||
Vector3 attackerStart = new Vector3(
|
||||
midpoint.x - direction.x * weaponOffset,
|
||||
midpoint.y - direction.y * weaponOffset,
|
||||
10f);
|
||||
Vector3 defenderStart = new Vector3(
|
||||
midpoint.x + direction.x * weaponOffset,
|
||||
midpoint.y + direction.y * weaponOffset,
|
||||
10f);
|
||||
|
||||
// Clash point (closer to midpoint)
|
||||
Vector3 attackerClash = new Vector3(
|
||||
midpoint.x - direction.x * (weaponOffset - thrustDistance),
|
||||
midpoint.y - direction.y * (weaponOffset - thrustDistance),
|
||||
10f);
|
||||
Vector3 defenderClash = new Vector3(
|
||||
midpoint.x + direction.x * (weaponOffset - thrustDistance),
|
||||
midpoint.y + direction.y * (weaponOffset - thrustDistance),
|
||||
10f);
|
||||
|
||||
attackerWeapon.transform.localPosition = attackerStart;
|
||||
defenderWeapon.transform.localPosition = defenderStart;
|
||||
|
||||
float attackerBaseAngle = baseAngle + baseRotationOffset;
|
||||
float defenderBaseAngle = baseAngle + 180f + baseRotationOffset;
|
||||
|
||||
// Animate clash cycles with alternating swing directions
|
||||
for (int i = 0; i < clashCount; i++) {
|
||||
// Alternate swing direction each cycle
|
||||
float swingDirection = (i % 2 == 0) ? 1f : -1f;
|
||||
|
||||
// Swing/thrust toward clash point
|
||||
yield return StartCoroutine(AnimateWeaponSwing(
|
||||
attackerWeapon,
|
||||
attackerStart,
|
||||
attackerClash,
|
||||
attackerBaseAngle,
|
||||
attackerSwings ? -swingArc * swingDirection : 0f,
|
||||
attackerSwings ? 0f : 0f,
|
||||
clashDuration / 2f,
|
||||
true,
|
||||
attackerViolence));
|
||||
|
||||
yield return StartCoroutine(AnimateWeaponSwing(
|
||||
defenderWeapon,
|
||||
defenderStart,
|
||||
defenderClash,
|
||||
defenderBaseAngle,
|
||||
defenderSwings ? swingArc * swingDirection : 0f,
|
||||
defenderSwings ? 0f : 0f,
|
||||
clashDuration / 2f,
|
||||
true,
|
||||
defenderViolence));
|
||||
|
||||
// Brief violent shake at impact (use max violence of both)
|
||||
float impactViolence = Mathf.Max(attackerViolence, defenderViolence);
|
||||
yield return StartCoroutine(ImpactShake(
|
||||
attackerWeapon,
|
||||
defenderWeapon,
|
||||
attackerClash,
|
||||
defenderClash,
|
||||
0.05f,
|
||||
impactViolence));
|
||||
|
||||
// Swing/pull back
|
||||
yield return StartCoroutine(AnimateWeaponSwing(
|
||||
attackerWeapon,
|
||||
attackerClash,
|
||||
attackerStart,
|
||||
attackerBaseAngle,
|
||||
0f,
|
||||
attackerSwings ? -swingArc * swingDirection : 0f,
|
||||
clashDuration / 2f,
|
||||
false,
|
||||
attackerViolence));
|
||||
|
||||
yield return StartCoroutine(AnimateWeaponSwing(
|
||||
defenderWeapon,
|
||||
defenderClash,
|
||||
defenderStart,
|
||||
defenderBaseAngle,
|
||||
0f,
|
||||
defenderSwings ? swingArc * swingDirection : 0f,
|
||||
clashDuration / 2f,
|
||||
false,
|
||||
defenderViolence));
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
if (attackerWeapon != null) { Destroy(attackerWeapon); }
|
||||
if (defenderWeapon != null) { Destroy(defenderWeapon); }
|
||||
_activeAnimation = null;
|
||||
}
|
||||
|
||||
private GameObject CreateWeapon(WeaponConfig config, float angle) {
|
||||
GameObject weapon = new GameObject("MeleeWeapon");
|
||||
weapon.transform.SetParent(hexGrid.gridCanvas.transform, false);
|
||||
|
||||
SpriteRenderer renderer = weapon.AddComponent<SpriteRenderer>();
|
||||
renderer.sprite = config.sprite;
|
||||
renderer.sortingLayerName = "Effects";
|
||||
|
||||
weapon.transform.localRotation = Quaternion.Euler(0, 0, angle + baseRotationOffset);
|
||||
float scale = weaponScale * config.scaleMultiplier;
|
||||
weapon.transform.localScale = new Vector3(scale, scale, scale);
|
||||
|
||||
return weapon;
|
||||
}
|
||||
|
||||
private IEnumerator AnimateWeaponSwing(
|
||||
GameObject weapon,
|
||||
Vector3 fromPos,
|
||||
Vector3 toPos,
|
||||
float baseAngle,
|
||||
float fromAngleOffset,
|
||||
float toAngleOffset,
|
||||
float duration,
|
||||
bool isAttacking,
|
||||
float violence) {
|
||||
float elapsed = 0f;
|
||||
|
||||
while (elapsed < duration) {
|
||||
if (weapon == null) { yield break; }
|
||||
|
||||
float t = elapsed / duration;
|
||||
// Aggressive ease for attack, smoother for retreat
|
||||
float smoothT = isAttacking ? t * t * (3f - 2f * t) : t * (2f - t);
|
||||
|
||||
// Position with shake (scaled by violence)
|
||||
Vector3 shake = new Vector3(
|
||||
Random.Range(-violence, violence),
|
||||
Random.Range(-violence, violence),
|
||||
0f);
|
||||
weapon.transform.localPosition = Vector3.Lerp(fromPos, toPos, smoothT) + shake;
|
||||
|
||||
// Rotation (for swinging weapons)
|
||||
float currentAngle =
|
||||
Mathf.Lerp(baseAngle + fromAngleOffset, baseAngle + toAngleOffset, smoothT);
|
||||
weapon.transform.localRotation = Quaternion.Euler(0, 0, currentAngle);
|
||||
|
||||
elapsed += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
if (weapon != null) {
|
||||
weapon.transform.localPosition = toPos;
|
||||
weapon.transform.localRotation = Quaternion.Euler(0, 0, baseAngle + toAngleOffset);
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator ImpactShake(
|
||||
GameObject weapon1,
|
||||
GameObject weapon2,
|
||||
Vector3 pos1,
|
||||
Vector3 pos2,
|
||||
float duration,
|
||||
float violence) {
|
||||
float elapsed = 0f;
|
||||
float intensity = violence * 2f; // Extra intense at impact
|
||||
|
||||
while (elapsed < duration) {
|
||||
if (weapon1 == null || weapon2 == null) { yield break; }
|
||||
|
||||
Vector3 shake1 = new Vector3(
|
||||
Random.Range(-intensity, intensity),
|
||||
Random.Range(-intensity, intensity),
|
||||
0f);
|
||||
Vector3 shake2 = new Vector3(
|
||||
Random.Range(-intensity, intensity),
|
||||
Random.Range(-intensity, intensity),
|
||||
0f);
|
||||
|
||||
weapon1.transform.localPosition = pos1 + shake1;
|
||||
weapon2.transform.localPosition = pos2 + shake2;
|
||||
|
||||
elapsed += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels any active melee animation.
|
||||
/// </summary>
|
||||
public void CancelAnimation() {
|
||||
if (_activeAnimation != null) {
|
||||
StopCoroutine(_activeAnimation);
|
||||
_activeAnimation = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aef150a57b2b74d6d981bbb46012223f
|
||||
+258
-8
@@ -20,6 +20,44 @@ using static Net.Eagle0.Shardok.Api.BattalionView.Types;
|
||||
using Random = System.Random;
|
||||
|
||||
namespace Shardok {
|
||||
/// <summary>
|
||||
/// Unified animation type used to trigger both animations and sounds.
|
||||
/// Maps from both CommandType (player actions) and ActionType (history results).
|
||||
/// </summary>
|
||||
public enum AnimationType {
|
||||
None,
|
||||
// Combat
|
||||
Melee,
|
||||
Archery,
|
||||
Charge,
|
||||
Move,
|
||||
Fear,
|
||||
// Magic
|
||||
LightningBolt,
|
||||
MeteorCast,
|
||||
HolyWave,
|
||||
RaiseDead,
|
||||
Control,
|
||||
// Fire
|
||||
StartFire,
|
||||
ExtinguishFire,
|
||||
FireDamage,
|
||||
FireSpread,
|
||||
// Water/Ice
|
||||
FreezeWater,
|
||||
BuildBridge,
|
||||
BraveWater,
|
||||
WaterDamage,
|
||||
// Unit management
|
||||
Scout,
|
||||
DismissUnit,
|
||||
Repair,
|
||||
Reduce,
|
||||
Flee,
|
||||
// Other
|
||||
Duel
|
||||
}
|
||||
|
||||
public class ShardokGameController : MonoBehaviour, IClickReceiver {
|
||||
public HexGrid hexGrid;
|
||||
public HexGrid HexGrid => hexGrid;
|
||||
@@ -69,6 +107,8 @@ namespace Shardok {
|
||||
|
||||
public ParticleSystem fireEffectPrefab;
|
||||
public Texture bridgeImage;
|
||||
public ArrowVolleyAnimator arrowVolleyAnimator;
|
||||
public MeleeAnimator meleeAnimator;
|
||||
|
||||
public CommandTypeUIManager commandTypeUIManager;
|
||||
public ActionResultTypeManager actionResultTypeManager;
|
||||
@@ -469,8 +509,48 @@ namespace Shardok {
|
||||
turnHistoryPanel.AddLine(GetActionResultDescription(historyEntry));
|
||||
|
||||
ActionType type = historyEntry.Type;
|
||||
var thisSound = soundManager.SoundForType(type);
|
||||
if (thisSound != null) { audioClipSource.PlayOneShot(thisSound, 1.0f); }
|
||||
AnimationType animationType = AnimationTypeForAction(type);
|
||||
|
||||
bool isOtherPlayer =
|
||||
historyEntry.Player != null && historyEntry.Player != Model.PlayerId;
|
||||
|
||||
// For other players' actions with animations, use unified helper
|
||||
if (animationType != AnimationType.None && isOtherPlayer &&
|
||||
historyEntry.Actor != null) {
|
||||
if (Model.UnitsById.TryGetValue(
|
||||
historyEntry.Actor.Value,
|
||||
out var actorUnit)) {
|
||||
int sourceIndex = MapCoordsToGridIndex(actorUnit.Location);
|
||||
|
||||
// Get target - either coords (archery) or unit location (melee)
|
||||
int targetIndex = -1;
|
||||
UnitViewWithName targetUnit = null;
|
||||
|
||||
if (historyEntry.TargetCoords != null) {
|
||||
targetIndex = MapCoordsToGridIndex(historyEntry.TargetCoords);
|
||||
}
|
||||
if (historyEntry.TargetUnit != null &&
|
||||
Model.UnitsById.TryGetValue(
|
||||
historyEntry.TargetUnit.Value,
|
||||
out targetUnit)) {
|
||||
targetIndex = MapCoordsToGridIndex(targetUnit.Location);
|
||||
}
|
||||
|
||||
if (targetIndex >= 0) {
|
||||
PlayAnimationAndSound(
|
||||
animationType,
|
||||
sourceIndex,
|
||||
targetIndex,
|
||||
actorUnit,
|
||||
targetUnit);
|
||||
}
|
||||
}
|
||||
} else if (isOtherPlayer) {
|
||||
// Other player's action without animation - just play sound
|
||||
var thisSound = soundManager.SoundForType(type);
|
||||
if (thisSound != null) { audioClipSource.PlayOneShot(thisSound, 1.0f); }
|
||||
}
|
||||
// Current player's actions: sound was already played in PerformAction
|
||||
}
|
||||
_lastRetrievedHistoryCount = Model.History.Count;
|
||||
turnHistoryButtonText.text = GetActionResultDescription(Model.History.Last());
|
||||
@@ -1175,16 +1255,186 @@ namespace Shardok {
|
||||
return Model.HasPlaceUnitCommand(GridIndexToMapCoords(finishGridIndex), unit.UnitId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a CommandType to an AnimationType.
|
||||
/// </summary>
|
||||
static AnimationType AnimationTypeForCommand(CommandType commandType) {
|
||||
switch (commandType) {
|
||||
// Combat
|
||||
case CommandType.MeleeCommand: return AnimationType.Melee;
|
||||
case CommandType.ArcheryCommand: return AnimationType.Archery;
|
||||
case CommandType.ChargeCommand: return AnimationType.Charge;
|
||||
case CommandType.MoveCommand: return AnimationType.Move;
|
||||
// Magic
|
||||
case CommandType.LightningBoltCommand: return AnimationType.LightningBolt;
|
||||
case CommandType.MeteorStartCommand:
|
||||
case CommandType.MeteorTargetCommand: return AnimationType.MeteorCast;
|
||||
case CommandType.HolyWaveCommand: return AnimationType.HolyWave;
|
||||
case CommandType.RaiseDeadCommand: return AnimationType.RaiseDead;
|
||||
case CommandType.ControlCommand: return AnimationType.Control;
|
||||
// Fire
|
||||
case CommandType.StartFireCommand: return AnimationType.StartFire;
|
||||
case CommandType.ExtinguishFireCommand: return AnimationType.ExtinguishFire;
|
||||
// Water/Ice
|
||||
case CommandType.FreezeWaterCommand: return AnimationType.FreezeWater;
|
||||
case CommandType.BuildBridgeCommand: return AnimationType.BuildBridge;
|
||||
case CommandType.BraveWaterCommand: return AnimationType.BraveWater;
|
||||
// Unit management
|
||||
case CommandType.ScoutCommand: return AnimationType.Scout;
|
||||
case CommandType.DismissUnitCommand: return AnimationType.DismissUnit;
|
||||
case CommandType.RepairCommand: return AnimationType.Repair;
|
||||
case CommandType.ReduceCommand: return AnimationType.Reduce;
|
||||
case CommandType.FleeCommand: return AnimationType.Flee;
|
||||
case CommandType.FearCommand: return AnimationType.Fear;
|
||||
// Other
|
||||
case CommandType.ChallengeDuelCommand: return AnimationType.Duel;
|
||||
default: return AnimationType.None;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an ActionType to an AnimationType.
|
||||
/// </summary>
|
||||
static AnimationType AnimationTypeForAction(ActionType actionType) {
|
||||
switch (actionType) {
|
||||
// Combat
|
||||
case ActionType.MeleeAttack:
|
||||
case ActionType.MeleeAmbush: return AnimationType.Melee;
|
||||
case ActionType.ArcheryAttack:
|
||||
case ActionType.ArcheryAmbush: return AnimationType.Archery;
|
||||
case ActionType.ChargeAttack: return AnimationType.Charge;
|
||||
case ActionType.Move: return AnimationType.Move;
|
||||
case ActionType.FearAttack: return AnimationType.Fear;
|
||||
// Magic
|
||||
case ActionType.LightningBolt: return AnimationType.LightningBolt;
|
||||
case ActionType.MeteorStart:
|
||||
case ActionType.MeteorTarget:
|
||||
case ActionType.MeteorCast: return AnimationType.MeteorCast;
|
||||
case ActionType.HolyWave:
|
||||
case ActionType.HolyWaveDamage: return AnimationType.HolyWave;
|
||||
case ActionType.RaisedUndead: return AnimationType.RaiseDead;
|
||||
case ActionType.Controlled: return AnimationType.Control;
|
||||
// Fire
|
||||
case ActionType.StartFire: return AnimationType.StartFire;
|
||||
case ActionType.ExtinguishFire: return AnimationType.ExtinguishFire;
|
||||
case ActionType.FireDamage: return AnimationType.FireDamage;
|
||||
case ActionType.FireSpread: return AnimationType.FireSpread;
|
||||
// Water/Ice
|
||||
case ActionType.FreezeWater: return AnimationType.FreezeWater;
|
||||
case ActionType.BuildBridge: return AnimationType.BuildBridge;
|
||||
case ActionType.CrossedWater: return AnimationType.BraveWater;
|
||||
case ActionType.WaterDamage: return AnimationType.WaterDamage;
|
||||
// Unit management
|
||||
case ActionType.Scouted: return AnimationType.Scout;
|
||||
case ActionType.DismissUnit: return AnimationType.DismissUnit;
|
||||
case ActionType.Repair: return AnimationType.Repair;
|
||||
case ActionType.Reduce: return AnimationType.Reduce;
|
||||
case ActionType.Fled: return AnimationType.Flee;
|
||||
// Other
|
||||
case ActionType.DuelChallenged:
|
||||
case ActionType.DuelAccepted:
|
||||
case ActionType.DuelDeclined: return AnimationType.Duel;
|
||||
default: return AnimationType.None;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the ActionType to use for sound lookup for a given AnimationType.
|
||||
/// </summary>
|
||||
static ActionType ActionTypeForSound(AnimationType animationType) {
|
||||
switch (animationType) {
|
||||
// Combat
|
||||
case AnimationType.Melee: return ActionType.MeleeAttack;
|
||||
case AnimationType.Archery: return ActionType.ArcheryAttack;
|
||||
case AnimationType.Charge: return ActionType.ChargeAttack;
|
||||
case AnimationType.Move: return ActionType.Move;
|
||||
case AnimationType.Fear: return ActionType.FearAttack;
|
||||
// Magic
|
||||
case AnimationType.LightningBolt: return ActionType.LightningBolt;
|
||||
case AnimationType.MeteorCast: return ActionType.MeteorCast;
|
||||
case AnimationType.HolyWave: return ActionType.HolyWave;
|
||||
case AnimationType.RaiseDead: return ActionType.RaisedUndead;
|
||||
case AnimationType.Control: return ActionType.Controlled;
|
||||
// Fire
|
||||
case AnimationType.StartFire: return ActionType.StartFire;
|
||||
case AnimationType.ExtinguishFire: return ActionType.ExtinguishFire;
|
||||
case AnimationType.FireDamage: return ActionType.FireDamage;
|
||||
case AnimationType.FireSpread: return ActionType.FireSpread;
|
||||
// Water/Ice
|
||||
case AnimationType.FreezeWater: return ActionType.FreezeWater;
|
||||
case AnimationType.BuildBridge: return ActionType.BuildBridge;
|
||||
case AnimationType.BraveWater: return ActionType.CrossedWater;
|
||||
case AnimationType.WaterDamage: return ActionType.WaterDamage;
|
||||
// Unit management
|
||||
case AnimationType.Scout: return ActionType.Scouted;
|
||||
case AnimationType.DismissUnit: return ActionType.DismissUnit;
|
||||
case AnimationType.Repair: return ActionType.Repair;
|
||||
case AnimationType.Reduce: return ActionType.Reduce;
|
||||
case AnimationType.Flee: return ActionType.Fled;
|
||||
// Other
|
||||
case AnimationType.Duel: return ActionType.DuelChallenged;
|
||||
default: return ActionType.UnknownAction;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays the appropriate animation and sound for an action.
|
||||
/// </summary>
|
||||
void PlayAnimationAndSound(
|
||||
AnimationType animationType,
|
||||
int sourceGridIndex,
|
||||
int targetGridIndex,
|
||||
UnitViewWithName attackerUnit,
|
||||
UnitViewWithName defenderUnit) {
|
||||
if (animationType == AnimationType.None) return;
|
||||
|
||||
// Play sound
|
||||
var sound = soundManager.SoundForType(ActionTypeForSound(animationType));
|
||||
if (sound != null) { audioClipSource.PlayOneShot(sound, 1.0f); }
|
||||
|
||||
// Play animation
|
||||
switch (animationType) {
|
||||
case AnimationType.Archery:
|
||||
if (arrowVolleyAnimator != null) {
|
||||
arrowVolleyAnimator.AnimateVolley(sourceGridIndex, targetGridIndex);
|
||||
}
|
||||
break;
|
||||
case AnimationType.Melee:
|
||||
if (meleeAnimator != null && attackerUnit != null && defenderUnit != null) {
|
||||
meleeAnimator.AnimateMelee(
|
||||
sourceGridIndex,
|
||||
targetGridIndex,
|
||||
attackerUnit.Battalion.Type,
|
||||
defenderUnit.Battalion.Type);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void PerformAction(int startGridIndex, int finishGridIndex) {
|
||||
Coords startCoords = GridIndexToMapCoords(startGridIndex);
|
||||
Coords finishCoords = GridIndexToMapCoords(finishGridIndex);
|
||||
|
||||
bool success = Model.PerformTargetedCommand(
|
||||
startCoords,
|
||||
finishCoords,
|
||||
commandTypeUIManager.CommandTypesForGroup(_displayedCommandGroup));
|
||||
if (success && Model.InSetUp) {
|
||||
audioClipSource.PlayOneShot(soundManager.GenericClickSound());
|
||||
var commandTypes = commandTypeUIManager.CommandTypesForGroup(_displayedCommandGroup);
|
||||
CommandType? executedCommand =
|
||||
Model.PerformTargetedCommand(startCoords, finishCoords, commandTypes);
|
||||
|
||||
if (executedCommand != null) {
|
||||
var attackerUnit = Model.UnitAtCoords(startCoords);
|
||||
var defenderUnit = Model.UnitAtCoords(finishCoords);
|
||||
var animationType = AnimationTypeForCommand(executedCommand.Value);
|
||||
|
||||
if (animationType != AnimationType.None) {
|
||||
PlayAnimationAndSound(
|
||||
animationType,
|
||||
startGridIndex,
|
||||
finishGridIndex,
|
||||
attackerUnit,
|
||||
defenderUnit);
|
||||
} else if (Model.InSetUp) {
|
||||
// For commands without specific animations, play generic click in setup
|
||||
audioClipSource.PlayOneShot(soundManager.GenericClickSound());
|
||||
}
|
||||
}
|
||||
hexGrid.ClearOverlays();
|
||||
|
||||
|
||||
@@ -267,18 +267,19 @@ public class ShardokGameModel {
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool
|
||||
PerformTargetedCommand(Coords start, Coords target, List<CommandType> possibleTypes) {
|
||||
public CommandType? PerformTargetedCommand(
|
||||
Coords start,
|
||||
Coords target,
|
||||
List<CommandType> possibleTypes) {
|
||||
List<CommandDescriptor> heroActions = GetCommandsForCoords(start);
|
||||
|
||||
foreach (CommandDescriptor action in heroActions) {
|
||||
if (target.Equals(action.Target) && possibleTypes.Contains(action.Type)) {
|
||||
// Handle Target actions
|
||||
PostAction(action);
|
||||
return true;
|
||||
return action.Type;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
public bool PerformReinforceCommand(Coords start, Coords target, UnitId reinforcedUnitId) {
|
||||
|
||||
@@ -25,10 +25,9 @@ namespace common {
|
||||
// Client that doesn't follow redirects, so we can retry each hop independently
|
||||
private readonly HttpClient _noRedirectClient;
|
||||
|
||||
// Auth header to send only to eagle0.net (not to S3 signed URLs)
|
||||
private readonly System.Net.Http.Headers.AuthenticationHeaderValue _authHeader;
|
||||
|
||||
private static readonly Uri BaseUri = new("https://eagle0.net/assets/headshots/");
|
||||
// Public CDN URL for headshots (no auth required)
|
||||
private static readonly Uri BaseUri =
|
||||
new("https://eagle0-headshots.sfo3.cdn.digitaloceanspaces.com/");
|
||||
|
||||
private const int MaxRetryAttempts = 5;
|
||||
private const int MaxRedirectHops = 5;
|
||||
@@ -73,9 +72,6 @@ namespace common {
|
||||
};
|
||||
_noRedirectClient = new HttpClient(
|
||||
handler) { Timeout = TimeSpan.FromSeconds(RequestTimeoutSeconds) };
|
||||
// Store auth header to add per-request only for eagle0.net
|
||||
// (sending it to S3 signed URLs causes HTTP 400)
|
||||
_authHeader = httpClient.DefaultRequestHeaders.Authorization;
|
||||
|
||||
// Start periodic retry timer
|
||||
_periodicRetryTimer = new Timer(
|
||||
@@ -163,10 +159,6 @@ namespace common {
|
||||
try {
|
||||
var request =
|
||||
new HttpRequestMessage { Method = HttpMethod.Get, RequestUri = uri };
|
||||
// Only send auth header to eagle0.net, not to S3 signed URLs
|
||||
if (_authHeader != null && uri.Host.EndsWith("eagle0.net")) {
|
||||
request.Headers.Authorization = _authHeader;
|
||||
}
|
||||
var response = await _noRedirectClient.SendAsync(request);
|
||||
|
||||
var statusCode = (int)response.StatusCode;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!78 &1
|
||||
TagManager:
|
||||
serializedVersion: 2
|
||||
serializedVersion: 3
|
||||
tags: []
|
||||
layers:
|
||||
- Default
|
||||
@@ -11,7 +11,7 @@ TagManager:
|
||||
-
|
||||
- Water
|
||||
- UI
|
||||
-
|
||||
- Effects
|
||||
-
|
||||
- Dropdown UI
|
||||
-
|
||||
@@ -53,3 +53,5 @@ TagManager:
|
||||
- name: UI Highest
|
||||
uniqueID: 3536461817
|
||||
locked: 0
|
||||
m_RenderingLayers:
|
||||
- Default
|
||||
|
||||
@@ -11,8 +11,11 @@ go_library(
|
||||
visibility = ["//visibility:private"],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:api_go_proto", # keep
|
||||
"//src/main/protobuf/net/eagle0/eagle/api/admin:admin_go_proto",
|
||||
"@org_golang_google_grpc//:grpc",
|
||||
"@org_golang_google_grpc//credentials",
|
||||
"@org_golang_google_grpc//credentials/insecure",
|
||||
"@org_golang_google_grpc//metadata",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user