mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-29 05:15:44 +00:00
Compare commits
57
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d55bb8436 | ||
|
|
7a31d34aba | ||
|
|
9a4dda0eea | ||
|
|
353eb3907c | ||
|
|
ded06ba815 | ||
|
|
70392b109a | ||
|
|
821e3f1a4a | ||
|
|
ebe819a5a1 | ||
|
|
3e868d0305 | ||
|
|
e46867333c | ||
|
|
2d59d4ff51 | ||
|
|
d493182184 | ||
|
|
8ce4e4c4a9 | ||
|
|
9729110ea8 | ||
|
|
2224e78a36 | ||
|
|
007a57eea2 | ||
|
|
3febf2a6cd | ||
|
|
26204cc879 | ||
|
|
7032260a31 | ||
|
|
23ab715ffc | ||
|
|
73f42d5b51 | ||
|
|
3cb61f69f4 | ||
|
|
62fff3c0ea | ||
|
|
ca86926a25 | ||
|
|
1d334c91ae | ||
|
|
64e1e46e0a | ||
|
|
4722a40384 | ||
|
|
6653a14660 | ||
|
|
056571651e | ||
|
|
d8224e7886 | ||
|
|
9cc9a65e4a | ||
|
|
025563b607 | ||
|
|
030f6b2c2e | ||
|
|
1d5dd7b971 | ||
|
|
649e80c4ac | ||
|
|
a1b4e41553 | ||
|
|
1c24474a0b | ||
|
|
26eec6cabc | ||
|
|
d134f40438 | ||
|
|
a638dd26ca | ||
|
|
5e93b0eaa0 | ||
|
|
6940312d3f | ||
|
|
243f0d43e7 | ||
|
|
1ddd015449 | ||
|
|
da85d0983a | ||
|
|
ae574e6ff5 | ||
|
|
c1ac57b929 | ||
|
|
be51daa148 | ||
|
|
97605d0a63 | ||
|
|
9cca922b70 | ||
|
|
6eab9ffc3c | ||
|
|
08eaffb927 | ||
|
|
a2bc55e0f5 | ||
|
|
19861377a6 | ||
|
|
cc70087efc | ||
|
|
b2383c8384 | ||
|
|
03958b6914 |
@@ -112,6 +112,16 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Copy update-env script to server
|
||||
uses: appleboy/scp-action@v0.1.7
|
||||
with:
|
||||
host: ${{ secrets.DO_DROPLET_IP }}
|
||||
username: deploy
|
||||
key: ${{ secrets.DO_SSH_KEY }}
|
||||
source: "deploy/update-env.sh,deploy/env.template"
|
||||
target: /opt/eagle0/
|
||||
strip_components: 1
|
||||
|
||||
- name: Deploy auth service to production
|
||||
uses: appleboy/ssh-action@v1.0.3
|
||||
with:
|
||||
@@ -124,36 +134,18 @@ jobs:
|
||||
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.tmp6 || true
|
||||
echo "JWT_PRIVATE_KEY=${JWT_PRIVATE_KEY:-}" >> .env.tmp6
|
||||
# Update Fastmail JMAP credentials for email sending
|
||||
grep -v '^FASTMAIL_API_TOKEN=' .env.tmp6 > .env.tmp7 || true
|
||||
echo "FASTMAIL_API_TOKEN=${FASTMAIL_API_TOKEN:-}" >> .env.tmp7
|
||||
grep -v '^FASTMAIL_FROM_EMAIL=' .env.tmp7 > .env.tmp8 || true
|
||||
echo "FASTMAIL_FROM_EMAIL=${FASTMAIL_FROM_EMAIL:-}" >> .env.tmp8
|
||||
grep -v '^FASTMAIL_FROM_NAME=' .env.tmp8 > .env || true
|
||||
echo "FASTMAIL_FROM_NAME=${FASTMAIL_FROM_NAME:-}" >> .env
|
||||
rm -f .env.tmp .env.tmp2 .env.tmp3 .env.tmp4 .env.tmp5 .env.tmp6 .env.tmp7 .env.tmp8
|
||||
chmod 600 .env
|
||||
else
|
||||
echo "ERROR: .env file not found. Run main deploy first."
|
||||
exit 1
|
||||
fi
|
||||
# Update env vars using shared script (preserves vars set by other workflows)
|
||||
chmod +x update-env.sh
|
||||
./update-env.sh \
|
||||
"AUTH_IMAGE=${AUTH_IMAGE}" \
|
||||
"DISCORD_CLIENT_ID=${DISCORD_CLIENT_ID}" \
|
||||
"DISCORD_CLIENT_SECRET=${DISCORD_CLIENT_SECRET}" \
|
||||
"GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID}" \
|
||||
"GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET}" \
|
||||
"JWT_PRIVATE_KEY=${JWT_PRIVATE_KEY}" \
|
||||
"FASTMAIL_API_TOKEN=${FASTMAIL_API_TOKEN}" \
|
||||
"FASTMAIL_FROM_EMAIL=${FASTMAIL_FROM_EMAIL}" \
|
||||
"FASTMAIL_FROM_NAME=${FASTMAIL_FROM_NAME}"
|
||||
|
||||
# Login to registry
|
||||
echo "${{ secrets.DO_REGISTRY_TOKEN }}" | docker login registry.digitalocean.com -u "${{ secrets.DO_REGISTRY_TOKEN }}" --password-stdin
|
||||
|
||||
+169
-416
@@ -26,196 +26,71 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build-eagle:
|
||||
# Single consolidated build job - builds all images with one bazel invocation
|
||||
# This uses 1 runner slot instead of 4, and Bazel parallelizes internally
|
||||
build-all:
|
||||
runs-on: self-hosted
|
||||
outputs:
|
||||
image_tag: ${{ steps.push-eagle.outputs.image_tag }}
|
||||
eagle_image_tag: ${{ steps.push-images.outputs.eagle_image_tag }}
|
||||
shardok_image_tag: ${{ steps.push-images.outputs.shardok_image_tag }}
|
||||
admin_image_tag: ${{ steps.push-images.outputs.admin_image_tag }}
|
||||
jfr_sidecar_image_tag: ${{ steps.push-images.outputs.jfr_sidecar_image_tag }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: false
|
||||
|
||||
- name: Build Eagle Docker image
|
||||
id: build-eagle
|
||||
run: |
|
||||
set -ex
|
||||
bazel build --platforms=//:linux_x86_64 //ci:eagle_server_image
|
||||
|
||||
# Save the resolved path before any other bazel command changes bazel-bin symlink
|
||||
IMAGE_PATH=$(readlink -f bazel-bin/ci/eagle_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
|
||||
# Also set for current directory in case Bazel uses different home
|
||||
mkdir -p .docker
|
||||
cp ~/.docker/config.json .docker/
|
||||
|
||||
- name: Push Eagle image to DO registry
|
||||
id: push-eagle
|
||||
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
|
||||
env:
|
||||
DOCKER_CONFIG: ${{ github.workspace }}/.docker
|
||||
- name: Build all Docker images
|
||||
id: build-all
|
||||
run: |
|
||||
set -ex
|
||||
|
||||
# Use cross-compiled image path from build step
|
||||
EAGLE_IMAGE="${{ steps.build-eagle.outputs.image_path }}"
|
||||
echo "Using Eagle image: $EAGLE_IMAGE"
|
||||
|
||||
if [ -z "$EAGLE_IMAGE" ] || [ ! -d "$EAGLE_IMAGE" ]; then
|
||||
echo "ERROR: Eagle image not found at: $EAGLE_IMAGE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Debug: show OCI layout contents
|
||||
echo "=== OCI Layout Contents ==="
|
||||
cat "$EAGLE_IMAGE/index.json"
|
||||
echo ""
|
||||
echo "=== Blobs ==="
|
||||
ls -la "$EAGLE_IMAGE/blobs/sha256/" | head -20
|
||||
|
||||
# Verify OCI layout consistency before pushing
|
||||
echo "=== Verifying OCI layout consistency ==="
|
||||
for digest in $(cat "$EAGLE_IMAGE/index.json" | grep -o '"sha256:[^"]*"' | tr -d '"'); do
|
||||
blob_path="$EAGLE_IMAGE/blobs/${digest/://}"
|
||||
if [ ! -f "$blob_path" ]; then
|
||||
echo "ERROR: Blob not found: $blob_path"
|
||||
exit 1
|
||||
fi
|
||||
actual_digest="sha256:$(shasum -a 256 "$blob_path" | cut -d' ' -f1)"
|
||||
if [ "$digest" != "$actual_digest" ]; then
|
||||
echo "ERROR: Digest mismatch for $blob_path"
|
||||
echo " Index says: $digest"
|
||||
echo " Actual: $actual_digest"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ Verified: $digest"
|
||||
done
|
||||
|
||||
# Build the push target to get crane in runfiles
|
||||
bazel build //ci:eagle_server_push
|
||||
|
||||
# Use crane directly for push (avoids OCI->Docker digest mismatch)
|
||||
CRANE="bazel-bin/ci/push_eagle_server_push.sh.runfiles/rules_oci~~oci~oci_crane_darwin_arm64/crane"
|
||||
echo "Using crane: $CRANE"
|
||||
|
||||
# Push with SHA tag
|
||||
GIT_SHA=$(git rev-parse --short=8 HEAD)
|
||||
IMAGE_TAG="registry.digitalocean.com/eagle0/eagle-server:${GIT_SHA}"
|
||||
echo "Pushing eagle image: $IMAGE_TAG"
|
||||
$CRANE push "$EAGLE_IMAGE" "$IMAGE_TAG"
|
||||
|
||||
# Verify push by checking what's in the registry
|
||||
echo "=== Verifying push ==="
|
||||
$CRANE manifest "$IMAGE_TAG" | head -50
|
||||
PUSHED_DIGEST=$($CRANE digest "$IMAGE_TAG")
|
||||
echo "Registry reports digest: $PUSHED_DIGEST"
|
||||
|
||||
# Output the full image tag for deploy step
|
||||
echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT
|
||||
|
||||
# Also update :latest for convenience (but deploy won't use it)
|
||||
echo "Copying to :latest tag"
|
||||
$CRANE copy "$IMAGE_TAG" "registry.digitalocean.com/eagle0/eagle-server:latest"
|
||||
|
||||
build-shardok:
|
||||
runs-on: self-hosted
|
||||
outputs:
|
||||
image_tag: ${{ steps.push-shardok.outputs.image_tag }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: false
|
||||
|
||||
- name: Build Shardok binary (cross-compile for Linux)
|
||||
run: |
|
||||
set -ex
|
||||
|
||||
# Step 1: Build JUST the binary with cross-compilation
|
||||
# We need --extra_toolchains to force the Linux toolchain to be used
|
||||
# because toolchains_llvm registers with dev_dependency=True
|
||||
echo "=== Building shardok-server binary for linux-x86_64 ==="
|
||||
# Build ALL images in a single bazel command - Bazel parallelizes internally
|
||||
# This is much more efficient than 4 separate GitHub Actions jobs
|
||||
echo "=== Building all Docker images ==="
|
||||
bazel build \
|
||||
--platforms=//:linux_x86_64 \
|
||||
--extra_toolchains=@llvm_toolchain_linux//:all \
|
||||
//src/main/cpp/net/eagle0/shardok:shardok-server
|
||||
//ci:eagle_server_image \
|
||||
//ci:shardok_server_image \
|
||||
//ci:admin_server_image \
|
||||
//ci:jfr_sidecar_image \
|
||||
//src/main/go/net/eagle0/warmup:warmup_linux_amd64
|
||||
|
||||
# Step 2: Check the binary directly from bazel-bin
|
||||
# bazel-bin is a symlink that points to the correct output directory
|
||||
LINUX_BIN="bazel-bin/src/main/cpp/net/eagle0/shardok/shardok-server"
|
||||
echo "=== Checking binary at: $LINUX_BIN ==="
|
||||
# Copy warmup binary to scripts/ for deployment
|
||||
mkdir -p scripts/bin
|
||||
cp bazel-bin/src/main/go/net/eagle0/warmup/warmup_linux_amd64_/warmup_linux_amd64 scripts/bin/warmup
|
||||
|
||||
if [ ! -f "$LINUX_BIN" ]; then
|
||||
echo "ERROR: Binary not found at $LINUX_BIN"
|
||||
exit 1
|
||||
fi
|
||||
# Save all image paths before any other bazel command changes bazel-bin symlink
|
||||
EAGLE_PATH=$(readlink -f bazel-bin/ci/eagle_server_image)
|
||||
SHARDOK_PATH=$(readlink -f bazel-bin/ci/shardok_server_image)
|
||||
ADMIN_PATH=$(readlink -f bazel-bin/ci/admin_server_image)
|
||||
JFR_PATH=$(readlink -f bazel-bin/ci/jfr_sidecar_image)
|
||||
|
||||
# Debug: show what bazel-bin points to
|
||||
echo "bazel-bin symlink target: $(readlink bazel-bin || echo 'not a symlink')"
|
||||
echo "eagle_path=$EAGLE_PATH" >> $GITHUB_OUTPUT
|
||||
echo "shardok_path=$SHARDOK_PATH" >> $GITHUB_OUTPUT
|
||||
echo "admin_path=$ADMIN_PATH" >> $GITHUB_OUTPUT
|
||||
echo "jfr_path=$JFR_PATH" >> $GITHUB_OUTPUT
|
||||
|
||||
# Step 3: Verify it's ELF (Linux) not Mach-O (macOS)
|
||||
echo "=== Verifying binary format ==="
|
||||
MAGIC=$(head -c 4 "$LINUX_BIN" | xxd -p)
|
||||
echo "Binary magic bytes: $MAGIC"
|
||||
echo "=== Image paths ==="
|
||||
echo "Eagle: $EAGLE_PATH"
|
||||
echo "Shardok: $SHARDOK_PATH"
|
||||
echo "Admin: $ADMIN_PATH"
|
||||
echo "JFR Sidecar: $JFR_PATH"
|
||||
|
||||
if [ "$MAGIC" = "7f454c46" ]; then
|
||||
echo "SUCCESS: Binary is ELF format (Linux)"
|
||||
elif [ "$MAGIC" = "cfaeedfe" ] || [ "$MAGIC" = "cffaedfe" ]; then
|
||||
echo "ERROR: Binary is Mach-O format (macOS) - cross-compilation failed!"
|
||||
echo ""
|
||||
echo "Debug info:"
|
||||
echo "- bazel-bin points to: $(readlink bazel-bin)"
|
||||
file "$LINUX_BIN" || true
|
||||
exit 1
|
||||
else
|
||||
echo "WARNING: Unknown binary format: $MAGIC"
|
||||
file "$LINUX_BIN" || true
|
||||
fi
|
||||
|
||||
- name: Build Shardok Docker image
|
||||
id: build-shardok
|
||||
run: |
|
||||
set -ex
|
||||
|
||||
# Build the OCI image with cross-compilation flags
|
||||
bazel build \
|
||||
--platforms=//:linux_x86_64 \
|
||||
--extra_toolchains=@llvm_toolchain_linux//:all \
|
||||
//ci:shardok_server_image
|
||||
|
||||
# The image is output to bazel-bin which is a symlink.
|
||||
# Resolve it now before any other bazel commands change where it points.
|
||||
IMAGE_PATH=$(readlink -f bazel-bin/ci/shardok_server_image)
|
||||
echo "Image path: $IMAGE_PATH"
|
||||
echo "image_path=$IMAGE_PATH" >> $GITHUB_OUTPUT
|
||||
|
||||
# Verify the binary inside the tar layer is ELF
|
||||
echo "=== Verifying binary in image tar ==="
|
||||
# Verify Shardok binary is ELF (Linux) format
|
||||
echo "=== Verifying Shardok binary format ==="
|
||||
BINARY_TAR="bazel-bin/ci/shardok_binary_layer.tar"
|
||||
if [ -f "$BINARY_TAR" ]; then
|
||||
echo "Checking binary in $BINARY_TAR"
|
||||
# Extract just the first 4 bytes of the binary from the tar
|
||||
MAGIC=$(tar -xOf "$BINARY_TAR" app/shardok-server 2>/dev/null | head -c 4 | xxd -p)
|
||||
echo "Binary magic in tar: $MAGIC"
|
||||
if [ "$MAGIC" = "7f454c46" ]; then
|
||||
echo "SUCCESS: Binary in tar is ELF format (Linux)"
|
||||
echo "SUCCESS: Binary is ELF format (Linux)"
|
||||
else
|
||||
echo "ERROR: Binary in tar is NOT ELF format!"
|
||||
echo "This means pkg_tar is packaging the wrong binary."
|
||||
echo "ERROR: Binary is NOT ELF format!"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "WARNING: Could not find $BINARY_TAR"
|
||||
fi
|
||||
|
||||
- name: Login to DigitalOcean Container Registry
|
||||
@@ -226,210 +101,79 @@ jobs:
|
||||
mkdir -p ~/.docker
|
||||
AUTH=$(echo -n "${DO_TOKEN}:${DO_TOKEN}" | base64)
|
||||
echo "{\"auths\":{\"registry.digitalocean.com\":{\"auth\":\"${AUTH}\"}}}" > ~/.docker/config.json
|
||||
# Also set for current directory in case Bazel uses different home
|
||||
mkdir -p .docker
|
||||
cp ~/.docker/config.json .docker/
|
||||
|
||||
- name: Push Shardok image to DO registry
|
||||
id: push-shardok
|
||||
- name: Push all images to DO registry
|
||||
id: push-images
|
||||
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
|
||||
|
||||
# Use cross-compiled image path from build step
|
||||
CROSS_IMAGE="${{ steps.build-shardok.outputs.image_path }}"
|
||||
echo "Using cross-compiled image: $CROSS_IMAGE"
|
||||
GIT_SHA=$(git rev-parse --short=8 HEAD)
|
||||
|
||||
if [ -z "$CROSS_IMAGE" ] || [ ! -d "$CROSS_IMAGE" ]; then
|
||||
echo "ERROR: Cross-compiled image not found at: $CROSS_IMAGE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get crane from Eagle push target (which doesn't need cross-compilation)
|
||||
# This gives us a macOS crane binary we can actually run.
|
||||
# We can't build shardok_server_push with platform flags because it would
|
||||
# download a Linux crane that can't run on macOS.
|
||||
# Get crane from push target runfiles
|
||||
bazel build //ci:eagle_server_push
|
||||
CRANE="bazel-bin/ci/push_eagle_server_push.sh.runfiles/rules_oci~~oci~oci_crane_darwin_arm64/crane"
|
||||
|
||||
# Find the Darwin crane binary (may be a symlink)
|
||||
RUNFILES="bazel-bin/ci/push_eagle_server_push.sh.runfiles"
|
||||
CRANE=$(find "$RUNFILES" -path "*darwin*" -name crane 2>/dev/null | head -1)
|
||||
if [ -z "$CRANE" ]; then
|
||||
# Fallback to any crane
|
||||
CRANE=$(find "$RUNFILES" -name crane 2>/dev/null | head -1)
|
||||
if [ ! -e "$CRANE" ]; then
|
||||
# Fallback: find any Darwin crane
|
||||
RUNFILES="bazel-bin/ci/push_eagle_server_push.sh.runfiles"
|
||||
CRANE=$(find "$RUNFILES" -path "*darwin*" -name crane 2>/dev/null | head -1)
|
||||
fi
|
||||
|
||||
if [ -z "$CRANE" ] || [ ! -e "$CRANE" ]; then
|
||||
echo "ERROR: crane not found. Listing runfiles:"
|
||||
find "$RUNFILES" -name crane 2>/dev/null || true
|
||||
echo "ERROR: crane not found"
|
||||
exit 1
|
||||
fi
|
||||
echo "Using crane: $CRANE"
|
||||
|
||||
# Push the cross-compiled image with SHA tag
|
||||
GIT_SHA=$(git rev-parse --short=8 HEAD)
|
||||
IMAGE_TAG="registry.digitalocean.com/eagle0/shardok-server:${GIT_SHA}"
|
||||
echo "Pushing shardok image: $IMAGE_TAG"
|
||||
$CRANE push "$CROSS_IMAGE" "$IMAGE_TAG"
|
||||
# Push Eagle image
|
||||
EAGLE_IMAGE="${{ steps.build-all.outputs.eagle_path }}"
|
||||
EAGLE_TAG="registry.digitalocean.com/eagle0/eagle-server:${GIT_SHA}"
|
||||
echo "Pushing Eagle: $EAGLE_TAG"
|
||||
$CRANE push "$EAGLE_IMAGE" "$EAGLE_TAG"
|
||||
$CRANE copy "$EAGLE_TAG" "registry.digitalocean.com/eagle0/eagle-server:latest"
|
||||
echo "eagle_image_tag=$EAGLE_TAG" >> $GITHUB_OUTPUT
|
||||
|
||||
# Output the full image tag for deploy step
|
||||
echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT
|
||||
# Push Shardok image
|
||||
SHARDOK_IMAGE="${{ steps.build-all.outputs.shardok_path }}"
|
||||
SHARDOK_TAG="registry.digitalocean.com/eagle0/shardok-server:${GIT_SHA}"
|
||||
echo "Pushing Shardok: $SHARDOK_TAG"
|
||||
$CRANE push "$SHARDOK_IMAGE" "$SHARDOK_TAG"
|
||||
$CRANE copy "$SHARDOK_TAG" "registry.digitalocean.com/eagle0/shardok-server:latest"
|
||||
echo "shardok_image_tag=$SHARDOK_TAG" >> $GITHUB_OUTPUT
|
||||
|
||||
# Also update :latest for convenience (but deploy won't use it)
|
||||
echo "Copying to :latest tag"
|
||||
$CRANE copy "$IMAGE_TAG" "registry.digitalocean.com/eagle0/shardok-server:latest"
|
||||
# Push Admin image
|
||||
ADMIN_IMAGE="${{ steps.build-all.outputs.admin_path }}"
|
||||
ADMIN_TAG="registry.digitalocean.com/eagle0/admin-server:${GIT_SHA}"
|
||||
echo "Pushing Admin: $ADMIN_TAG"
|
||||
$CRANE push "$ADMIN_IMAGE" "$ADMIN_TAG"
|
||||
$CRANE copy "$ADMIN_TAG" "registry.digitalocean.com/eagle0/admin-server:latest"
|
||||
echo "admin_image_tag=$ADMIN_TAG" >> $GITHUB_OUTPUT
|
||||
|
||||
build-admin:
|
||||
runs-on: self-hosted
|
||||
outputs:
|
||||
image_tag: ${{ steps.push-admin.outputs.image_tag }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: false
|
||||
# Push JFR Sidecar image
|
||||
JFR_IMAGE="${{ steps.build-all.outputs.jfr_path }}"
|
||||
JFR_TAG="registry.digitalocean.com/eagle0/jfr-sidecar:${GIT_SHA}"
|
||||
echo "Pushing JFR Sidecar: $JFR_TAG"
|
||||
$CRANE push "$JFR_IMAGE" "$JFR_TAG"
|
||||
$CRANE copy "$JFR_TAG" "registry.digitalocean.com/eagle0/jfr-sidecar:latest"
|
||||
echo "jfr_sidecar_image_tag=$JFR_TAG" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build Admin Server Docker image
|
||||
id: build-admin
|
||||
run: |
|
||||
set -ex
|
||||
|
||||
# Build admin server image (Go binary has explicit goos/goarch in BUILD.bazel)
|
||||
bazel build //ci:admin_server_image
|
||||
|
||||
# Save the resolved path before any other bazel command changes bazel-bin symlink
|
||||
IMAGE_PATH=$(readlink -f bazel-bin/ci/admin_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 Admin image to DO registry
|
||||
id: push-admin
|
||||
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
|
||||
|
||||
ADMIN_IMAGE="${{ steps.build-admin.outputs.image_path }}"
|
||||
echo "Using Admin image: $ADMIN_IMAGE"
|
||||
|
||||
if [ -z "$ADMIN_IMAGE" ] || [ ! -d "$ADMIN_IMAGE" ]; then
|
||||
echo "ERROR: Admin image not found at: $ADMIN_IMAGE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build the push target to get crane in runfiles
|
||||
bazel build //ci:admin_server_push
|
||||
|
||||
# Use crane directly for push
|
||||
CRANE="bazel-bin/ci/push_admin_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/admin-server:${GIT_SHA}"
|
||||
echo "Pushing admin image: $IMAGE_TAG"
|
||||
$CRANE push "$ADMIN_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/admin-server:latest"
|
||||
|
||||
build-jfr-sidecar:
|
||||
runs-on: self-hosted
|
||||
outputs:
|
||||
image_tag: ${{ steps.push-jfr-sidecar.outputs.image_tag }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: false
|
||||
|
||||
- name: Build JFR Sidecar Docker image
|
||||
id: build-jfr-sidecar
|
||||
run: |
|
||||
set -ex
|
||||
|
||||
# Build JFR sidecar image (Go binary has explicit goos/goarch in BUILD.bazel)
|
||||
bazel build //ci:jfr_sidecar_image
|
||||
|
||||
# Save the resolved path before any other bazel command changes bazel-bin symlink
|
||||
IMAGE_PATH=$(readlink -f bazel-bin/ci/jfr_sidecar_image)
|
||||
echo "Image path: $IMAGE_PATH"
|
||||
echo "image_path=$IMAGE_PATH" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Login to DigitalOcean Container Registry
|
||||
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
|
||||
env:
|
||||
DO_TOKEN: ${{ secrets.DO_REGISTRY_TOKEN }}
|
||||
run: |
|
||||
mkdir -p ~/.docker
|
||||
AUTH=$(echo -n "${DO_TOKEN}:${DO_TOKEN}" | base64)
|
||||
echo "{\"auths\":{\"registry.digitalocean.com\":{\"auth\":\"${AUTH}\"}}}" > ~/.docker/config.json
|
||||
mkdir -p .docker
|
||||
cp ~/.docker/config.json .docker/
|
||||
|
||||
- name: Push JFR Sidecar image to DO registry
|
||||
id: push-jfr-sidecar
|
||||
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
|
||||
env:
|
||||
DOCKER_CONFIG: ${{ github.workspace }}/.docker
|
||||
run: |
|
||||
set -ex
|
||||
|
||||
JFR_IMAGE="${{ steps.build-jfr-sidecar.outputs.image_path }}"
|
||||
echo "Using JFR Sidecar image: $JFR_IMAGE"
|
||||
|
||||
if [ -z "$JFR_IMAGE" ] || [ ! -d "$JFR_IMAGE" ]; then
|
||||
echo "ERROR: JFR Sidecar image not found at: $JFR_IMAGE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build the push target to get crane in runfiles
|
||||
bazel build //ci:jfr_sidecar_push
|
||||
|
||||
# Use crane directly for push
|
||||
CRANE="bazel-bin/ci/push_jfr_sidecar_push.sh.runfiles/rules_oci~~oci~oci_crane_darwin_arm64/crane"
|
||||
echo "Using crane: $CRANE"
|
||||
|
||||
# Push with SHA tag
|
||||
GIT_SHA=$(git rev-parse --short=8 HEAD)
|
||||
IMAGE_TAG="registry.digitalocean.com/eagle0/jfr-sidecar:${GIT_SHA}"
|
||||
echo "Pushing JFR sidecar image: $IMAGE_TAG"
|
||||
$CRANE push "$JFR_IMAGE" "$IMAGE_TAG"
|
||||
|
||||
# Output the full image tag for deploy step
|
||||
echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT
|
||||
|
||||
# Also update :latest for convenience
|
||||
echo "Copying to :latest tag"
|
||||
$CRANE copy "$IMAGE_TAG" "registry.digitalocean.com/eagle0/jfr-sidecar:latest"
|
||||
echo "=== All images pushed successfully ==="
|
||||
|
||||
deploy:
|
||||
runs-on: self-hosted
|
||||
needs: [build-eagle, build-shardok, build-admin, build-jfr-sidecar]
|
||||
needs: [build-all]
|
||||
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
|
||||
environment: production
|
||||
env:
|
||||
EAGLE_IMAGE: ${{ needs.build-eagle.outputs.image_tag }}
|
||||
SHARDOK_IMAGE: ${{ needs.build-shardok.outputs.image_tag }}
|
||||
ADMIN_IMAGE: ${{ needs.build-admin.outputs.image_tag }}
|
||||
JFR_SIDECAR_IMAGE: ${{ needs.build-jfr-sidecar.outputs.image_tag }}
|
||||
EAGLE_IMAGE: ${{ needs.build-all.outputs.eagle_image_tag }}
|
||||
SHARDOK_IMAGE: ${{ needs.build-all.outputs.shardok_image_tag }}
|
||||
ADMIN_IMAGE: ${{ needs.build-all.outputs.admin_image_tag }}
|
||||
JFR_SIDECAR_IMAGE: ${{ needs.build-all.outputs.jfr_sidecar_image_tag }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
GPT_MODEL_NAME: ${{ secrets.GPT_MODEL_NAME }}
|
||||
EAGLE_ENABLE_S3: ${{ secrets.EAGLE_ENABLE_S3 }}
|
||||
@@ -443,6 +187,9 @@ jobs:
|
||||
SHARDOK_ADDRESS: ${{ secrets.SHARDOK_ADDRESS }}
|
||||
SHARDOK_AUTH_TOKEN: ${{ secrets.SHARDOK_AUTH_TOKEN }}
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
FASTMAIL_API_TOKEN: ${{ secrets.FASTMAIL_API_TOKEN }}
|
||||
FASTMAIL_FROM_EMAIL: ${{ secrets.FASTMAIL_FROM_EMAIL }}
|
||||
FASTMAIL_FROM_NAME: ${{ secrets.FASTMAIL_FROM_NAME }}
|
||||
DO_DROPLET_IP: ${{ secrets.DO_DROPLET_IP }}
|
||||
DO_REGISTRY_TOKEN: ${{ secrets.DO_REGISTRY_TOKEN }}
|
||||
steps:
|
||||
@@ -456,11 +203,27 @@ jobs:
|
||||
chmod 600 ~/.ssh/deploy_key
|
||||
ssh-keyscan -H "$DO_DROPLET_IP" >> ~/.ssh/known_hosts 2>/dev/null || true
|
||||
|
||||
- name: Build warmup tool
|
||||
run: |
|
||||
bazel build //src/main/go/net/eagle0/warmup:warmup_linux_amd64
|
||||
mkdir -p scripts/bin
|
||||
cp bazel-bin/src/main/go/net/eagle0/warmup/warmup_linux_amd64_/warmup_linux_amd64 scripts/bin/warmup
|
||||
|
||||
- name: Copy config files to droplet
|
||||
run: |
|
||||
scp -i ~/.ssh/deploy_key \
|
||||
docker-compose.prod.yml nginx/nginx.conf \
|
||||
deploy@"$DO_DROPLET_IP":/opt/eagle0/
|
||||
# Create directory structure on remote
|
||||
ssh -i ~/.ssh/deploy_key deploy@"$DO_DROPLET_IP" bash -s << 'SETUP_DIRS'
|
||||
set -e
|
||||
mkdir -p /opt/eagle0/scripts/bin /opt/eagle0/nginx /opt/eagle0/deploy
|
||||
rm -f /opt/eagle0/scripts/bin/warmup
|
||||
SETUP_DIRS
|
||||
|
||||
# Copy files
|
||||
scp -i ~/.ssh/deploy_key docker-compose.prod.yml deploy@"$DO_DROPLET_IP":/opt/eagle0/
|
||||
scp -i ~/.ssh/deploy_key nginx/nginx.conf deploy@"$DO_DROPLET_IP":/opt/eagle0/nginx/
|
||||
scp -i ~/.ssh/deploy_key scripts/deploy-blue-green.sh scripts/warmup-eagle.sh deploy@"$DO_DROPLET_IP":/opt/eagle0/scripts/
|
||||
scp -i ~/.ssh/deploy_key scripts/bin/warmup deploy@"$DO_DROPLET_IP":/opt/eagle0/scripts/bin/
|
||||
scp -i ~/.ssh/deploy_key deploy/env.template deploy/update-env.sh deploy@"$DO_DROPLET_IP":/opt/eagle0/deploy/
|
||||
|
||||
- name: Deploy to production droplet
|
||||
run: |
|
||||
@@ -486,113 +249,103 @@ jobs:
|
||||
SHARDOK_ADDRESS="${SHARDOK_ADDRESS}"
|
||||
SHARDOK_AUTH_TOKEN="${SHARDOK_AUTH_TOKEN}"
|
||||
SENTRY_DSN="${SENTRY_DSN}"
|
||||
FASTMAIL_API_TOKEN="${FASTMAIL_API_TOKEN}"
|
||||
FASTMAIL_FROM_EMAIL="${FASTMAIL_FROM_EMAIL}"
|
||||
FASTMAIL_FROM_NAME="${FASTMAIL_FROM_NAME}"
|
||||
DO_REGISTRY_TOKEN="${DO_REGISTRY_TOKEN}"
|
||||
|
||||
# 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)
|
||||
# Check Docker has IPv6 support
|
||||
if ! cat /etc/docker/daemon.json 2>/dev/null | grep -q '"ip6tables"'; then
|
||||
echo "WARNING: Docker IPv6 not configured. Eagle may not reach Hetzner Shardok."
|
||||
echo "Run: sudo tee /etc/docker/daemon.json <<< '{\"ipv6\": true, \"ip6tables\": true, \"experimental\": true, \"fixed-cidr-v6\": \"fd00::/80\"}' && sudo systemctl restart docker"
|
||||
fi
|
||||
|
||||
# Write env vars to .env file for docker-compose
|
||||
# Preserve AUTH_IMAGE if it exists (managed by auth_build.yml)
|
||||
EXISTING_AUTH_IMAGE=""
|
||||
if [ -f .env ]; then
|
||||
EXISTING_AUTH_IMAGE=\$(grep '^AUTH_IMAGE=' .env | cut -d= -f2- || true)
|
||||
fi
|
||||
|
||||
rm -f .env 2>/dev/null || true
|
||||
cat > .env << EOF
|
||||
EAGLE_IMAGE=\${EAGLE_IMAGE}
|
||||
SHARDOK_IMAGE=\${SHARDOK_IMAGE}
|
||||
ADMIN_IMAGE=\${ADMIN_IMAGE}
|
||||
JFR_SIDECAR_IMAGE=\${JFR_SIDECAR_IMAGE}
|
||||
AUTH_IMAGE=\${EXISTING_AUTH_IMAGE:-registry.digitalocean.com/eagle0/auth-server:latest}
|
||||
OPENAI_API_KEY=\${OPENAI_API_KEY:-}
|
||||
GPT_MODEL_NAME=\${GPT_MODEL_NAME:-gpt-4o}
|
||||
EAGLE_ENABLE_S3=\${EAGLE_ENABLE_S3:-false}
|
||||
DO_SPACES_ACCESS_KEY=\${DO_SPACES_ACCESS_KEY:-}
|
||||
DO_SPACES_SECRET_KEY=\${DO_SPACES_SECRET_KEY:-}
|
||||
JWT_PRIVATE_KEY=\${JWT_PRIVATE_KEY:-}
|
||||
DISCORD_CLIENT_ID=\${DISCORD_CLIENT_ID:-}
|
||||
DISCORD_CLIENT_SECRET=\${DISCORD_CLIENT_SECRET:-}
|
||||
GOOGLE_CLIENT_ID=\${GOOGLE_CLIENT_ID:-}
|
||||
GOOGLE_CLIENT_SECRET=\${GOOGLE_CLIENT_SECRET:-}
|
||||
SHARDOK_ADDRESS=\${SHARDOK_ADDRESS:-shardok:40042}
|
||||
SHARDOK_AUTH_TOKEN=\${SHARDOK_AUTH_TOKEN:-}
|
||||
SENTRY_DSN=\${SENTRY_DSN:-}
|
||||
EOF
|
||||
chmod 600 .env
|
||||
# Update env vars
|
||||
chmod +x deploy/update-env.sh
|
||||
cd deploy && ./update-env.sh \
|
||||
"EAGLE_IMAGE=\${EAGLE_IMAGE}" \
|
||||
"SHARDOK_IMAGE=\${SHARDOK_IMAGE}" \
|
||||
"ADMIN_IMAGE=\${ADMIN_IMAGE}" \
|
||||
"JFR_SIDECAR_IMAGE=\${JFR_SIDECAR_IMAGE}" \
|
||||
"OPENAI_API_KEY=\${OPENAI_API_KEY}" \
|
||||
"GPT_MODEL_NAME=\${GPT_MODEL_NAME:-gpt-4o}" \
|
||||
"EAGLE_ENABLE_S3=\${EAGLE_ENABLE_S3:-false}" \
|
||||
"DO_SPACES_ACCESS_KEY=\${DO_SPACES_ACCESS_KEY}" \
|
||||
"DO_SPACES_SECRET_KEY=\${DO_SPACES_SECRET_KEY}" \
|
||||
"JWT_PRIVATE_KEY=\${JWT_PRIVATE_KEY}" \
|
||||
"DISCORD_CLIENT_ID=\${DISCORD_CLIENT_ID}" \
|
||||
"DISCORD_CLIENT_SECRET=\${DISCORD_CLIENT_SECRET}" \
|
||||
"GOOGLE_CLIENT_ID=\${GOOGLE_CLIENT_ID}" \
|
||||
"GOOGLE_CLIENT_SECRET=\${GOOGLE_CLIENT_SECRET}" \
|
||||
"SHARDOK_ADDRESS=\${SHARDOK_ADDRESS:-shardok:40042}" \
|
||||
"SHARDOK_AUTH_TOKEN=\${SHARDOK_AUTH_TOKEN}" \
|
||||
"SENTRY_DSN=\${SENTRY_DSN}" \
|
||||
"FASTMAIL_API_TOKEN=\${FASTMAIL_API_TOKEN}" \
|
||||
"FASTMAIL_FROM_EMAIL=\${FASTMAIL_FROM_EMAIL}" \
|
||||
"FASTMAIL_FROM_NAME=\${FASTMAIL_FROM_NAME}"
|
||||
cd ..
|
||||
|
||||
# Login to registry
|
||||
echo "\$DO_REGISTRY_TOKEN" | docker login registry.digitalocean.com -u "\$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
|
||||
# This avoids digest mismatch from DO registry's OCI->Docker format conversion
|
||||
# Install crane for pulling OCI images
|
||||
echo "Installing crane..."
|
||||
rm -f 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
|
||||
|
||||
# crane uses Docker config for auth
|
||||
echo "Pulling Eagle image with crane..."
|
||||
./crane pull "\${EAGLE_IMAGE}" eagle.tar || { echo "ERROR: Failed to pull eagle image"; exit 1; }
|
||||
echo "Loading Eagle image into Docker..."
|
||||
docker load -i eagle.tar
|
||||
rm eagle.tar
|
||||
# Pull and load all images
|
||||
echo "Pulling Eagle image..."
|
||||
./crane pull "\${EAGLE_IMAGE}" eagle.tar && docker load -i eagle.tar && rm eagle.tar
|
||||
|
||||
echo "Pulling Shardok image with crane..."
|
||||
./crane pull "\${SHARDOK_IMAGE}" shardok.tar || { echo "ERROR: Failed to pull shardok image"; exit 1; }
|
||||
echo "Loading Shardok image into Docker..."
|
||||
docker load -i shardok.tar
|
||||
rm shardok.tar
|
||||
echo "Pulling Shardok image..."
|
||||
./crane pull "\${SHARDOK_IMAGE}" shardok.tar && docker load -i shardok.tar && rm shardok.tar
|
||||
|
||||
echo "Pulling Admin image with crane..."
|
||||
./crane pull "\${ADMIN_IMAGE}" admin.tar || { echo "ERROR: Failed to pull admin image"; exit 1; }
|
||||
echo "Loading Admin image into Docker..."
|
||||
docker load -i admin.tar
|
||||
rm admin.tar
|
||||
echo "Pulling Admin image..."
|
||||
./crane pull "\${ADMIN_IMAGE}" admin.tar && docker load -i admin.tar && rm admin.tar
|
||||
|
||||
echo "Pulling JFR Sidecar image with crane..."
|
||||
./crane pull "\${JFR_SIDECAR_IMAGE}" jfr-sidecar.tar || { echo "ERROR: Failed to pull jfr-sidecar image"; exit 1; }
|
||||
echo "Loading JFR Sidecar image into Docker..."
|
||||
docker load -i jfr-sidecar.tar
|
||||
rm jfr-sidecar.tar
|
||||
echo "Pulling JFR Sidecar image..."
|
||||
./crane pull "\${JFR_SIDECAR_IMAGE}" jfr-sidecar.tar && docker load -i jfr-sidecar.tar && rm jfr-sidecar.tar
|
||||
|
||||
rm ./crane
|
||||
|
||||
# Also pull other compose images
|
||||
# Pull other compose images
|
||||
docker pull nginx:alpine || true
|
||||
docker pull certbot/certbot || true
|
||||
|
||||
echo "All images pulled successfully"
|
||||
|
||||
# Recreate only the services we're updating (not auth - managed by auth_build.yml)
|
||||
# This prevents restarting auth service during every Eagle deploy
|
||||
docker compose -f docker-compose.prod.yml up -d --no-deps --force-recreate eagle shardok admin jfr-sidecar
|
||||
# Recreate non-Eagle services
|
||||
docker compose -f docker-compose.prod.yml up -d --no-deps --force-recreate shardok admin
|
||||
|
||||
# Ensure auth is running (but don't force-recreate it)
|
||||
# Deploy Eagle with blue-green
|
||||
if [ -x "/opt/eagle0/scripts/deploy-blue-green.sh" ]; then
|
||||
echo "Using blue-green deployment for Eagle..."
|
||||
chmod +x /opt/eagle0/scripts/*.sh
|
||||
[ -f "/opt/eagle0/scripts/bin/warmup" ] && chmod +x /opt/eagle0/scripts/bin/warmup
|
||||
GIT_SHA=\$(echo "\${EAGLE_IMAGE}" | sed 's/.*://')
|
||||
/opt/eagle0/scripts/deploy-blue-green.sh "\${GIT_SHA}" || {
|
||||
echo "Blue-green failed, falling back to direct restart"
|
||||
docker compose -f docker-compose.prod.yml up -d --no-deps --force-recreate eagle-blue
|
||||
}
|
||||
else
|
||||
docker compose -f docker-compose.prod.yml up -d --no-deps --force-recreate eagle-blue
|
||||
fi
|
||||
|
||||
# Start jfr-sidecar after eagle-blue
|
||||
docker compose -f docker-compose.prod.yml up -d --no-deps --force-recreate jfr-sidecar
|
||||
|
||||
# Ensure auth is running
|
||||
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)
|
||||
# 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
|
||||
# Verify
|
||||
sleep 10
|
||||
docker compose -f docker-compose.prod.yml ps
|
||||
|
||||
# Verify containers are using correct images
|
||||
echo "=== Verifying container image tags ==="
|
||||
docker compose -f docker-compose.prod.yml images
|
||||
|
||||
# Cleanup old images
|
||||
# Cleanup
|
||||
docker container prune -f
|
||||
docker image prune -f
|
||||
DEPLOY_SCRIPT
|
||||
|
||||
@@ -10,6 +10,7 @@ on:
|
||||
paths:
|
||||
- ".github/workflows/installer_build.yml"
|
||||
- "src/main/csharp/net/eagle0/clients/win/installer/**"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -29,6 +30,19 @@ jobs:
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
- name: Inject manifest public key
|
||||
env:
|
||||
MANIFEST_PUBLIC_KEY: ${{ secrets.MANIFEST_PUBLIC_KEY }}
|
||||
run: |
|
||||
if [ -n "$MANIFEST_PUBLIC_KEY" ]; then
|
||||
CONFIG_FILE="src/main/csharp/net/eagle0/clients/win/installer/EagleInstaller/configuration.txt"
|
||||
echo "manifest_public_key = $MANIFEST_PUBLIC_KEY" >> "$CONFIG_FILE"
|
||||
echo "Injected manifest public key into configuration.txt"
|
||||
cat "$CONFIG_FILE"
|
||||
else
|
||||
echo "MANIFEST_PUBLIC_KEY not set, skipping injection"
|
||||
fi
|
||||
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore src/main/csharp/net/eagle0/clients/win/installer/EagleInstaller/EagleInstaller.csproj
|
||||
|
||||
@@ -68,15 +82,30 @@ jobs:
|
||||
env:
|
||||
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
MANIFEST_SIGNING_KEY: ${{ secrets.MANIFEST_SIGNING_KEY }}
|
||||
run: |
|
||||
# Create installer manifest content
|
||||
# Create installer manifest content
|
||||
INSTALLER_SHA=$(sha256sum ./installer-output/EagleInstaller.exe | cut -d' ' -f1)
|
||||
echo "installer_version=$INSTALLER_SHA" > /tmp/installer_manifest.txt
|
||||
echo "installer_url=installer/EagleInstaller.exe" >> /tmp/installer_manifest.txt
|
||||
|
||||
|
||||
echo "=== Installer manifest content ==="
|
||||
cat /tmp/installer_manifest.txt
|
||||
echo "=================================="
|
||||
|
||||
# Update the unified manifest
|
||||
bazel run //src/main/go/net/eagle0/build/manifest_manager:manifest_manager -- installer /tmp/installer_manifest.txt
|
||||
|
||||
# Write signing key to temp file (if available)
|
||||
SIGNING_ARGS=""
|
||||
if [ -n "$MANIFEST_SIGNING_KEY" ]; then
|
||||
echo "$MANIFEST_SIGNING_KEY" > /tmp/manifest_signing_key
|
||||
chmod 600 /tmp/manifest_signing_key
|
||||
SIGNING_ARGS="/tmp/manifest_signing_key"
|
||||
echo "Manifest signing key available"
|
||||
else
|
||||
echo "Warning: MANIFEST_SIGNING_KEY not set, manifest will be unsigned"
|
||||
fi
|
||||
|
||||
# Update the unified manifest (with optional signing)
|
||||
bazel run //src/main/go/net/eagle0/build/manifest_manager:manifest_manager -- installer /tmp/installer_manifest.txt $SIGNING_ARGS
|
||||
|
||||
# Cleanup
|
||||
rm -f /tmp/manifest_signing_key
|
||||
@@ -0,0 +1,154 @@
|
||||
name: Mac Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
paths:
|
||||
- ".github/workflows/mac_build.yml"
|
||||
- "src/main/csharp/net/eagle0/clients/unity/**"
|
||||
- "src/main/proto/**"
|
||||
- "src/main/go/net/eagle0/build/mac_build_handler/**"
|
||||
- "scripts/build_protos.sh"
|
||||
- "scripts/build_mac_plugin.sh"
|
||||
- "scripts/inject_sparkle.sh"
|
||||
- "scripts/codesign_mac_app.sh"
|
||||
- "scripts/notarize_mac_app.sh"
|
||||
- "ci/github_actions/build_mac.sh"
|
||||
- "ci/github_actions/build_unity_mac.sh"
|
||||
- "ci/mac/**"
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/mac_build.yml"
|
||||
- "src/main/csharp/net/eagle0/clients/unity/**"
|
||||
- "src/main/go/net/eagle0/build/mac_build_handler/**"
|
||||
- "scripts/build_mac_plugin.sh"
|
||||
- "scripts/inject_sparkle.sh"
|
||||
- "scripts/codesign_mac_app.sh"
|
||||
- "scripts/notarize_mac_app.sh"
|
||||
- "ci/github_actions/build_mac.sh"
|
||||
- "ci/github_actions/build_unity_mac.sh"
|
||||
- "ci/mac/**"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
skip_signing:
|
||||
description: 'Skip code signing, notarization, and deploy (build only)'
|
||||
required: false
|
||||
default: 'false'
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
mac-unity:
|
||||
runs-on: self-hosted
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: true
|
||||
clean: false
|
||||
fetch-depth: 0 # For version numbering from git history
|
||||
|
||||
- name: Pull LFS files
|
||||
run: git lfs pull
|
||||
|
||||
- name: Restore Library/
|
||||
run: ./ci/github_actions/restore_library.sh
|
||||
|
||||
- name: Build Mac Unity
|
||||
run: ./ci/github_actions/build_unity_mac.sh "/tmp/eagle0/eagle0MAC"
|
||||
|
||||
- name: Persist Library/
|
||||
run: ./ci/github_actions/persist_library.sh
|
||||
|
||||
- name: Inject Sparkle Framework
|
||||
if: success()
|
||||
env:
|
||||
SPARKLE_EDDSA_PUBLIC_KEY: ${{ secrets.SPARKLE_EDDSA_PUBLIC_KEY }}
|
||||
run: |
|
||||
chmod +x ./scripts/inject_sparkle.sh
|
||||
./scripts/inject_sparkle.sh "/tmp/eagle0/eagle0MAC/eagle0.app"
|
||||
|
||||
- name: Import Code Signing Certificate
|
||||
if: success() && ((github.event_name == 'workflow_dispatch' && github.event.inputs.skip_signing != 'true') || (github.event_name == 'push' && github.ref == 'refs/heads/main'))
|
||||
env:
|
||||
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
|
||||
MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }}
|
||||
run: |
|
||||
# Generate random keychain password (only used within this workflow run)
|
||||
KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
|
||||
echo "KEYCHAIN_PASSWORD=$KEYCHAIN_PASSWORD" >> $GITHUB_ENV
|
||||
|
||||
# Decode certificate
|
||||
echo "$MACOS_CERTIFICATE" | base64 --decode > certificate.p12
|
||||
|
||||
# Delete any existing keychain from previous runs
|
||||
security delete-keychain build.keychain 2>/dev/null || true
|
||||
|
||||
# Create temporary keychain
|
||||
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
|
||||
security default-keychain -s build.keychain
|
||||
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
|
||||
|
||||
# Import certificate
|
||||
security import certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign
|
||||
|
||||
# Allow codesign to access keychain
|
||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
|
||||
|
||||
# Clean up
|
||||
rm certificate.p12
|
||||
|
||||
- name: Code Sign App
|
||||
if: success() && ((github.event_name == 'workflow_dispatch' && github.event.inputs.skip_signing != 'true') || (github.event_name == 'push' && github.ref == 'refs/heads/main'))
|
||||
env:
|
||||
SIGNING_IDENTITY: ${{ secrets.SIGNING_IDENTITY }}
|
||||
run: |
|
||||
chmod +x ./scripts/codesign_mac_app.sh
|
||||
./scripts/codesign_mac_app.sh "/tmp/eagle0/eagle0MAC/eagle0.app" "ci/mac/eagle0.entitlements"
|
||||
|
||||
- name: Notarize App
|
||||
if: success() && ((github.event_name == 'workflow_dispatch' && github.event.inputs.skip_signing != 'true') || (github.event_name == 'push' && github.ref == 'refs/heads/main'))
|
||||
env:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APP_SPECIFIC_PASSWORD: ${{ secrets.APP_SPECIFIC_PASSWORD }}
|
||||
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
run: |
|
||||
chmod +x ./scripts/notarize_mac_app.sh
|
||||
./scripts/notarize_mac_app.sh "/tmp/eagle0/eagle0MAC/eagle0.app"
|
||||
|
||||
- name: Deploy Mac Build
|
||||
if: success() && ((github.event_name == 'workflow_dispatch' && github.event.inputs.skip_signing != 'true') || (github.event_name == 'push' && github.ref == 'refs/heads/main'))
|
||||
env:
|
||||
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
SPARKLE_EDDSA_PRIVATE_KEY: ${{ secrets.SPARKLE_EDDSA_PRIVATE_KEY }}
|
||||
run: |
|
||||
# Write private key to temp file for signing
|
||||
SPARKLE_PRIVATE_KEY_PATH="/tmp/sparkle_private_key"
|
||||
echo "$SPARKLE_EDDSA_PRIVATE_KEY" > "$SPARKLE_PRIVATE_KEY_PATH"
|
||||
chmod 600 "$SPARKLE_PRIVATE_KEY_PATH"
|
||||
|
||||
VERSION=$(git describe --tags --always)
|
||||
BUILD_NUMBER=$(git rev-list --count HEAD)
|
||||
|
||||
bazel run //src/main/go/net/eagle0/build/mac_build_handler:mac_build_handler -- \
|
||||
"/tmp/eagle0/eagle0MAC/eagle0.app" \
|
||||
"$VERSION" \
|
||||
"$BUILD_NUMBER" \
|
||||
"$SPARKLE_PRIVATE_KEY_PATH"
|
||||
|
||||
rm "$SPARKLE_PRIVATE_KEY_PATH"
|
||||
|
||||
- name: Cleanup Keychain
|
||||
if: always()
|
||||
run: |
|
||||
security delete-keychain build.keychain 2>/dev/null || true
|
||||
|
||||
- name: Archive Build Log
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: editor_mac.log
|
||||
path: /tmp/eagle0/editor_mac.log
|
||||
@@ -1,29 +0,0 @@
|
||||
name: Mac History Editor Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
paths:
|
||||
- ".github/workflows/mac_history_build.yml"
|
||||
- "src/main/swift/net/eagle0/EagleGameHistoryViewer/**"
|
||||
- "src/main/protobuf/net/eagle0/eagle/**"
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/mac_history_build.yml"
|
||||
- "src/main/swift/net/eagle0/EagleGameHistoryViewer/**"
|
||||
- "src/main/protobuf/net/eagle0/eagle/**"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
mac-history-build:
|
||||
runs-on: self-hosted
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: false
|
||||
clean: false
|
||||
- name: Build the mac history
|
||||
run: ./ci/github_actions/build_mac_history.sh
|
||||
@@ -63,7 +63,24 @@ jobs:
|
||||
env:
|
||||
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
run: bazel run //src/main/go/net/eagle0/build/manifest_manager:manifest_manager -- unity3d /tmp/unity_manifest.txt
|
||||
MANIFEST_SIGNING_KEY: ${{ secrets.MANIFEST_SIGNING_KEY }}
|
||||
run: |
|
||||
# Write signing key to temp file (if available)
|
||||
SIGNING_ARGS=""
|
||||
if [ -n "$MANIFEST_SIGNING_KEY" ]; then
|
||||
echo "$MANIFEST_SIGNING_KEY" > /tmp/manifest_signing_key
|
||||
chmod 600 /tmp/manifest_signing_key
|
||||
SIGNING_ARGS="/tmp/manifest_signing_key"
|
||||
echo "Manifest signing key available"
|
||||
else
|
||||
echo "Warning: MANIFEST_SIGNING_KEY not set, manifest will be unsigned"
|
||||
fi
|
||||
|
||||
# Update the unified manifest (with optional signing)
|
||||
bazel run //src/main/go/net/eagle0/build/manifest_manager:manifest_manager -- unity3d /tmp/unity_manifest.txt $SIGNING_ARGS
|
||||
|
||||
# Cleanup
|
||||
rm -f /tmp/manifest_signing_key
|
||||
- name: Archive build log
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -38,3 +38,4 @@ scripts/refresh_name_layers/refresh_name_layers.zip
|
||||
api_keys.txt
|
||||
|
||||
src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/Packages/com.unity.dedicated-server/
|
||||
node_modules/
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
# CLAUDE.md
|
||||
|
||||
## CRITICAL GIT RULES (NEVER VIOLATE)
|
||||
|
||||
**NEVER push directly to main/master.** No exceptions. Not for "small changes." Not for docs. Not ever.
|
||||
|
||||
**NEVER merge PRs.** You create PRs. The user merges them. No exceptions.
|
||||
|
||||
**ALWAYS use this workflow:**
|
||||
1. Create a feature branch from origin/main
|
||||
2. Commit to that branch
|
||||
3. Create a PR with `gh pr create`
|
||||
4. Wait for user to merge (DO NOT run `gh pr merge`)
|
||||
|
||||
If you catch yourself about to run `git push origin main` or `git push origin <branch>:main`, STOP. You are about to violate a critical rule. Create a PR instead.
|
||||
|
||||
If you catch yourself about to run `gh pr merge`, STOP. Only the user merges PRs.
|
||||
|
||||
---
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
+19
-12
@@ -13,8 +13,8 @@ AWS_SDK_VERSION = "2.28.1"
|
||||
# Core Build Tools
|
||||
#
|
||||
|
||||
bazel_dep(name = "bazel_skylib", version = "1.8.1")
|
||||
bazel_dep(name = "rules_pkg", version = "1.1.0")
|
||||
bazel_dep(name = "bazel_skylib", version = "1.9.0")
|
||||
bazel_dep(name = "rules_pkg", version = "1.2.0")
|
||||
|
||||
#
|
||||
# Language Support - Scala
|
||||
@@ -102,8 +102,8 @@ sysroot(
|
||||
# Language Support - Go
|
||||
#
|
||||
|
||||
bazel_dep(name = "rules_go", version = "0.56.1", repo_name = "io_bazel_rules_go")
|
||||
bazel_dep(name = "gazelle", version = "0.45.0", repo_name = "bazel_gazelle")
|
||||
bazel_dep(name = "rules_go", version = "0.59.0", repo_name = "io_bazel_rules_go")
|
||||
bazel_dep(name = "gazelle", version = "0.47.0", repo_name = "bazel_gazelle")
|
||||
|
||||
go_sdk = use_extension("@io_bazel_rules_go//go:extensions.bzl", "go_sdk")
|
||||
go_sdk.download(version = "1.23.3")
|
||||
@@ -127,8 +127,8 @@ use_repo(
|
||||
#
|
||||
|
||||
bazel_dep(name = "apple_support", version = "1.21.1", repo_name = "build_bazel_apple_support")
|
||||
bazel_dep(name = "rules_apple", version = "3.16.1", repo_name = "build_bazel_rules_apple")
|
||||
bazel_dep(name = "rules_swift", version = "2.3.1", repo_name = "build_bazel_rules_swift")
|
||||
bazel_dep(name = "rules_apple", version = "4.3.3", repo_name = "build_bazel_rules_apple")
|
||||
bazel_dep(name = "rules_swift", version = "2.4.0", repo_name = "build_bazel_rules_swift")
|
||||
|
||||
#
|
||||
# Protocol Buffers & RPC
|
||||
@@ -149,8 +149,8 @@ bazel_dep(name = "googletest", version = "1.17.0")
|
||||
# Container Images (OCI)
|
||||
#
|
||||
|
||||
bazel_dep(name = "rules_oci", version = "2.2.6")
|
||||
bazel_dep(name = "aspect_bazel_lib", version = "2.16.0")
|
||||
bazel_dep(name = "rules_oci", version = "2.2.7")
|
||||
bazel_dep(name = "aspect_bazel_lib", version = "2.22.4")
|
||||
|
||||
oci = use_extension("@rules_oci//oci:extensions.bzl", "oci")
|
||||
|
||||
@@ -186,7 +186,7 @@ use_repo(oci, "alpine_linux", "alpine_linux_linux_amd64", "eclipse_temurin_17",
|
||||
# Java/Scala Dependencies
|
||||
#
|
||||
|
||||
bazel_dep(name = "rules_jvm_external", version = "6.3")
|
||||
bazel_dep(name = "rules_jvm_external", version = "6.9")
|
||||
|
||||
maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")
|
||||
maven.install(
|
||||
@@ -294,11 +294,15 @@ http_archive(
|
||||
)
|
||||
|
||||
# Busybox static binary for Docker health checks (provides nc, wget, etc.)
|
||||
# https://busybox.net/downloads/binaries/
|
||||
# Primary: DigitalOcean Spaces (public, reliable)
|
||||
# Fallback: busybox.net (can be unreliable/slow)
|
||||
http_file(
|
||||
name = "busybox_x86_64",
|
||||
sha256 = "6e123e7f3202a8c1e9b1f94d8941580a25135382b99e8d3e34fb858bba311348",
|
||||
urls = ["https://busybox.net/downloads/binaries/1.35.0-x86_64-linux-musl/busybox"],
|
||||
urls = [
|
||||
"https://eagle0-sysroot.sfo3.digitaloceanspaces.com/busybox/busybox-1.35.0-x86_64-linux-musl",
|
||||
"https://busybox.net/downloads/binaries/1.35.0-x86_64-linux-musl/busybox",
|
||||
],
|
||||
downloaded_file_path = "busybox",
|
||||
executable = True,
|
||||
)
|
||||
@@ -306,7 +310,10 @@ http_file(
|
||||
http_file(
|
||||
name = "busybox_aarch64",
|
||||
sha256 = "141adb1b625a6f44c4b114f76b4387b4ea4f7ab802b88eb40e0d2f6adcccb1c3",
|
||||
urls = ["https://busybox.net/downloads/binaries/1.35.0-aarch64-linux-musl/busybox"],
|
||||
urls = [
|
||||
# TODO: Upload aarch64 binary to GitHub release when needed
|
||||
"https://busybox.net/downloads/binaries/1.35.0-aarch64-linux-musl/busybox",
|
||||
],
|
||||
downloaded_file_path = "busybox",
|
||||
executable = True,
|
||||
)
|
||||
|
||||
Generated
+79
-22
@@ -27,9 +27,9 @@
|
||||
"https://bcr.bazel.build/modules/aspect_bazel_lib/1.40.3/MODULE.bazel": "668e6bcb4d957fc0e284316dba546b705c8d43c857f87119619ee83c4555b859",
|
||||
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.11.0/MODULE.bazel": "cb1ba9f9999ed0bc08600c221f532c1ddd8d217686b32ba7d45b0713b5131452",
|
||||
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.14.0/MODULE.bazel": "2b31ffcc9bdc8295b2167e07a757dbbc9ac8906e7028e5170a3708cecaac119f",
|
||||
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.16.0/MODULE.bazel": "852f9ebbda017572a7c113a2434592dd3b2f55cd9a0faea3d4be5a09a59e4900",
|
||||
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.19.3/MODULE.bazel": "253d739ba126f62a5767d832765b12b59e9f8d2bc88cc1572f4a73e46eb298ca",
|
||||
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.19.3/source.json": "ffab9254c65ba945f8369297ad97ca0dec213d3adc6e07877e23a48624a8b456",
|
||||
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.22.4/MODULE.bazel": "a05cbd9bc16712a58dc27ffe0dceaefd0da59a9bd87a227379b2a934b26a39ab",
|
||||
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.22.4/source.json": "9780bc57f521968ee82b7c3e85b7d0c71518fb7ce83ed7a9e5077ce20923207b",
|
||||
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.7.2/MODULE.bazel": "780d1a6522b28f5edb7ea09630748720721dfe27690d65a2d33aa7509de77e07",
|
||||
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.7.7/MODULE.bazel": "491f8681205e31bb57892d67442ce448cda4f472a8e6b3dc062865e29a64f89c",
|
||||
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.8.1/MODULE.bazel": "812d2dd42f65dca362152101fbec418029cc8fd34cbad1a2fde905383d705838",
|
||||
@@ -57,12 +57,15 @@
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.27.0/MODULE.bazel": "621eeee06c4458a9121d1f104efb80f39d34deff4984e778359c60eaf1a8cb65",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.28.0/MODULE.bazel": "4b4200e6cbf8fa335b2c3f43e1d6ef3e240319c33d43d60cc0fbd4b87ece299d",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.3.0/MODULE.bazel": "cdcafe83ec318cda34e02948e81d790aab8df7a929cec6f6969f13a489ccecd9",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.30.0/MODULE.bazel": "a14b62d05969a293b80257e72e597c2da7f717e1e69fa8b339703ed6731bec87",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.34.0/MODULE.bazel": "e8475ad7c8965542e0c7aac8af68eb48c4af904be3d614b6aa6274c092c2ea1e",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.38.0/MODULE.bazel": "f9b8a9c890ebd216b4049fd12a31d3c2602e3403c7af636b04fbbd7453edc9c9",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.38.0/source.json": "31ba776c122b54a2885e23651642e32f087a87bf025465f8040751894b571277",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.4.1/MODULE.bazel": "e45b6bb2350aff3e442ae1111c555e27eac1d915e77775f6fdc4b351b758b5d7",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.9.0/MODULE.bazel": "885151d58d90d8d9c811eb75e3288c11f850e1d6b481a8c9f766adee4712358b",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.9.1/MODULE.bazel": "8f679097876a9b609ad1f60249c49d68bfab783dd9be012faf9d82547b14815a",
|
||||
"https://bcr.bazel.build/modules/bazel_lib/3.0.0/MODULE.bazel": "22b70b80ac89ad3f3772526cd9feee2fa412c2b01933fea7ed13238a448d370d",
|
||||
"https://bcr.bazel.build/modules/bazel_lib/3.0.0/source.json": "895f21909c6fba01d7c17914bb6c8e135982275a1b18cdaa4e62272217ef1751",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.0.3/MODULE.bazel": "bcb0fd896384802d1ad283b4e4eb4d718eebd8cb820b0a2c3a347fb971afd9d8",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.1.1/MODULE.bazel": "1add3e7d93ff2e6998f9e118022c84d163917d912f5afafb3058e3d2f1545b5e",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.2.0/MODULE.bazel": "44fe84260e454ed94ad326352a698422dbe372b21a1ac9f3eab76eb531223686",
|
||||
@@ -77,7 +80,8 @@
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.7.1/MODULE.bazel": "3120d80c5861aa616222ec015332e5f8d3171e062e3e804a2a0253e1be26e59b",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.8.1/MODULE.bazel": "88ade7293becda963e0e3ea33e7d54d3425127e0a326e0d17da085a5f1f03ff6",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.8.2/MODULE.bazel": "69ad6927098316848b34a9142bcc975e018ba27f08c4ff403f50c1b6e646ca67",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.8.2/source.json": "34a3c8bcf233b835eb74be9d628899bb32999d3e0eadef1947a0a562a2b16ffb",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.9.0/MODULE.bazel": "72997b29dfd95c3fa0d0c48322d05590418edef451f8db8db5509c57875fb4b7",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.9.0/source.json": "7ad77c1e8c1b84222d9b3f3cae016a76639435744c19330b0b37c0a3c9da7dc0",
|
||||
"https://bcr.bazel.build/modules/bazel_worker_api/0.0.6/MODULE.bazel": "fd1f9432ca04c947e91b500df69ce7c5b6dbfe1bc45ab1820338205dae3383a6",
|
||||
"https://bcr.bazel.build/modules/bazel_worker_api/0.0.6/source.json": "5d68545f224904745a3cabd35aea6bc2b6cc5a78b7f49f3f69660eab2eeeb273",
|
||||
"https://bcr.bazel.build/modules/boringssl/0.0.0-20211025-d4f1ab9/MODULE.bazel": "6ee6353f8b1a701fe2178e1d925034294971350b6d3ac37e67e5a7d463267834",
|
||||
@@ -115,8 +119,8 @@
|
||||
"https://bcr.bazel.build/modules/gazelle/0.34.0/MODULE.bazel": "abdd8ce4d70978933209db92e436deb3a8b737859e9354fb5fd11fb5c2004c8a",
|
||||
"https://bcr.bazel.build/modules/gazelle/0.36.0/MODULE.bazel": "e375d5d6e9a6ca59b0cb38b0540bc9a05b6aa926d322f2de268ad267a2ee74c0",
|
||||
"https://bcr.bazel.build/modules/gazelle/0.37.0/MODULE.bazel": "d1327ba0907d0275ed5103bfbbb13518f6c04955b402213319d0d6c0ce9839d4",
|
||||
"https://bcr.bazel.build/modules/gazelle/0.45.0/MODULE.bazel": "ecd19ebe9f8e024e1ccffb6d997cc893a974bcc581f1ae08f386bdd448b10687",
|
||||
"https://bcr.bazel.build/modules/gazelle/0.45.0/source.json": "111d182facc5f5e80f0b823d5f077b74128f40c3fd2eccc89a06f34191bd3392",
|
||||
"https://bcr.bazel.build/modules/gazelle/0.47.0/MODULE.bazel": "b61bb007c4efad134aa30ee7f4a8e2a39b22aa5685f005edaa022fbd1de43ebc",
|
||||
"https://bcr.bazel.build/modules/gazelle/0.47.0/source.json": "aeb2e5df14b7fb298625d75d08b9c65bdb0b56014c5eb89da9e5dd0572280ae6",
|
||||
"https://bcr.bazel.build/modules/google_benchmark/1.8.2/MODULE.bazel": "a70cf1bba851000ba93b58ae2f6d76490a9feb74192e57ab8e8ff13c34ec50cb",
|
||||
"https://bcr.bazel.build/modules/google_benchmark/1.8.4/MODULE.bazel": "c6d54a11dcf64ee63545f42561eda3fd94c1b5f5ebe1357011de63ae33739d5e",
|
||||
"https://bcr.bazel.build/modules/google_benchmark/1.8.5/MODULE.bazel": "9ba9b31b984022828a950e3300410977eda2e35df35584c6b0b2d0c2e52766b7",
|
||||
@@ -176,6 +180,7 @@
|
||||
"https://bcr.bazel.build/modules/opentracing-cpp/1.6.0/MODULE.bazel": "b3925269f63561b8b880ae7cf62ccf81f6ece55b62cd791eda9925147ae116ec",
|
||||
"https://bcr.bazel.build/modules/opentracing-cpp/1.6.0/source.json": "da1cb1add160f5e5074b7272e9db6fd8f1b3336c15032cd0a653af9d2f484aed",
|
||||
"https://bcr.bazel.build/modules/package_metadata/0.0.2/MODULE.bazel": "fb8d25550742674d63d7b250063d4580ca530499f045d70748b1b142081ebb92",
|
||||
"https://bcr.bazel.build/modules/package_metadata/0.0.3/MODULE.bazel": "77890552ecea9e284b5424c9de827a58099348763a4359e975c359a83d4faa83",
|
||||
"https://bcr.bazel.build/modules/package_metadata/0.0.5/MODULE.bazel": "ef4f9439e3270fdd6b9fd4dbc3d2f29d13888e44c529a1b243f7a31dfbc2e8e4",
|
||||
"https://bcr.bazel.build/modules/package_metadata/0.0.5/source.json": "2326db2f6592578177751c3e1f74786b79382cd6008834c9d01ec865b9126a85",
|
||||
"https://bcr.bazel.build/modules/platforms/0.0.10/MODULE.bazel": "8cb8efaf200bdeb2150d93e162c40f388529a25852b332cec879373771e48ed5",
|
||||
@@ -229,9 +234,9 @@
|
||||
"https://bcr.bazel.build/modules/rules_android/0.1.1/MODULE.bazel": "48809ab0091b07ad0182defb787c4c5328bd3a278938415c00a7b69b50c4d3a8",
|
||||
"https://bcr.bazel.build/modules/rules_android/0.1.1/source.json": "e6986b41626ee10bdc864937ffb6d6bf275bb5b9c65120e6137d56e6331f089e",
|
||||
"https://bcr.bazel.build/modules/rules_apple/3.16.0/MODULE.bazel": "0d1caf0b8375942ce98ea944be754a18874041e4e0459401d925577624d3a54a",
|
||||
"https://bcr.bazel.build/modules/rules_apple/3.16.1/MODULE.bazel": "8294474defa70af2534a558ab905c083d69203344145e6f7d544d5098611ec7d",
|
||||
"https://bcr.bazel.build/modules/rules_apple/3.16.1/source.json": "9190fd9d34a5d048bfbba8a530a57f2c2bf3f61e5634a9ab0b6ab005458857f9",
|
||||
"https://bcr.bazel.build/modules/rules_apple/3.5.1/MODULE.bazel": "3d1bbf65ad3692003d36d8a29eff54d4e5c1c5f4bfb60f79e28646a924d9101c",
|
||||
"https://bcr.bazel.build/modules/rules_apple/4.3.3/MODULE.bazel": "c5c2c4adeeac5f3f2f9b7f16abfa8be7ffefa596171d0d92bed4cae9ade0a498",
|
||||
"https://bcr.bazel.build/modules/rules_apple/4.3.3/source.json": "3cb1d69c8243ffcc42ecbf84ae8b9cccd7b1e2f091b0aee5a3e9c9a45267f312",
|
||||
"https://bcr.bazel.build/modules/rules_buf/0.1.1/MODULE.bazel": "6189aec18a4f7caff599ad41b851ab7645d4f1e114aa6431acf9b0666eb92162",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.0.1/MODULE.bazel": "cb2aa0747f84c6c3a78dad4e2049c154f08ab9d166b1273835a8174940365647",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.0.10/MODULE.bazel": "ec1705118f7eaedd6e118508d3d26deba2a4e76476ada7e0e3965211be012002",
|
||||
@@ -246,6 +251,8 @@
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.0.8/MODULE.bazel": "964c85c82cfeb6f3855e6a07054fdb159aced38e99a5eecf7bce9d53990afa3e",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.0.9/MODULE.bazel": "836e76439f354b89afe6a911a7adf59a6b2518fafb174483ad78a2a2fde7b1c5",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.1.1/MODULE.bazel": "2f0222a6f229f0bf44cd711dc13c858dad98c62d52bd51d8fc3a764a83125513",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.1.2/MODULE.bazel": "557ddc3a96858ec0d465a87c0a931054d7dcfd6583af2c7ed3baf494407fd8d0",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.1.5/MODULE.bazel": "88dfc9361e8b5ae1008ac38f7cdfd45ad738e4fa676a3ad67d19204f045a1fd8",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.2.14/MODULE.bazel": "353c99ed148887ee89c54a17d4100ae7e7e436593d104b668476019023b58df8",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.2.14/source.json": "55d0a4587c5592fad350f6e698530f4faf0e7dd15e69d43f8d87e220c78bea54",
|
||||
"https://bcr.bazel.build/modules/rules_foreign_cc/0.10.1/MODULE.bazel": "b9527010e5fef060af92b6724edb3691970a5b1f76f74b21d39f7d433641be60",
|
||||
@@ -263,8 +270,8 @@
|
||||
"https://bcr.bazel.build/modules/rules_go/0.48.0/MODULE.bazel": "d00ebcae0908ee3f5e6d53f68677a303d6d59a77beef879598700049c3980a03",
|
||||
"https://bcr.bazel.build/modules/rules_go/0.50.1/MODULE.bazel": "b91a308dc5782bb0a8021ad4330c81fea5bda77f96b9e4c117b9b9c8f6665ee0",
|
||||
"https://bcr.bazel.build/modules/rules_go/0.53.0/MODULE.bazel": "a4ed760d3ac0dbc0d7b967631a9a3fd9100d28f7d9fcf214b4df87d4bfff5f9a",
|
||||
"https://bcr.bazel.build/modules/rules_go/0.56.1/MODULE.bazel": "d5b835c548ac917345f1780cd2da52edc1130a908fe091c92096895303ae78a0",
|
||||
"https://bcr.bazel.build/modules/rules_go/0.56.1/source.json": "0c902f7272e8d4e47e459af97be472bc19dadbbe6023a0719d1adce8483ac75a",
|
||||
"https://bcr.bazel.build/modules/rules_go/0.59.0/MODULE.bazel": "b7e43e7414a3139a7547d1b4909b29085fbe5182b6c58cbe1ed4c6272815aeae",
|
||||
"https://bcr.bazel.build/modules/rules_go/0.59.0/source.json": "1df17bb7865cfc029492c30163cee891d0dd8658ea0d5bfdf252c4b6db5c1ef6",
|
||||
"https://bcr.bazel.build/modules/rules_java/4.0.0/MODULE.bazel": "5a78a7ae82cd1a33cef56dc578c7d2a46ed0dca12643ee45edbb8417899e6f74",
|
||||
"https://bcr.bazel.build/modules/rules_java/5.1.0/MODULE.bazel": "324b6478b0343a3ce7a9add8586ad75d24076d6d43d2f622990b9c1cfd8a1b15",
|
||||
"https://bcr.bazel.build/modules/rules_java/5.3.5/MODULE.bazel": "a4ec4f2db570171e3e5eb753276ee4b389bae16b96207e9d3230895c99644b86",
|
||||
@@ -292,7 +299,8 @@
|
||||
"https://bcr.bazel.build/modules/rules_jvm_external/6.0/MODULE.bazel": "37c93a5a78d32e895d52f86a8d0416176e915daabd029ccb5594db422e87c495",
|
||||
"https://bcr.bazel.build/modules/rules_jvm_external/6.1/MODULE.bazel": "75b5fec090dbd46cf9b7d8ea08cf84a0472d92ba3585b476f44c326eda8059c4",
|
||||
"https://bcr.bazel.build/modules/rules_jvm_external/6.3/MODULE.bazel": "c998e060b85f71e00de5ec552019347c8bca255062c990ac02d051bb80a38df0",
|
||||
"https://bcr.bazel.build/modules/rules_jvm_external/6.3/source.json": "6f5f5a5a4419ae4e37c35a5bb0a6ae657ed40b7abc5a5189111b47fcebe43197",
|
||||
"https://bcr.bazel.build/modules/rules_jvm_external/6.9/MODULE.bazel": "07c5db05527db7744a54fcffd653e1550d40e0540207a7f7e6d0a4de5bef8274",
|
||||
"https://bcr.bazel.build/modules/rules_jvm_external/6.9/source.json": "b12970214f3cc144b26610caeb101fa622d910f1ab3d98f0bae1058edbd00bd4",
|
||||
"https://bcr.bazel.build/modules/rules_kotlin/1.9.0/MODULE.bazel": "ef85697305025e5a61f395d4eaede272a5393cee479ace6686dba707de804d59",
|
||||
"https://bcr.bazel.build/modules/rules_kotlin/1.9.6/MODULE.bazel": "d269a01a18ee74d0335450b10f62c9ed81f2321d7958a2934e44272fe82dcef3",
|
||||
"https://bcr.bazel.build/modules/rules_kotlin/1.9.6/source.json": "2faa4794364282db7c06600b7e5e34867a564ae91bda7cae7c29c64e9466b7d5",
|
||||
@@ -306,12 +314,12 @@
|
||||
"https://bcr.bazel.build/modules/rules_nodejs/6.3.0/MODULE.bazel": "45345e4aba35dd6e4701c1eebf5a4e67af4ed708def9ebcdc6027585b34ee52d",
|
||||
"https://bcr.bazel.build/modules/rules_nodejs/6.3.3/MODULE.bazel": "b66eadebd10f1f1b25f52f95ab5213a57e82c37c3f656fcd9a57ad04d2264ce7",
|
||||
"https://bcr.bazel.build/modules/rules_nodejs/6.3.3/source.json": "45bd343155bdfed2543f0e39b80ff3f6840efc31975da4b5795797f4c94147ad",
|
||||
"https://bcr.bazel.build/modules/rules_oci/2.2.6/MODULE.bazel": "2ba6ddd679269e00aeffe9ca04faa2d0ca4129650982c9246d0d459fe2da47d9",
|
||||
"https://bcr.bazel.build/modules/rules_oci/2.2.6/source.json": "94e7decb8f95d9465b0bbea71c65064cd16083be1350c7468f131818641dc4a5",
|
||||
"https://bcr.bazel.build/modules/rules_oci/2.2.7/MODULE.bazel": "f6150e4b224d459f7f6523ef65967464ca4efdd266c7fbf2f5a2a51011957e0c",
|
||||
"https://bcr.bazel.build/modules/rules_oci/2.2.7/source.json": "b099f02af330f47f19dc67fc9300ef6e1937a8c86882690db0e7a2fcea8c7f6b",
|
||||
"https://bcr.bazel.build/modules/rules_pkg/0.7.0/MODULE.bazel": "df99f03fc7934a4737122518bb87e667e62d780b610910f0447665a7e2be62dc",
|
||||
"https://bcr.bazel.build/modules/rules_pkg/1.0.1/MODULE.bazel": "5b1df97dbc29623bccdf2b0dcd0f5cb08e2f2c9050aab1092fd39a41e82686ff",
|
||||
"https://bcr.bazel.build/modules/rules_pkg/1.1.0/MODULE.bazel": "9db8031e71b6ef32d1846106e10dd0ee2deac042bd9a2de22b4761b0c3036453",
|
||||
"https://bcr.bazel.build/modules/rules_pkg/1.1.0/source.json": "fef768df13a92ce6067e1cd0cdc47560dace01354f1d921cfb1d632511f7d608",
|
||||
"https://bcr.bazel.build/modules/rules_pkg/1.2.0/MODULE.bazel": "c7db3c2b407e673c7a39e3625dc05dc9f12d6682cbd82a3a5924a13b491eda7e",
|
||||
"https://bcr.bazel.build/modules/rules_pkg/1.2.0/source.json": "9062e00845bf91a4247465d371baa837adf9b6ff44c542f73ba084f07667e1dc",
|
||||
"https://bcr.bazel.build/modules/rules_proto/4.0.0/MODULE.bazel": "a7a7b6ce9bee418c1a760b3d84f83a299ad6952f9903c67f19e4edd964894e06",
|
||||
"https://bcr.bazel.build/modules/rules_proto/5.3.0-21.7/MODULE.bazel": "e8dff86b0971688790ae75528fe1813f71809b5afd57facb44dad9e8eca631b7",
|
||||
"https://bcr.bazel.build/modules/rules_proto/6.0.0-rc1/MODULE.bazel": "1e5b502e2e1a9e825eef74476a5a1ee524a92297085015a052510b09a1a09483",
|
||||
@@ -334,7 +342,8 @@
|
||||
"https://bcr.bazel.build/modules/rules_python/0.4.0/MODULE.bazel": "9208ee05fd48bf09ac60ed269791cf17fb343db56c8226a720fbb1cdf467166c",
|
||||
"https://bcr.bazel.build/modules/rules_python/0.40.0/MODULE.bazel": "9d1a3cd88ed7d8e39583d9ffe56ae8a244f67783ae89b60caafc9f5cf318ada7",
|
||||
"https://bcr.bazel.build/modules/rules_python/1.0.0/MODULE.bazel": "898a3d999c22caa585eb062b600f88654bf92efb204fa346fb55f6f8edffca43",
|
||||
"https://bcr.bazel.build/modules/rules_python/1.0.0/source.json": "b0162a65c6312e45e7912e39abd1a7f8856c2c7e41ecc9b6dc688a6f6400a917",
|
||||
"https://bcr.bazel.build/modules/rules_python/1.3.0/MODULE.bazel": "8361d57eafb67c09b75bf4bbe6be360e1b8f4f18118ab48037f2bd50aa2ccb13",
|
||||
"https://bcr.bazel.build/modules/rules_python/1.3.0/source.json": "25932f917cd279c7baefa6cb1d3fa8750a7a29de522024449b19af6eab51f4a0",
|
||||
"https://bcr.bazel.build/modules/rules_rust/0.45.1/MODULE.bazel": "a69d0db3a958fab2c6520961e1b2287afcc8b36690fd31bbc4f6f7391397150d",
|
||||
"https://bcr.bazel.build/modules/rules_scala/7.1.1/MODULE.bazel": "b1f80c52ae49b27d41b9291d8b328b69247de2b7596d35d09afe6147b82cf562",
|
||||
"https://bcr.bazel.build/modules/rules_scala/7.1.1/source.json": "5038cb231d4020c5965c920681cf961a7bf137b40315025e40f3a7b6a0ac1f0f",
|
||||
@@ -345,8 +354,8 @@
|
||||
"https://bcr.bazel.build/modules/rules_swift/1.16.0/MODULE.bazel": "4a09f199545a60d09895e8281362b1ff3bb08bbde69c6fc87aff5b92fcc916ca",
|
||||
"https://bcr.bazel.build/modules/rules_swift/1.18.0/MODULE.bazel": "a6aba73625d0dc64c7b4a1e831549b6e375fbddb9d2dde9d80c9de6ec45b24c9",
|
||||
"https://bcr.bazel.build/modules/rules_swift/2.1.1/MODULE.bazel": "494900a80f944fc7aa61500c2073d9729dff0b764f0e89b824eb746959bc1046",
|
||||
"https://bcr.bazel.build/modules/rules_swift/2.3.1/MODULE.bazel": "0b42093600d9226bcbdb31fb86d25d4204293d716fdbb2e50a1852547032a660",
|
||||
"https://bcr.bazel.build/modules/rules_swift/2.3.1/source.json": "87d28609c37d2061db2f6fc3aae8ab7fbda9adf556cd88fbd0c7d520b8d81391",
|
||||
"https://bcr.bazel.build/modules/rules_swift/2.4.0/MODULE.bazel": "1639617eb1ede28d774d967a738b4a68b0accb40650beadb57c21846beab5efd",
|
||||
"https://bcr.bazel.build/modules/rules_swift/2.4.0/source.json": "a6577f57f9febbdc015a01f2a8f3487422032f134d6c61d18ed8e8ca3b9acc7c",
|
||||
"https://bcr.bazel.build/modules/stardoc/0.5.0/MODULE.bazel": "f9f1f46ba8d9c3362648eea571c6f9100680efc44913618811b58cc9c02cd678",
|
||||
"https://bcr.bazel.build/modules/stardoc/0.5.1/MODULE.bazel": "1a05d92974d0c122f5ccf09291442580317cdd859f07a8655f1db9a60374f9f8",
|
||||
"https://bcr.bazel.build/modules/stardoc/0.5.3/MODULE.bazel": "c7f6948dae6999bf0db32c1858ae345f112cacf98f174c7a8bb707e41b974f1c",
|
||||
@@ -360,6 +369,7 @@
|
||||
"https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.1/MODULE.bazel": "5e463fbfba7b1701d957555ed45097d7f984211330106ccd1352c6e0af0dcf91",
|
||||
"https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.1/source.json": "32bd87e5f4d7acc57c5b2ff7c325ae3061d5e242c0c4c214ae87e0f1c13e54cb",
|
||||
"https://bcr.bazel.build/modules/tar.bzl/0.2.1/MODULE.bazel": "52d1c00a80a8cc67acbd01649e83d8dd6a9dc426a6c0b754a04fe8c219c76468",
|
||||
"https://bcr.bazel.build/modules/tar.bzl/0.5.1/MODULE.bazel": "7c2eb3dcfc53b0f3d6f9acdfd911ca803eaf92aadf54f8ca6e4c1f3aee288351",
|
||||
"https://bcr.bazel.build/modules/tar.bzl/0.6.0/MODULE.bazel": "a3584b4edcfafcabd9b0ef9819808f05b372957bbdff41601429d5fd0aac2e7c",
|
||||
"https://bcr.bazel.build/modules/tar.bzl/0.6.0/source.json": "4a620381df075a16cb3a7ed57bd1d05f7480222394c64a20fa51bdb636fda658",
|
||||
"https://bcr.bazel.build/modules/toolchains_llvm/1.6.0/MODULE.bazel": "39603859cafb1c6830160fcd6370552e836790e6abb2bfb8d13bff53c0c10a64",
|
||||
@@ -386,7 +396,7 @@
|
||||
"@@apple_support~//crosstool:setup.bzl%apple_cc_configure_extension": {
|
||||
"general": {
|
||||
"bzlTransitiveDigest": "Z3yAd66IJL0GAZUTSeMOjoHiE1SZPPwiIs/XQui5BvE=",
|
||||
"usagesDigest": "TOb4CUri5UsTKxgIDTNzR0ddIc21eYLCRIm+jqQmjlg=",
|
||||
"usagesDigest": "tl3VVeQX3Hzh7FhM2gjnkCwEJpRMlY5S6a850WY/xvc=",
|
||||
"recordedFileInputs": {},
|
||||
"recordedDirentsInputs": {},
|
||||
"envVariables": {},
|
||||
@@ -413,7 +423,7 @@
|
||||
},
|
||||
"@@aspect_rules_esbuild~//esbuild:extensions.bzl%esbuild": {
|
||||
"general": {
|
||||
"bzlTransitiveDigest": "8jv3p0xDR/oitFeH8y0+Y5xlyrUbfsTRlc9TSwYkwl8=",
|
||||
"bzlTransitiveDigest": "RzTo7pj0Tdkwa3Ld5BAVhGFSvWroST8GAQLuwwC0jdQ=",
|
||||
"usagesDigest": "iDVoyPxUeADmfK8ssoyG3Ehq1bj6p7A43LpEiE266os=",
|
||||
"recordedFileInputs": {},
|
||||
"recordedDirentsInputs": {},
|
||||
@@ -521,6 +531,11 @@
|
||||
}
|
||||
},
|
||||
"recordedRepoMappingEntries": [
|
||||
[
|
||||
"aspect_bazel_lib~",
|
||||
"bazel_lib",
|
||||
"bazel_lib~"
|
||||
],
|
||||
[
|
||||
"aspect_bazel_lib~",
|
||||
"bazel_skylib",
|
||||
@@ -555,6 +570,11 @@
|
||||
"aspect_rules_js~",
|
||||
"bazel_tools",
|
||||
"bazel_tools"
|
||||
],
|
||||
[
|
||||
"bazel_lib~",
|
||||
"bazel_tools",
|
||||
"bazel_tools"
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -1292,8 +1312,8 @@
|
||||
},
|
||||
"@@rules_oci~//oci:extensions.bzl%oci": {
|
||||
"general": {
|
||||
"bzlTransitiveDigest": "FaY+7xb13bB3hmxqwAWaGp3Tf3Q4Nfdlr+F38CP5mcg=",
|
||||
"usagesDigest": "39yHQmifPoGf+JSYpnQSnJGugxbrFVge/+ENmUiqZ6M=",
|
||||
"bzlTransitiveDigest": "AOLP47LtVHSKSDiukosQymx543OwcgeoQP666wwuj3o=",
|
||||
"usagesDigest": "3Xsv1/UEV8MOARW4BZScPw3Gxtx19OQ5EXOqcL1p9bI=",
|
||||
"recordedFileInputs": {},
|
||||
"recordedDirentsInputs": {},
|
||||
"envVariables": {},
|
||||
@@ -1580,6 +1600,43 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"@@rules_python~//python/uv:uv.bzl%uv": {
|
||||
"general": {
|
||||
"bzlTransitiveDigest": "Xpqjnjzy6zZ90Es9Wa888ZLHhn7IsNGbph/e6qoxzw8=",
|
||||
"usagesDigest": "qI5PVlIum/YAnGJg5oXGHzDkMFWt2aNSUZY4G8PBbic=",
|
||||
"recordedFileInputs": {},
|
||||
"recordedDirentsInputs": {},
|
||||
"envVariables": {},
|
||||
"generatedRepoSpecs": {
|
||||
"uv": {
|
||||
"bzlFile": "@@rules_python~//python/uv/private:uv_toolchains_repo.bzl",
|
||||
"ruleClassName": "uv_toolchains_repo",
|
||||
"attributes": {
|
||||
"toolchain_type": "'@@rules_python~//python/uv:uv_toolchain_type'",
|
||||
"toolchain_names": [
|
||||
"none"
|
||||
],
|
||||
"toolchain_implementations": {
|
||||
"none": "'@@rules_python~//python:none'"
|
||||
},
|
||||
"toolchain_compatible_with": {
|
||||
"none": [
|
||||
"@platforms//:incompatible"
|
||||
]
|
||||
},
|
||||
"toolchain_target_settings": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"recordedRepoMappingEntries": [
|
||||
[
|
||||
"rules_python~",
|
||||
"platforms",
|
||||
"platforms"
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"@@rules_scala~//scala/extensions:config.bzl%scala_config": {
|
||||
"general": {
|
||||
"bzlTransitiveDigest": "TdBxhkZTM7VU6teIFS+KoonKU7wmb5BL7leCWWx7yX8=",
|
||||
@@ -5056,7 +5113,7 @@
|
||||
"@@rules_swift~//swift:extensions.bzl%non_module_deps": {
|
||||
"general": {
|
||||
"bzlTransitiveDigest": "PAIMhc1bVKfcyoHeg0xO8LMS9KN5yzbsMGwa5O2ifJM=",
|
||||
"usagesDigest": "A3fzk5iHsrLdI3PokT1bHIdeJ2j9tc09H3/3Old6IfU=",
|
||||
"usagesDigest": "l2vIL7SL4tJqHIVLWd78Y/ym+r0II64lmvoX8o+0Bb0=",
|
||||
"recordedFileInputs": {},
|
||||
"recordedDirentsInputs": {},
|
||||
"envVariables": {},
|
||||
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
. ./ci/unity_version.sh
|
||||
|
||||
WORKSPACE=$(pwd)
|
||||
UNITY_INSTALL_PATH="/Applications/Unity/Hub/Editor"
|
||||
BUILD_DIR=$1
|
||||
LOG_PATH=$2
|
||||
|
||||
echo "Building Mac in $BUILD_DIR"
|
||||
|
||||
echo "Cleaning up $BUILD_DIR"
|
||||
/bin/rm -rf "$BUILD_DIR"
|
||||
/bin/mkdir -p "$BUILD_DIR"
|
||||
|
||||
${UNITY_INSTALL_PATH}/${UNITY_VERSION}/Unity.app/Contents/MacOS/Unity \
|
||||
-nographics \
|
||||
-batchmode \
|
||||
-quit \
|
||||
-buildOSXUniversalPlayer "$BUILD_DIR/eagle0.app" \
|
||||
-logFile "$LOG_PATH" \
|
||||
-projectPath "$WORKSPACE/src/main/csharp/net/eagle0/clients/unity/eagle0"
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
COMMIT=$(/usr/bin/git rev-parse --short HEAD)
|
||||
|
||||
/bin/echo "build protos"
|
||||
./scripts/build_protos.sh
|
||||
|
||||
/bin/echo "build Mac plugin"
|
||||
./scripts/build_mac_plugin.sh
|
||||
|
||||
git log -3
|
||||
|
||||
/bin/echo "build Mac"
|
||||
LOG_PATH="/tmp/eagle0/editor_mac.log"
|
||||
BUILD_DIR=$1
|
||||
|
||||
./ci/github_actions/build_mac.sh "$BUILD_DIR" "$LOG_PATH"
|
||||
@@ -1,6 +1,20 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euxo pipefail
|
||||
set -uxo pipefail
|
||||
|
||||
/bin/echo "persist Library/"
|
||||
/usr/bin/rsync -rtlDvq src/main/csharp/net/eagle0/clients/unity/eagle0/Library/ /tmp/eagle0/Library/
|
||||
|
||||
# rsync may exit with code 23 ("partial transfer due to error") if Unity's
|
||||
# temporary files vanish during the copy. This is acceptable for a cache.
|
||||
/usr/bin/rsync -rtlDvq src/main/csharp/net/eagle0/clients/unity/eagle0/Library/ /tmp/eagle0/Library/
|
||||
rsync_exit=$?
|
||||
|
||||
if [ $rsync_exit -eq 0 ]; then
|
||||
exit 0
|
||||
elif [ $rsync_exit -eq 23 ]; then
|
||||
echo "Warning: rsync exited with 23 (some files vanished during copy). This is expected for Unity temp files."
|
||||
exit 0
|
||||
else
|
||||
echo "Error: rsync failed with exit code $rsync_exit"
|
||||
exit $rsync_exit
|
||||
fi
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<!-- Allow JIT compilation (required for Mono/IL2CPP) -->
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<!-- Allow unsigned executable memory (required for Unity) -->
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<!-- Disable library validation (required for plugins) -->
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
<!-- Allow outgoing network connections -->
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,40 @@
|
||||
# Environment template for production deployment
|
||||
# This file defines all env vars used by docker-compose.prod.yml
|
||||
# Workflows should update their specific vars without overwriting others
|
||||
|
||||
# Container images (managed by respective build workflows)
|
||||
EAGLE_IMAGE=registry.digitalocean.com/eagle0/eagle-server:latest
|
||||
SHARDOK_IMAGE=registry.digitalocean.com/eagle0/shardok-server:latest
|
||||
ADMIN_IMAGE=registry.digitalocean.com/eagle0/admin-server:latest
|
||||
JFR_SIDECAR_IMAGE=registry.digitalocean.com/eagle0/jfr-sidecar:latest
|
||||
AUTH_IMAGE=registry.digitalocean.com/eagle0/auth-server:latest
|
||||
|
||||
# OpenAI / LLM
|
||||
OPENAI_API_KEY=
|
||||
GPT_MODEL_NAME=gpt-4o
|
||||
|
||||
# DigitalOcean Spaces (S3-compatible storage)
|
||||
EAGLE_ENABLE_S3=false
|
||||
DO_SPACES_ACCESS_KEY=
|
||||
DO_SPACES_SECRET_KEY=
|
||||
|
||||
# JWT authentication
|
||||
JWT_PRIVATE_KEY=
|
||||
|
||||
# OAuth providers
|
||||
DISCORD_CLIENT_ID=
|
||||
DISCORD_CLIENT_SECRET=
|
||||
GOOGLE_CLIENT_ID=
|
||||
GOOGLE_CLIENT_SECRET=
|
||||
|
||||
# Shardok connection
|
||||
SHARDOK_ADDRESS=shardok:40042
|
||||
SHARDOK_AUTH_TOKEN=
|
||||
|
||||
# Monitoring
|
||||
SENTRY_DSN=
|
||||
|
||||
# Email (Fastmail JMAP)
|
||||
FASTMAIL_API_TOKEN=
|
||||
FASTMAIL_FROM_EMAIL=
|
||||
FASTMAIL_FROM_NAME=
|
||||
@@ -0,0 +1,59 @@
|
||||
#!/bin/bash
|
||||
# Update .env file without losing other variables
|
||||
# Usage: ./update-env.sh KEY1=value1 KEY2=value2 ...
|
||||
#
|
||||
# This script:
|
||||
# 1. Creates .env from template if it doesn't exist
|
||||
# 2. Updates only the specified KEY=value pairs
|
||||
# 3. Preserves all other existing values
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
ENV_FILE="${ENV_FILE:-/opt/eagle0/.env}"
|
||||
TEMPLATE_FILE="${TEMPLATE_FILE:-$SCRIPT_DIR/env.template}"
|
||||
|
||||
# Create .env from template if it doesn't exist
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
if [ -f "$TEMPLATE_FILE" ]; then
|
||||
echo "Creating .env from template..."
|
||||
grep -v '^#' "$TEMPLATE_FILE" | grep -v '^$' > "$ENV_FILE"
|
||||
else
|
||||
echo "Creating empty .env..."
|
||||
touch "$ENV_FILE"
|
||||
fi
|
||||
chmod 600 "$ENV_FILE"
|
||||
fi
|
||||
|
||||
# Process each KEY=VALUE argument
|
||||
for arg in "$@"; do
|
||||
# Skip empty args
|
||||
[ -z "$arg" ] && continue
|
||||
|
||||
# Parse KEY=VALUE
|
||||
KEY="${arg%%=*}"
|
||||
VALUE="${arg#*=}"
|
||||
|
||||
# Skip if no key
|
||||
[ -z "$KEY" ] && continue
|
||||
|
||||
# Skip setting empty values (keeps existing value)
|
||||
if [ -z "$VALUE" ]; then
|
||||
echo "Skipping $KEY (empty value)"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Remove existing line for this key and add new one
|
||||
if grep -q "^${KEY}=" "$ENV_FILE" 2>/dev/null; then
|
||||
# Key exists, update it
|
||||
sed -i "s|^${KEY}=.*|${KEY}=${VALUE}|" "$ENV_FILE"
|
||||
echo "Updated $KEY"
|
||||
else
|
||||
# Key doesn't exist, add it
|
||||
echo "${KEY}=${VALUE}" >> "$ENV_FILE"
|
||||
echo "Added $KEY"
|
||||
fi
|
||||
done
|
||||
|
||||
chmod 600 "$ENV_FILE"
|
||||
echo "Done updating $ENV_FILE"
|
||||
+64
-8
@@ -8,9 +8,13 @@
|
||||
# Run: docker compose -f docker-compose.prod.yml up -d
|
||||
|
||||
services:
|
||||
eagle:
|
||||
# Blue-green deployment: eagle-blue is the primary (production) instance
|
||||
# eagle-green is the staging instance for zero-downtime deployments
|
||||
# See scripts/deploy-blue-green.sh for deployment workflow
|
||||
|
||||
eagle-blue:
|
||||
image: ${EAGLE_IMAGE:-registry.digitalocean.com/eagle0/eagle-server:latest}
|
||||
container_name: eagle-server
|
||||
container_name: eagle-blue
|
||||
command:
|
||||
- "--gpt-model-name"
|
||||
- "${GPT_MODEL_NAME:-gpt-5.1}"
|
||||
@@ -39,7 +43,7 @@ services:
|
||||
volumes:
|
||||
- ./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
|
||||
- ./jfr:/app/jfr # JFR recordings - dump with: docker exec eagle-blue jcmd 1 JFR.dump filename=/app/jfr/profile.jfr
|
||||
- jvm-tmp:/tmp # Shared with jfr-sidecar for JVM attach socket files
|
||||
- jwt-keys:/etc/eagle0/keys # Shared JWT keys with auth service
|
||||
depends_on:
|
||||
@@ -58,6 +62,58 @@ services:
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
|
||||
eagle-green:
|
||||
image: ${EAGLE_IMAGE_NEW:-registry.digitalocean.com/eagle0/eagle-server:latest}
|
||||
container_name: eagle-green
|
||||
profiles: ["blue-green"] # Only started during blue-green deployment
|
||||
command:
|
||||
- "--gpt-model-name"
|
||||
- "${GPT_MODEL_NAME:-gpt-5.1}"
|
||||
- "--shardok-interface-remote-address"
|
||||
- "${SHARDOK_ADDRESS:-shardok:40042}"
|
||||
- "--auth-service-url"
|
||||
- "auth:40033"
|
||||
ports:
|
||||
- "40034:40032" # Different host port for staging
|
||||
environment:
|
||||
OPENAI_API_KEY: "${OPENAI_API_KEY:-}"
|
||||
EAGLE_ENABLE_S3: "${EAGLE_ENABLE_S3:-false}"
|
||||
DO_SPACES_ENDPOINT: "${DO_SPACES_ENDPOINT:-https://sfo3.digitaloceanspaces.com}"
|
||||
DO_SPACES_ACCESS_KEY: "${DO_SPACES_ACCESS_KEY:-}"
|
||||
DO_SPACES_SECRET_KEY: "${DO_SPACES_SECRET_KEY:-}"
|
||||
JWT_PRIVATE_KEY: "${JWT_PRIVATE_KEY:-}"
|
||||
DISCORD_CLIENT_ID: "${DISCORD_CLIENT_ID:-}"
|
||||
DISCORD_CLIENT_SECRET: "${DISCORD_CLIENT_SECRET:-}"
|
||||
SHARDOK_AUTH_TOKEN: "${SHARDOK_AUTH_TOKEN:-}"
|
||||
EAGLE_SAVE_DIR: "/app/saves"
|
||||
EAGLE_ARCHIVE_DIR: "/app/archived"
|
||||
SENTRY_DSN: "${SENTRY_DSN:-}"
|
||||
SENTRY_ENVIRONMENT: "production"
|
||||
volumes:
|
||||
- ./saves:/app/saves # Same save directory as blue
|
||||
- ./archived:/app/archived # Same archive directory as blue
|
||||
- jwt-keys:/etc/eagle0/keys # Shared JWT keys with auth service
|
||||
depends_on:
|
||||
- shardok
|
||||
- auth
|
||||
restart: "no" # Don't auto-restart during deployment
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "100m"
|
||||
max-file: "5"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "nc -z localhost 40032 || exit 1"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 6
|
||||
start_period: 60s
|
||||
|
||||
# Backward compatibility alias - for scripts that reference 'eagle' service
|
||||
eagle:
|
||||
extends:
|
||||
service: eagle-blue
|
||||
|
||||
auth:
|
||||
image: ${AUTH_IMAGE:-registry.digitalocean.com/eagle0/auth-server:latest}
|
||||
container_name: auth-server
|
||||
@@ -140,7 +196,7 @@ services:
|
||||
- ./certbot/www:/var/www/certbot:ro
|
||||
- ./auth:/etc/nginx/auth:ro
|
||||
depends_on:
|
||||
- eagle
|
||||
- eagle-blue
|
||||
- admin
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
@@ -154,7 +210,7 @@ services:
|
||||
container_name: admin-server
|
||||
command:
|
||||
- "--eagle-addr"
|
||||
- "eagle:40032"
|
||||
- "${EAGLE_ADDR:-eagle-blue:40032}" # Can be switched for blue-green
|
||||
- "--auth-addr"
|
||||
- "auth:40033"
|
||||
- "--jfr-sidecar-addr"
|
||||
@@ -163,7 +219,7 @@ services:
|
||||
- "8080"
|
||||
# No external port - accessed via nginx at admin.eagle0.net
|
||||
depends_on:
|
||||
- eagle
|
||||
- eagle-blue
|
||||
- auth
|
||||
- jfr-sidecar
|
||||
restart: unless-stopped
|
||||
@@ -183,11 +239,11 @@ services:
|
||||
image: ${JFR_SIDECAR_IMAGE:-registry.digitalocean.com/eagle0/jfr-sidecar:latest}
|
||||
container_name: jfr-sidecar
|
||||
# Share PID namespace with Eagle to access its JVM via jcmd
|
||||
pid: "service:eagle"
|
||||
pid: "service:eagle-blue"
|
||||
volumes:
|
||||
- jvm-tmp:/tmp # Shared with Eagle for JVM attach socket files
|
||||
depends_on:
|
||||
- eagle
|
||||
- eagle-blue
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
driver: "json-file"
|
||||
|
||||
+18
-8
@@ -24,10 +24,13 @@ http {
|
||||
# This prevents stale IP caching when containers restart
|
||||
resolver 127.0.0.11 valid=10s ipv6=off;
|
||||
|
||||
# Upstream for Eagle gRPC server
|
||||
upstream eagle_grpc {
|
||||
server eagle:40032;
|
||||
keepalive 100;
|
||||
# Eagle backend address - configured via variable for blue-green deployments
|
||||
# Using a variable makes nginx resolve the hostname at request time, not config load time
|
||||
# This allows nginx to reload even when the backend is temporarily down
|
||||
# For blue-green deployments, this is switched between eagle-blue:40032 and eagle-green:40032
|
||||
# by scripts/deploy-blue-green.sh, then nginx is reloaded with 'nginx -s reload'
|
||||
map $host $eagle_backend {
|
||||
default "eagle-blue:40032";
|
||||
}
|
||||
|
||||
# HTTP server for Let's Encrypt challenge and redirect
|
||||
@@ -71,8 +74,8 @@ http {
|
||||
# Rate limiting
|
||||
limit_req zone=grpc_limit burst=50 nodelay;
|
||||
|
||||
# gRPC proxy
|
||||
grpc_pass grpc://eagle_grpc;
|
||||
# gRPC proxy - uses variable for blue-green deployment support
|
||||
grpc_pass grpc://$eagle_backend;
|
||||
|
||||
# Timeouts for long-running streams
|
||||
grpc_read_timeout 1200s;
|
||||
@@ -88,8 +91,8 @@ http {
|
||||
# Rate limiting
|
||||
limit_req zone=grpc_limit burst=50 nodelay;
|
||||
|
||||
# gRPC proxy
|
||||
grpc_pass grpc://eagle_grpc;
|
||||
# gRPC proxy - uses variable for blue-green deployment support
|
||||
grpc_pass grpc://$eagle_backend;
|
||||
|
||||
# Timeouts
|
||||
grpc_read_timeout 30s;
|
||||
@@ -106,6 +109,13 @@ http {
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
# Invitation landing page (proxied to Go auth service)
|
||||
location /invite/ {
|
||||
proxy_pass http://auth:8080;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
# Health check endpoint
|
||||
location /health {
|
||||
access_log off;
|
||||
|
||||
Executable
+88
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Code sign a macOS .app bundle for distribution
|
||||
# Usage: codesign_mac_app.sh <app_path> [entitlements_path]
|
||||
#
|
||||
# Environment variables:
|
||||
# SIGNING_IDENTITY - The signing identity (default: "Developer ID Application")
|
||||
# KEYCHAIN_PASSWORD - Password to unlock the build keychain (optional)
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
APP_PATH="$1"
|
||||
ENTITLEMENTS_PATH="${2:-}"
|
||||
SIGNING_IDENTITY="${SIGNING_IDENTITY:-Developer ID Application}"
|
||||
|
||||
if [ ! -d "$APP_PATH" ]; then
|
||||
echo "ERROR: App not found at $APP_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Unlock keychain if password provided
|
||||
if [ -n "${KEYCHAIN_PASSWORD:-}" ]; then
|
||||
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain || true
|
||||
fi
|
||||
|
||||
echo "=== Signing nested components first ==="
|
||||
|
||||
# Sign all dylibs
|
||||
find "$APP_PATH" -name "*.dylib" -print0 | while IFS= read -r -d '' item; do
|
||||
echo "Signing dylib: $item"
|
||||
codesign --force --verify --verbose --timestamp --options runtime \
|
||||
--sign "$SIGNING_IDENTITY" "$item"
|
||||
done
|
||||
|
||||
# Sign all bundles (plugins)
|
||||
find "$APP_PATH" -name "*.bundle" -print0 | while IFS= read -r -d '' item; do
|
||||
echo "Signing bundle: $item"
|
||||
codesign --force --verify --verbose --timestamp --options runtime \
|
||||
--sign "$SIGNING_IDENTITY" "$item"
|
||||
done
|
||||
|
||||
# Sign XPC services (inside Sparkle framework)
|
||||
find "$APP_PATH" -name "*.xpc" -print0 | while IFS= read -r -d '' item; do
|
||||
echo "Signing XPC service: $item"
|
||||
codesign --force --verify --verbose --timestamp --options runtime \
|
||||
--sign "$SIGNING_IDENTITY" "$item"
|
||||
done
|
||||
|
||||
# Sign nested apps (like Sparkle's Updater.app)
|
||||
find "$APP_PATH" -path "*/Frameworks/*.app" -print0 | while IFS= read -r -d '' item; do
|
||||
echo "Signing nested app: $item"
|
||||
codesign --force --verify --verbose --timestamp --options runtime \
|
||||
--sign "$SIGNING_IDENTITY" "$item"
|
||||
done
|
||||
|
||||
# Sign standalone executables inside frameworks (like Autoupdate)
|
||||
find "$APP_PATH" -path "*/Frameworks/*/Versions/*/Autoupdate" -type f -print0 | while IFS= read -r -d '' item; do
|
||||
echo "Signing executable: $item"
|
||||
codesign --force --verify --verbose --timestamp --options runtime \
|
||||
--sign "$SIGNING_IDENTITY" "$item"
|
||||
done
|
||||
|
||||
# Sign all frameworks (after their contents are signed)
|
||||
find "$APP_PATH" -name "*.framework" -print0 | while IFS= read -r -d '' item; do
|
||||
echo "Signing framework: $item"
|
||||
codesign --force --verify --verbose --timestamp --options runtime \
|
||||
--sign "$SIGNING_IDENTITY" "$item"
|
||||
done
|
||||
|
||||
echo "=== Signing main app bundle ==="
|
||||
|
||||
if [ -n "$ENTITLEMENTS_PATH" ] && [ -f "$ENTITLEMENTS_PATH" ]; then
|
||||
echo "Using entitlements: $ENTITLEMENTS_PATH"
|
||||
codesign --force --verify --verbose --timestamp --options runtime \
|
||||
--entitlements "$ENTITLEMENTS_PATH" \
|
||||
--sign "$SIGNING_IDENTITY" "$APP_PATH"
|
||||
else
|
||||
codesign --force --verify --verbose --timestamp --options runtime \
|
||||
--sign "$SIGNING_IDENTITY" "$APP_PATH"
|
||||
fi
|
||||
|
||||
echo "=== Verifying signature ==="
|
||||
codesign --verify --verbose=4 "$APP_PATH"
|
||||
|
||||
echo "=== Checking Gatekeeper assessment ==="
|
||||
spctl --assess --type exec -v "$APP_PATH" || echo "Note: Gatekeeper may reject until notarized"
|
||||
|
||||
echo "Code signing complete: $APP_PATH"
|
||||
Executable
+247
@@ -0,0 +1,247 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Blue-Green Deployment Script for Eagle Server
|
||||
#
|
||||
# This script performs a zero-downtime deployment by:
|
||||
# 1. Starting the new version on a staging port (green)
|
||||
# 2. Waiting for it to become healthy
|
||||
# 3. Running warmup traffic to pre-heat the JIT
|
||||
# 4. Stopping the old version (blue) - which flushes state to disk
|
||||
# 5. Telling green to reload games from disk
|
||||
# 6. Switching nginx to route traffic to green
|
||||
# 7. Cleaning up
|
||||
#
|
||||
# Usage: ./deploy-blue-green.sh [NEW_IMAGE_TAG]
|
||||
#
|
||||
# Example:
|
||||
# ./deploy-blue-green.sh latest
|
||||
# ./deploy-blue-green.sh sha-abc123
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
APP_DIR="${APP_DIR:-/opt/eagle0}"
|
||||
NGINX_CONF="${APP_DIR}/nginx/nginx.conf"
|
||||
COMPOSE_FILE="${APP_DIR}/docker-compose.prod.yml"
|
||||
WARMUP_SCRIPT="${SCRIPT_DIR}/warmup-eagle.sh"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
|
||||
# Determine which instance is currently active
|
||||
get_active_instance() {
|
||||
if grep -q "eagle-blue:40032" "${NGINX_CONF}"; then
|
||||
echo "blue"
|
||||
elif grep -q "eagle-green:40032" "${NGINX_CONF}"; then
|
||||
echo "green"
|
||||
else
|
||||
log_error "Cannot determine active instance from nginx config"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Pull image with retry using crane (handles OCI/Docker digest mismatch)
|
||||
pull_with_retry() {
|
||||
local image=$1
|
||||
local max_attempts=${2:-3}
|
||||
local attempt=1
|
||||
|
||||
# Use crane if available (handles OCI format correctly)
|
||||
if [ -x "${APP_DIR}/crane" ]; then
|
||||
while [ $attempt -le $max_attempts ]; do
|
||||
log_info "Pulling image with crane (attempt ${attempt}/${max_attempts})..."
|
||||
if "${APP_DIR}/crane" pull "${image}" /tmp/image.tar && docker load -i /tmp/image.tar; then
|
||||
rm -f /tmp/image.tar
|
||||
log_info "Image pulled and loaded successfully"
|
||||
return 0
|
||||
fi
|
||||
rm -f /tmp/image.tar
|
||||
log_warn "Pull failed, retrying in 5 seconds..."
|
||||
sleep 5
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
else
|
||||
# Fallback to docker pull if crane not available
|
||||
log_warn "crane not found at ${APP_DIR}/crane, falling back to docker pull"
|
||||
while [ $attempt -le $max_attempts ]; do
|
||||
log_info "Pulling image (attempt ${attempt}/${max_attempts})..."
|
||||
if docker pull "${image}"; then
|
||||
log_info "Image pulled successfully"
|
||||
return 0
|
||||
fi
|
||||
log_warn "Pull failed, retrying in 5 seconds..."
|
||||
sleep 5
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
fi
|
||||
log_error "Failed to pull image after ${max_attempts} attempts"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Wait for a container to be healthy
|
||||
wait_for_healthy() {
|
||||
local container=$1
|
||||
local max_attempts=${2:-60}
|
||||
local attempt=1
|
||||
|
||||
log_info "Waiting for ${container} to become healthy..."
|
||||
while [ $attempt -le $max_attempts ]; do
|
||||
health=$(docker inspect --format='{{.State.Health.Status}}' "${container}" 2>/dev/null || echo "unknown")
|
||||
if [ "$health" = "healthy" ]; then
|
||||
log_info "${container} is healthy"
|
||||
return 0
|
||||
fi
|
||||
echo -n "."
|
||||
sleep 2
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
echo ""
|
||||
log_error "${container} did not become healthy after $((max_attempts * 2)) seconds"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Main deployment logic
|
||||
main() {
|
||||
local new_tag="${1:-latest}"
|
||||
local registry="registry.digitalocean.com/eagle0/eagle-server"
|
||||
local new_image="${registry}:${new_tag}"
|
||||
|
||||
log_info "Starting blue-green deployment"
|
||||
log_info "New image: ${new_image}"
|
||||
|
||||
cd "${APP_DIR}"
|
||||
|
||||
# Determine current active instance
|
||||
local active=$(get_active_instance)
|
||||
local staging
|
||||
if [ "$active" = "blue" ]; then
|
||||
staging="green"
|
||||
else
|
||||
staging="blue"
|
||||
fi
|
||||
|
||||
log_info "Active instance: eagle-${active}"
|
||||
log_info "Staging instance: eagle-${staging}"
|
||||
|
||||
# Pull the new image (with retry for intermittent registry issues)
|
||||
if ! pull_with_retry "${new_image}" 3; then
|
||||
log_error "Failed to pull new image, aborting deployment"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Start staging instance with new image
|
||||
log_info "Starting eagle-${staging} with new image..."
|
||||
if [ "$staging" = "green" ]; then
|
||||
EAGLE_IMAGE_NEW="${new_image}" docker compose -f "${COMPOSE_FILE}" --profile blue-green up -d eagle-green
|
||||
else
|
||||
EAGLE_IMAGE="${new_image}" docker compose -f "${COMPOSE_FILE}" up -d eagle-blue
|
||||
fi
|
||||
|
||||
# Wait for staging to be healthy
|
||||
if ! wait_for_healthy "eagle-${staging}" 90; then
|
||||
log_error "Staging instance failed health check, aborting deployment"
|
||||
docker compose -f "${COMPOSE_FILE}" stop "eagle-${staging}"
|
||||
docker compose -f "${COMPOSE_FILE}" rm -f "eagle-${staging}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run warmup/smoke test
|
||||
local staging_port
|
||||
if [ "$staging" = "green" ]; then
|
||||
staging_port=40034
|
||||
else
|
||||
staging_port=40032
|
||||
fi
|
||||
|
||||
log_info "Running warmup against eagle-${staging}..."
|
||||
if [ -x "${WARMUP_SCRIPT}" ]; then
|
||||
if ! "${WARMUP_SCRIPT}" "localhost:${staging_port}"; then
|
||||
log_error "Warmup/smoke test failed, aborting deployment"
|
||||
docker compose -f "${COMPOSE_FILE}" stop "eagle-${staging}"
|
||||
docker compose -f "${COMPOSE_FILE}" rm -f "eagle-${staging}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
log_warn "Warmup script not found at ${WARMUP_SCRIPT}, skipping warmup"
|
||||
log_warn "JIT will be cold on first requests"
|
||||
fi
|
||||
|
||||
# Stop the active instance (this flushes state to disk)
|
||||
log_info "Stopping eagle-${active} (flushing state to disk)..."
|
||||
docker compose -f "${COMPOSE_FILE}" stop "eagle-${active}"
|
||||
|
||||
# Tell staging to reload games from disk
|
||||
log_info "Telling eagle-${staging} to reload games from disk..."
|
||||
if command -v grpcurl &> /dev/null; then
|
||||
grpcurl -plaintext -d '{}' "localhost:${staging_port}" net.eagle0.eagle.api.Eagle/ReloadGames || true
|
||||
else
|
||||
log_warn "grpcurl not installed, skipping game reload"
|
||||
log_warn "New instance will use games loaded at startup"
|
||||
fi
|
||||
|
||||
# Switch nginx upstream
|
||||
log_info "Switching nginx upstream to eagle-${staging}..."
|
||||
if [ "$staging" = "green" ]; then
|
||||
sed -i.bak 's/eagle-blue:40032/eagle-green:40032/g' "${NGINX_CONF}"
|
||||
else
|
||||
sed -i.bak 's/eagle-green:40032/eagle-blue:40032/g' "${NGINX_CONF}"
|
||||
fi
|
||||
|
||||
# Reload nginx
|
||||
log_info "Reloading nginx..."
|
||||
docker compose -f "${COMPOSE_FILE}" exec nginx nginx -s reload
|
||||
|
||||
# Clean up old instance
|
||||
log_info "Removing old eagle-${active} container..."
|
||||
docker compose -f "${COMPOSE_FILE}" rm -f "eagle-${active}"
|
||||
|
||||
# Update the staging instance's restart policy and image var
|
||||
# For blue, we need to update EAGLE_IMAGE; for green, update EAGLE_IMAGE_NEW
|
||||
if [ "$staging" = "blue" ]; then
|
||||
log_info "Updating EAGLE_IMAGE to ${new_image} for future restarts"
|
||||
# User should update their .env file
|
||||
else
|
||||
log_info "Green is now active. Consider switching to blue on next deployment."
|
||||
fi
|
||||
|
||||
log_info "Deployment complete!"
|
||||
log_info "Active instance: eagle-${staging}"
|
||||
log_info ""
|
||||
log_info "Note: Update your .env file with EAGLE_IMAGE=${new_image}"
|
||||
log_info " if you want future 'docker compose up' to use this version."
|
||||
}
|
||||
|
||||
# Check for required tools
|
||||
check_requirements() {
|
||||
if ! command -v docker &> /dev/null; then
|
||||
log_error "docker is required but not installed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v sed &> /dev/null; then
|
||||
log_error "sed is required but not installed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "${NGINX_CONF}" ]; then
|
||||
log_error "nginx config not found at ${NGINX_CONF}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "${COMPOSE_FILE}" ]; then
|
||||
log_error "docker-compose file not found at ${COMPOSE_FILE}"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Run
|
||||
check_requirements
|
||||
main "$@"
|
||||
@@ -0,0 +1,41 @@
|
||||
// +build ignore
|
||||
|
||||
// Script to generate Ed25519 key pair for manifest signing.
|
||||
// Run with: go run scripts/generate_manifest_keys.go
|
||||
//
|
||||
// This will output:
|
||||
// - Private key (base64): Store as MANIFEST_SIGNING_KEY GitHub secret
|
||||
// - Public key (base64): Embed in EagleInstaller for verification
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Generate Ed25519 key pair
|
||||
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to generate key pair: %v", err)
|
||||
}
|
||||
|
||||
// Encode keys as base64
|
||||
privateKeyB64 := base64.StdEncoding.EncodeToString(privateKey)
|
||||
publicKeyB64 := base64.StdEncoding.EncodeToString(publicKey)
|
||||
|
||||
fmt.Println("=== Ed25519 Key Pair for Manifest Signing ===")
|
||||
fmt.Println()
|
||||
fmt.Println("PRIVATE KEY (store as GitHub secret MANIFEST_SIGNING_KEY):")
|
||||
fmt.Println(privateKeyB64)
|
||||
fmt.Println()
|
||||
fmt.Println("PUBLIC KEY (embed in EagleInstaller.cs for verification):")
|
||||
fmt.Println(publicKeyB64)
|
||||
fmt.Println()
|
||||
fmt.Printf("Private key size: %d bytes\n", len(privateKey))
|
||||
fmt.Printf("Public key size: %d bytes\n", len(publicKey))
|
||||
}
|
||||
Executable
+93
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Inject Sparkle framework into a macOS .app bundle for auto-updates
|
||||
# Usage: inject_sparkle.sh <app_path>
|
||||
#
|
||||
# Environment variables (required):
|
||||
# SPARKLE_EDDSA_PUBLIC_KEY - EdDSA public key for verifying updates
|
||||
#
|
||||
# Optional environment variables:
|
||||
# SPARKLE_FEED_URL - Appcast URL (default: https://assets.eagle0.net/mac/appcast.xml)
|
||||
# SPARKLE_VERSION - Sparkle version to use (default: 2.6.4)
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
APP_PATH="$1"
|
||||
SPARKLE_VERSION="${SPARKLE_VERSION:-2.6.4}"
|
||||
SPARKLE_FEED_URL="${SPARKLE_FEED_URL:-https://assets.eagle0.net/mac/appcast.xml}"
|
||||
SPARKLE_CACHE_DIR="/tmp/sparkle-cache"
|
||||
|
||||
if [ ! -d "$APP_PATH" ]; then
|
||||
echo "ERROR: App not found at $APP_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${SPARKLE_EDDSA_PUBLIC_KEY:-}" ]; then
|
||||
echo "ERROR: SPARKLE_EDDSA_PUBLIC_KEY environment variable not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Download Sparkle if not cached
|
||||
SPARKLE_DIR="$SPARKLE_CACHE_DIR/Sparkle-$SPARKLE_VERSION"
|
||||
if [ ! -d "$SPARKLE_DIR/Sparkle.framework" ]; then
|
||||
echo "=== Downloading Sparkle $SPARKLE_VERSION ==="
|
||||
mkdir -p "$SPARKLE_CACHE_DIR"
|
||||
SPARKLE_URL="https://github.com/sparkle-project/Sparkle/releases/download/${SPARKLE_VERSION}/Sparkle-${SPARKLE_VERSION}.tar.xz"
|
||||
curl -L "$SPARKLE_URL" | tar -xJ -C "$SPARKLE_CACHE_DIR"
|
||||
mv "$SPARKLE_CACHE_DIR/Sparkle-$SPARKLE_VERSION" "$SPARKLE_DIR" 2>/dev/null || true
|
||||
# If the extracted directory doesn't match version pattern, it may just be "Sparkle"
|
||||
if [ ! -d "$SPARKLE_DIR" ]; then
|
||||
mkdir -p "$SPARKLE_DIR"
|
||||
mv "$SPARKLE_CACHE_DIR/Sparkle.framework" "$SPARKLE_DIR/" 2>/dev/null || true
|
||||
mv "$SPARKLE_CACHE_DIR/bin" "$SPARKLE_DIR/" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "=== Injecting Sparkle framework ==="
|
||||
FRAMEWORKS_DIR="$APP_PATH/Contents/Frameworks"
|
||||
mkdir -p "$FRAMEWORKS_DIR"
|
||||
|
||||
# Copy Sparkle framework
|
||||
cp -R "$SPARKLE_DIR/Sparkle.framework" "$FRAMEWORKS_DIR/"
|
||||
|
||||
# Also copy the XPC services if present
|
||||
if [ -d "$SPARKLE_DIR/Sparkle.framework/Versions/B/XPCServices" ]; then
|
||||
echo "Sparkle XPC services present"
|
||||
fi
|
||||
|
||||
echo "=== Updating Info.plist ==="
|
||||
PLIST_PATH="$APP_PATH/Contents/Info.plist"
|
||||
|
||||
# Add Sparkle configuration to Info.plist
|
||||
/usr/libexec/PlistBuddy -c "Delete :SUFeedURL" "$PLIST_PATH" 2>/dev/null || true
|
||||
/usr/libexec/PlistBuddy -c "Add :SUFeedURL string '$SPARKLE_FEED_URL'" "$PLIST_PATH"
|
||||
|
||||
/usr/libexec/PlistBuddy -c "Delete :SUPublicEDKey" "$PLIST_PATH" 2>/dev/null || true
|
||||
/usr/libexec/PlistBuddy -c "Add :SUPublicEDKey string '$SPARKLE_EDDSA_PUBLIC_KEY'" "$PLIST_PATH"
|
||||
|
||||
/usr/libexec/PlistBuddy -c "Delete :SUEnableAutomaticChecks" "$PLIST_PATH" 2>/dev/null || true
|
||||
/usr/libexec/PlistBuddy -c "Add :SUEnableAutomaticChecks bool true" "$PLIST_PATH"
|
||||
|
||||
# Set bundle version from git for Sparkle version comparison
|
||||
VERSION=$(git describe --tags --always 2>/dev/null || echo "1.0.0")
|
||||
BUILD_NUMBER=$(git rev-list --count HEAD 2>/dev/null || echo "1")
|
||||
|
||||
echo "Setting version: $VERSION (build $BUILD_NUMBER)"
|
||||
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $VERSION" "$PLIST_PATH" 2>/dev/null || \
|
||||
/usr/libexec/PlistBuddy -c "Add :CFBundleShortVersionString string '$VERSION'" "$PLIST_PATH"
|
||||
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $BUILD_NUMBER" "$PLIST_PATH" 2>/dev/null || \
|
||||
/usr/libexec/PlistBuddy -c "Add :CFBundleVersion string '$BUILD_NUMBER'" "$PLIST_PATH"
|
||||
|
||||
# Add URL scheme for invitation codes (eagle0://invite?code=XXXX)
|
||||
echo "=== Adding URL scheme for invitation codes ==="
|
||||
/usr/libexec/PlistBuddy -c "Delete :CFBundleURLTypes" "$PLIST_PATH" 2>/dev/null || true
|
||||
/usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes array" "$PLIST_PATH"
|
||||
/usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes:0 dict" "$PLIST_PATH"
|
||||
/usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes:0:CFBundleURLName string 'com.Shardok-Games.eagle0'" "$PLIST_PATH"
|
||||
/usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes:0:CFBundleURLSchemes array" "$PLIST_PATH"
|
||||
/usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes:0:CFBundleURLSchemes:0 string 'eagle0'" "$PLIST_PATH"
|
||||
|
||||
echo "=== Sparkle injection complete ==="
|
||||
echo "App: $APP_PATH"
|
||||
echo "Feed URL: $SPARKLE_FEED_URL"
|
||||
echo "Version: $VERSION (build $BUILD_NUMBER)"
|
||||
Executable
+84
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Notarize a macOS .app bundle with Apple
|
||||
# Usage: notarize_mac_app.sh <app_path>
|
||||
#
|
||||
# Environment variables (required):
|
||||
# APPLE_ID - Apple Developer account email
|
||||
# APP_SPECIFIC_PASSWORD - App-specific password for notarytool
|
||||
# TEAM_ID - Apple Developer Team ID
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
APP_PATH="$1"
|
||||
|
||||
if [ ! -d "$APP_PATH" ]; then
|
||||
echo "ERROR: App not found at $APP_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${APPLE_ID:-}" ] || [ -z "${APP_SPECIFIC_PASSWORD:-}" ] || [ -z "${TEAM_ID:-}" ]; then
|
||||
echo "ERROR: Required environment variables not set"
|
||||
echo " APPLE_ID: ${APPLE_ID:-<not set>}"
|
||||
echo " APP_SPECIFIC_PASSWORD: ${APP_SPECIFIC_PASSWORD:+<set>}"
|
||||
echo " TEAM_ID: ${TEAM_ID:-<not set>}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create ZIP for notarization submission
|
||||
ZIP_PATH="${APP_PATH%.app}.zip"
|
||||
echo "=== Creating ZIP for notarization: $ZIP_PATH ==="
|
||||
ditto -c -k --keepParent "$APP_PATH" "$ZIP_PATH"
|
||||
|
||||
echo "=== Submitting to Apple for notarization ==="
|
||||
SUBMIT_OUTPUT=$(xcrun notarytool submit "$ZIP_PATH" \
|
||||
--apple-id "$APPLE_ID" \
|
||||
--password "$APP_SPECIFIC_PASSWORD" \
|
||||
--team-id "$TEAM_ID" \
|
||||
--wait 2>&1) || true
|
||||
|
||||
echo "$SUBMIT_OUTPUT"
|
||||
|
||||
# Extract submission ID and status (look for " status:" to avoid matching "Current status:")
|
||||
SUBMISSION_ID=$(echo "$SUBMIT_OUTPUT" | grep "id:" | head -1 | awk '{print $2}')
|
||||
STATUS=$(echo "$SUBMIT_OUTPUT" | grep "^ status:" | awk '{print $2}')
|
||||
|
||||
echo "Submission ID: $SUBMISSION_ID"
|
||||
echo "Status: $STATUS"
|
||||
|
||||
# Clean up the zip
|
||||
rm "$ZIP_PATH"
|
||||
|
||||
if [ "$STATUS" != "Accepted" ]; then
|
||||
echo "=== Notarization failed! Fetching log for details ==="
|
||||
xcrun notarytool log "$SUBMISSION_ID" \
|
||||
--apple-id "$APPLE_ID" \
|
||||
--password "$APP_SPECIFIC_PASSWORD" \
|
||||
--team-id "$TEAM_ID"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=== Stapling notarization ticket to app ==="
|
||||
# Retry stapling - Apple's CloudKit can have a brief delay after notarization completes
|
||||
MAX_STAPLE_ATTEMPTS=5
|
||||
STAPLE_ATTEMPT=1
|
||||
while [ $STAPLE_ATTEMPT -le $MAX_STAPLE_ATTEMPTS ]; do
|
||||
echo "Stapling attempt $STAPLE_ATTEMPT/$MAX_STAPLE_ATTEMPTS..."
|
||||
if xcrun stapler staple "$APP_PATH"; then
|
||||
echo "Stapling successful"
|
||||
break
|
||||
fi
|
||||
if [ $STAPLE_ATTEMPT -eq $MAX_STAPLE_ATTEMPTS ]; then
|
||||
echo "ERROR: Stapling failed after $MAX_STAPLE_ATTEMPTS attempts"
|
||||
exit 1
|
||||
fi
|
||||
echo "Stapling failed, waiting 10 seconds before retry..."
|
||||
sleep 10
|
||||
STAPLE_ATTEMPT=$((STAPLE_ATTEMPT + 1))
|
||||
done
|
||||
|
||||
echo "=== Verifying notarization ==="
|
||||
xcrun stapler validate "$APP_PATH"
|
||||
spctl --assess --type exec -v "$APP_PATH"
|
||||
|
||||
echo "Notarization complete: $APP_PATH"
|
||||
Executable
+142
@@ -0,0 +1,142 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Warmup Script for Eagle Server
|
||||
#
|
||||
# This script warms up the JIT compiler before switching traffic to a new instance.
|
||||
# It uses the Go warmup tool which:
|
||||
# 1. Creates a test game via bidirectional streaming
|
||||
# 2. Posts an Improve command
|
||||
# 3. Verifies action results and new commands
|
||||
# 4. Cleans up the test game
|
||||
#
|
||||
# Usage: ./warmup-eagle.sh HOST:PORT
|
||||
#
|
||||
# Example:
|
||||
# ./warmup-eagle.sh localhost:40032
|
||||
# ./warmup-eagle.sh localhost:40034
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
|
||||
HOST="${1:-localhost:40032}"
|
||||
|
||||
log_info "Warming up Eagle server at ${HOST}..."
|
||||
|
||||
# Try to find the Go warmup tool
|
||||
WARMUP_TOOL=""
|
||||
|
||||
# Check if we're in the project directory with bazel
|
||||
if [ -f "${PROJECT_ROOT}/WORKSPACE" ] || [ -f "${PROJECT_ROOT}/WORKSPACE.bazel" ]; then
|
||||
# Try to find the pre-built binary
|
||||
BAZEL_BIN="${PROJECT_ROOT}/bazel-bin/src/main/go/net/eagle0/warmup/warmup_/warmup"
|
||||
if [ -x "${BAZEL_BIN}" ]; then
|
||||
WARMUP_TOOL="${BAZEL_BIN}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check for the warmup tool in common locations (for deployed environments)
|
||||
if [ -z "${WARMUP_TOOL}" ]; then
|
||||
for path in \
|
||||
"${SCRIPT_DIR}/bin/warmup" \
|
||||
"/opt/eagle0/scripts/bin/warmup" \
|
||||
"/opt/eagle0/bin/warmup" \
|
||||
"/usr/local/bin/eagle-warmup" \
|
||||
"${SCRIPT_DIR}/warmup"; do
|
||||
if [ -x "${path}" ]; then
|
||||
WARMUP_TOOL="${path}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# If we found the Go tool, use it
|
||||
if [ -n "${WARMUP_TOOL}" ]; then
|
||||
log_info "Using Go warmup tool: ${WARMUP_TOOL}"
|
||||
if "${WARMUP_TOOL}" --address="${HOST}" --timeout=60s; then
|
||||
log_info "Warmup complete!"
|
||||
exit 0
|
||||
else
|
||||
log_error "Go warmup tool failed"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Fallback to grpcurl-based warmup
|
||||
log_warn "Go warmup tool not found, falling back to grpcurl"
|
||||
|
||||
# Check for grpcurl
|
||||
if ! command -v grpcurl &> /dev/null; then
|
||||
log_error "Neither Go warmup tool nor grpcurl is available"
|
||||
log_error "Build the warmup tool with: bazel build //src/main/go/net/eagle0/warmup"
|
||||
log_error "Or install grpcurl: brew install grpcurl (macOS)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Warmup iterations
|
||||
WARMUP_ITERATIONS=3
|
||||
|
||||
# 1. Call GetRunningGames multiple times - this exercises the gRPC layer and basic game access
|
||||
log_info "Warming up GetRunningGames..."
|
||||
for i in $(seq 1 ${WARMUP_ITERATIONS}); do
|
||||
RESULT=$(grpcurl -plaintext -d '{}' "${HOST}" net.eagle0.eagle.api.Eagle/GetRunningGames 2>&1) || true
|
||||
if echo "$RESULT" | grep -q "games\|{}"; then
|
||||
echo -n "."
|
||||
else
|
||||
log_error "GetRunningGames failed on iteration $i"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo " done"
|
||||
|
||||
# 2. Call GetSettings - exercises settings loading
|
||||
log_info "Warming up GetSettings..."
|
||||
for i in $(seq 1 ${WARMUP_ITERATIONS}); do
|
||||
if grpcurl -plaintext -d '{}' "${HOST}" net.eagle0.eagle.api.Eagle/GetSettings > /dev/null 2>&1; then
|
||||
echo -n "."
|
||||
else
|
||||
log_warn "GetSettings failed on iteration $i (non-fatal)"
|
||||
fi
|
||||
done
|
||||
echo " done"
|
||||
|
||||
# 3. Call AddSettings with empty list - exercises settings path
|
||||
log_info "Warming up AddSettings..."
|
||||
for i in $(seq 1 ${WARMUP_ITERATIONS}); do
|
||||
if grpcurl -plaintext -d '{"settings": []}' "${HOST}" net.eagle0.eagle.api.Eagle/AddSettings > /dev/null 2>&1; then
|
||||
echo -n "."
|
||||
else
|
||||
log_warn "AddSettings failed on iteration $i (non-fatal)"
|
||||
fi
|
||||
done
|
||||
echo " done"
|
||||
|
||||
# Final health check
|
||||
log_info "Verifying server health..."
|
||||
if grpcurl -plaintext -d '{}' "${HOST}" net.eagle0.eagle.api.Eagle/GetRunningGames > /dev/null 2>&1; then
|
||||
log_info "Health check passed"
|
||||
else
|
||||
log_error "Health check failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_info ""
|
||||
log_info "Warmup complete (basic mode - bidirectional streaming warmup not available)!"
|
||||
log_info "The JIT should be warmed for:"
|
||||
log_info " - gRPC layer and protobuf parsing"
|
||||
log_info " - Settings loading and management"
|
||||
log_info ""
|
||||
log_warn "Note: For full warmup including game creation and command processing,"
|
||||
log_warn " build and use the Go warmup tool: bazel build //src/main/go/net/eagle0/warmup"
|
||||
@@ -107,6 +107,23 @@ auto AIAttackerStrategySelector::BestAttackerStrategy(
|
||||
alCache,
|
||||
battalionTypeGetter,
|
||||
braveWaterCost));
|
||||
} else if (!defenderPositions.empty()) {
|
||||
// Defenders exist but none are on castles - they're scattering/fleeing.
|
||||
// Chase them down rather than holding empty castles, since eliminating
|
||||
// all defenders also wins the battle via LAST_PLAYER_STANDING.
|
||||
chosenStrategy = AttackUnitsStrategy(GenerateTargetPriorities(
|
||||
Occupants(
|
||||
*gameState->units(),
|
||||
gameState->hex_map()->row_count(),
|
||||
gameState->hex_map()->column_count()),
|
||||
gameState->hex_map(),
|
||||
defenderPositions,
|
||||
attackerPid,
|
||||
attackerUnits,
|
||||
apdCache,
|
||||
alCache,
|
||||
battalionTypeGetter,
|
||||
braveWaterCost));
|
||||
} else {
|
||||
chosenStrategy = HoldCastlesStrategy;
|
||||
}
|
||||
|
||||
@@ -126,6 +126,7 @@
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Notification/NotificationManagerEditor.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Button/ButtonManagerBasicEditor.cs" />
|
||||
<Compile Include="Assets/ConnectionHandler/RunningGameItem.cs" />
|
||||
<Compile Include="Assets/Tutorial/UI/TutorialCanvasBuilder.cs" />
|
||||
<Compile Include="Assets/Shardok/Table Rows/ArmyRowController.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/PrisonerExchangeDetailsNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/Notification.cs" />
|
||||
@@ -372,6 +373,7 @@
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/SendSuppliesCommandSelector.cs" />
|
||||
<Compile Include="Assets/Tutorial/UI/TutorialUIManager.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Window/WindowManager.cs" />
|
||||
<Compile Include="Assets/Auth/InvitationCodeManager.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/CapturedHeroExiledDetailsNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/SuppressBeastsSucceededNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Shardok/ReservesTableController.cs" />
|
||||
@@ -384,6 +386,7 @@
|
||||
<Compile Include="Assets/Eagle/Table Rows/IncomingArmyTableRow.cs" />
|
||||
<Compile Include="Assets/ConnectionHandler/CreateGameItem.cs" />
|
||||
<Compile Include="Assets/Eagle/EagleGameController.cs" />
|
||||
<Compile Include="Assets/Tutorial/TutorialTestSetup.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/FailedSwearBrotherhoodNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Shardok/FireEffectAnimator.cs" />
|
||||
<Compile Include="Assets/common/GUIUtils/EventBasedTable.cs" />
|
||||
|
||||
+6
-1
@@ -122,8 +122,13 @@ namespace Auth {
|
||||
string localAppData =
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||
return Path.Combine(localAppData, "eagle0");
|
||||
#elif UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX
|
||||
// Mac: ~/Library/Application Support/eagle0
|
||||
string appSupport =
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||
return Path.Combine(appSupport, "eagle0");
|
||||
#else
|
||||
// Other platforms don't use the Windows installer
|
||||
// Other platforms not yet supported
|
||||
return null;
|
||||
#endif
|
||||
}
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a2a19f11d1f82412e8e2811b366113ac
|
||||
@@ -2992,6 +2992,90 @@ MonoBehaviour:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_IsOn: 1
|
||||
--- !u!1 &16227905
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 16227909}
|
||||
- component: {fileID: 16227908}
|
||||
- component: {fileID: 16227907}
|
||||
- component: {fileID: 16227906}
|
||||
m_Layer: 0
|
||||
m_Name: TutorialManager
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!114 &16227906
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 16227905}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 370b3269832442e196d1b432bdae2f1e, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::Eagle0.Tutorial.TutorialTestSetup
|
||||
EnableTestTutorials: 1
|
||||
ResetProgressOnStart: 0
|
||||
--- !u!114 &16227907
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 16227905}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 52abae862492497cb7a3f8c72f837b37, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::Eagle0.Tutorial.TutorialUIManager
|
||||
ModalPanel: {fileID: 0}
|
||||
OverlayController: {fileID: 0}
|
||||
HintIndicatorPrefab: {fileID: 0}
|
||||
TutorialCanvas: {fileID: 0}
|
||||
CanvasFont: {fileID: 11400000, guid: ec2736df0379a45bfa7349b652fd07d6, type: 2}
|
||||
UseFallbackUI: 1
|
||||
FallbackFont: {fileID: 12800000, guid: f0f6814ff8ef048bcbfd5002f77188ea, type: 3}
|
||||
AutoBuildCanvasUI: 1
|
||||
--- !u!114 &16227908
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 16227905}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: c529affa73b04d129ec5826ed0ac75e7, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::Eagle0.Tutorial.TutorialManager
|
||||
TutorialsEnabled: 1
|
||||
OnboardingSequence: {fileID: 0}
|
||||
UIManager: {fileID: 16227907}
|
||||
DebugLogging: 0
|
||||
--- !u!4 &16227909
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 16227905}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 2560, y: 1440, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &17277548
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -8532,10 +8616,10 @@ RectTransform:
|
||||
- {fileID: 1396438488}
|
||||
m_Father: {fileID: 2130578398}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 333.5, y: -11.255}
|
||||
m_SizeDelta: {x: 200, y: 20}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &75920627
|
||||
MonoBehaviour:
|
||||
@@ -10927,10 +11011,10 @@ RectTransform:
|
||||
m_Children: []
|
||||
m_Father: {fileID: 2130578398}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 218.5, y: -11.255}
|
||||
m_SizeDelta: {x: 20, y: 20}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &103511974
|
||||
MonoBehaviour:
|
||||
@@ -12298,10 +12382,10 @@ RectTransform:
|
||||
m_Children: []
|
||||
m_Father: {fileID: 683322975}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 306.5, y: -40}
|
||||
m_SizeDelta: {x: 51, y: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &114306270
|
||||
MonoBehaviour:
|
||||
@@ -15614,6 +15698,10 @@ MonoBehaviour:
|
||||
displayNameField: {fileID: 1956057658}
|
||||
setDisplayNameButton: {fileID: 700483942}
|
||||
displayNameErrorText: {fileID: 77360322}
|
||||
invitationCodePanel: {fileID: 0}
|
||||
invitationCodeField: {fileID: 0}
|
||||
submitInvitationCodeButton: {fileID: 0}
|
||||
invitationCodeErrorText: {fileID: 0}
|
||||
connectionStatusText: {fileID: 77360322}
|
||||
connectionPanel: {fileID: 596196018}
|
||||
gameSelectionPanel: {fileID: 1102342355}
|
||||
@@ -20624,10 +20712,10 @@ RectTransform:
|
||||
m_Children: []
|
||||
m_Father: {fileID: 1566150938}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 256, y: -32.504997}
|
||||
m_SizeDelta: {x: 492, y: 45.01}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &171635196
|
||||
MonoBehaviour:
|
||||
@@ -29397,7 +29485,7 @@ RectTransform:
|
||||
m_Father: {fileID: 208702977}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 10, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
@@ -31829,7 +31917,7 @@ RectTransform:
|
||||
m_Father: {fileID: 1396438488}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 20, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
@@ -33967,6 +34055,7 @@ MonoBehaviour:
|
||||
eagleGameController: {fileID: 1965467940}
|
||||
ShardokCanvas: {fileID: 2088078267}
|
||||
connectionHandler: {fileID: 135486720}
|
||||
resetTutorialsButton: {fileID: 0}
|
||||
--- !u!1 &305665272
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -36778,10 +36867,10 @@ RectTransform:
|
||||
- {fileID: 1666727525}
|
||||
m_Father: {fileID: 2039520752}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 321, y: -11.255}
|
||||
m_SizeDelta: {x: 200, y: 20}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &329919723
|
||||
MonoBehaviour:
|
||||
@@ -47131,8 +47220,8 @@ RectTransform:
|
||||
m_Children: []
|
||||
m_Father: {fileID: 1666727525}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.1, y: 0}
|
||||
m_AnchorMax: {x: 0.1, y: 1}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 20, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
@@ -64015,10 +64104,10 @@ RectTransform:
|
||||
- {fileID: 1375685348}
|
||||
m_Father: {fileID: 1566150938}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 256, y: -215.03}
|
||||
m_SizeDelta: {x: 492, y: 30}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &548637657
|
||||
MonoBehaviour:
|
||||
@@ -71487,10 +71576,10 @@ RectTransform:
|
||||
m_Children: []
|
||||
m_Father: {fileID: 1566150938}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 10, y: -120.03}
|
||||
m_SizeDelta: {x: 0, y: 10}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &590819392
|
||||
MonoBehaviour:
|
||||
@@ -74595,6 +74684,146 @@ CanvasRenderer:
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 624827069}
|
||||
m_CullTransparentMesh: 0
|
||||
--- !u!1 &625062879
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 625062880}
|
||||
- component: {fileID: 625062882}
|
||||
- component: {fileID: 625062881}
|
||||
m_Layer: 0
|
||||
m_Name: Open Resources
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &625062880
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 625062879}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 673182551}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &625062881
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 625062879}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_text: 'Reset Tutorials
|
||||
|
||||
'
|
||||
m_isRightToLeft: 0
|
||||
m_fontAsset: {fileID: 11400000, guid: ec2736df0379a45bfa7349b652fd07d6, type: 2}
|
||||
m_sharedMaterial: {fileID: 1026247389042360723, guid: ec2736df0379a45bfa7349b652fd07d6,
|
||||
type: 2}
|
||||
m_fontSharedMaterials: []
|
||||
m_fontMaterial: {fileID: 0}
|
||||
m_fontMaterials: []
|
||||
m_fontColor32:
|
||||
serializedVersion: 2
|
||||
rgba: 4281479730
|
||||
m_fontColor: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
|
||||
m_enableVertexGradient: 0
|
||||
m_colorMode: 3
|
||||
m_fontColorGradient:
|
||||
topLeft: {r: 1, g: 1, b: 1, a: 1}
|
||||
topRight: {r: 1, g: 1, b: 1, a: 1}
|
||||
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
|
||||
bottomRight: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_fontColorGradientPreset: {fileID: 0}
|
||||
m_spriteAsset: {fileID: 0}
|
||||
m_tintAllSprites: 0
|
||||
m_StyleSheet: {fileID: 0}
|
||||
m_TextStyleHashCode: -1183493901
|
||||
m_overrideHtmlColors: 0
|
||||
m_faceColor:
|
||||
serializedVersion: 2
|
||||
rgba: 4294967295
|
||||
m_fontSize: 18
|
||||
m_fontSizeBase: 18
|
||||
m_fontWeight: 400
|
||||
m_enableAutoSizing: 0
|
||||
m_fontSizeMin: 18
|
||||
m_fontSizeMax: 72
|
||||
m_fontStyle: 0
|
||||
m_HorizontalAlignment: 2
|
||||
m_VerticalAlignment: 512
|
||||
m_textAlignment: 65535
|
||||
m_characterSpacing: 0
|
||||
m_characterHorizontalScale: 1
|
||||
m_wordSpacing: 0
|
||||
m_lineSpacing: 0
|
||||
m_lineSpacingMax: 0
|
||||
m_paragraphSpacing: 0
|
||||
m_charWidthMaxAdj: 0
|
||||
m_TextWrappingMode: 1
|
||||
m_wordWrappingRatios: 0.4
|
||||
m_overflowMode: 0
|
||||
m_linkedTextComponent: {fileID: 0}
|
||||
parentLinkedComponent: {fileID: 0}
|
||||
m_enableKerning: 0
|
||||
m_ActiveFontFeatures: 6e72656b
|
||||
m_enableExtraPadding: 0
|
||||
checkPaddingRequired: 0
|
||||
m_isRichText: 1
|
||||
m_EmojiFallbackSupport: 1
|
||||
m_parseCtrlCharacters: 1
|
||||
m_isOrthographic: 1
|
||||
m_isCullingEnabled: 0
|
||||
m_horizontalMapping: 0
|
||||
m_verticalMapping: 0
|
||||
m_uvLineOffset: 0
|
||||
m_geometrySortingOrder: 0
|
||||
m_IsTextObjectScaleStatic: 0
|
||||
m_VertexBufferAutoSizeReduction: 0
|
||||
m_useMaxVisibleDescender: 1
|
||||
m_pageToDisplay: 1
|
||||
m_margin: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_isUsingLegacyAnimationComponent: 0
|
||||
m_isVolumetricText: 0
|
||||
m_hasFontAssetChanged: 0
|
||||
m_baseMaterial: {fileID: 0}
|
||||
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
|
||||
--- !u!222 &625062882
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 625062879}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!1 &625252446
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -82452,6 +82681,160 @@ CanvasRenderer:
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 673008387}
|
||||
m_CullTransparentMesh: 0
|
||||
--- !u!1 &673182550
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 673182551}
|
||||
- component: {fileID: 673182555}
|
||||
- component: {fileID: 673182554}
|
||||
- component: {fileID: 673182553}
|
||||
- component: {fileID: 673182552}
|
||||
m_Layer: 0
|
||||
m_Name: Reset Tutorials Button
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &673182551
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 673182550}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 625062880}
|
||||
m_Father: {fileID: 1566150938}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &673182552
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 673182550}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_IgnoreLayout: 0
|
||||
m_MinWidth: -1
|
||||
m_MinHeight: 30
|
||||
m_PreferredWidth: -1
|
||||
m_PreferredHeight: 30
|
||||
m_FlexibleWidth: 1
|
||||
m_FlexibleHeight: -1
|
||||
m_LayoutPriority: 1
|
||||
--- !u!114 &673182553
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 673182550}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Navigation:
|
||||
m_Mode: 3
|
||||
m_WrapAround: 0
|
||||
m_SelectOnUp: {fileID: 0}
|
||||
m_SelectOnDown: {fileID: 0}
|
||||
m_SelectOnLeft: {fileID: 0}
|
||||
m_SelectOnRight: {fileID: 0}
|
||||
m_Transition: 1
|
||||
m_Colors:
|
||||
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
|
||||
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
|
||||
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
|
||||
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
|
||||
m_ColorMultiplier: 1
|
||||
m_FadeDuration: 0.1
|
||||
m_SpriteState:
|
||||
m_HighlightedSprite: {fileID: 0}
|
||||
m_PressedSprite: {fileID: 0}
|
||||
m_SelectedSprite: {fileID: 0}
|
||||
m_DisabledSprite: {fileID: 0}
|
||||
m_AnimationTriggers:
|
||||
m_NormalTrigger: Normal
|
||||
m_HighlightedTrigger: Highlighted
|
||||
m_PressedTrigger: Pressed
|
||||
m_SelectedTrigger: Selected
|
||||
m_DisabledTrigger: Disabled
|
||||
m_Interactable: 1
|
||||
m_TargetGraphic: {fileID: 673182554}
|
||||
m_OnClick:
|
||||
m_PersistentCalls:
|
||||
m_Calls:
|
||||
- m_Target: {fileID: 300036928}
|
||||
m_TargetAssemblyTypeName: SettingsPanelController, Assembly-CSharp
|
||||
m_MethodName: OnResetTutorialsClick
|
||||
m_Mode: 1
|
||||
m_Arguments:
|
||||
m_ObjectArgument: {fileID: 0}
|
||||
m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine
|
||||
m_IntArgument: 0
|
||||
m_FloatArgument: 0
|
||||
m_StringArgument:
|
||||
m_BoolArgument: 0
|
||||
m_CallState: 2
|
||||
--- !u!114 &673182554
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 673182550}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0}
|
||||
m_Type: 1
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
m_FillMethod: 4
|
||||
m_FillAmount: 1
|
||||
m_FillClockwise: 1
|
||||
m_FillOrigin: 0
|
||||
m_UseSpriteMesh: 0
|
||||
m_PixelsPerUnitMultiplier: 1
|
||||
--- !u!222 &673182555
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 673182550}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!1 &673307124
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -83590,10 +83973,10 @@ RectTransform:
|
||||
- {fileID: 1119022577}
|
||||
m_Father: {fileID: 1566150938}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 256, y: -354}
|
||||
m_SizeDelta: {x: 492, y: 40}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &683322976
|
||||
MonoBehaviour:
|
||||
@@ -93480,7 +93863,7 @@ GameObject:
|
||||
- component: {fileID: 749366808}
|
||||
- component: {fileID: 749366807}
|
||||
m_Layer: 0
|
||||
m_Name: Button
|
||||
m_Name: Open Resources Button
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
@@ -93501,10 +93884,10 @@ RectTransform:
|
||||
- {fileID: 392813076}
|
||||
m_Father: {fileID: 1566150938}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 256, y: -250.03}
|
||||
m_SizeDelta: {x: 492, y: 30}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &749366807
|
||||
MonoBehaviour:
|
||||
@@ -95360,10 +95743,10 @@ RectTransform:
|
||||
- {fileID: 699949417}
|
||||
m_Father: {fileID: 1566150938}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 256, y: -180.03}
|
||||
m_SizeDelta: {x: 492, y: 30}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &770361456
|
||||
MonoBehaviour:
|
||||
@@ -96170,10 +96553,10 @@ RectTransform:
|
||||
m_Children: []
|
||||
m_Father: {fileID: 2039520752}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 208.5, y: -11.255}
|
||||
m_SizeDelta: {x: 15, y: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &779503534
|
||||
MonoBehaviour:
|
||||
@@ -99264,10 +99647,10 @@ RectTransform:
|
||||
m_Children: []
|
||||
m_Father: {fileID: 2039520752}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 446, y: -11.255}
|
||||
m_SizeDelta: {x: 40, y: 15.01}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &806086202
|
||||
MonoBehaviour:
|
||||
@@ -99806,10 +100189,10 @@ RectTransform:
|
||||
m_Children: []
|
||||
m_Father: {fileID: 1566150938}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 10, y: -313.27002}
|
||||
m_SizeDelta: {x: 0, y: 31.459991}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &808870206
|
||||
MonoBehaviour:
|
||||
@@ -105263,10 +105646,10 @@ RectTransform:
|
||||
- {fileID: 113544276}
|
||||
m_Father: {fileID: 1590704177}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 333.5, y: -11.255}
|
||||
m_SizeDelta: {x: 200, y: 20}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &858025936
|
||||
MonoBehaviour:
|
||||
@@ -121385,10 +121768,10 @@ RectTransform:
|
||||
m_Children: []
|
||||
m_Father: {fileID: 1590704177}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 196, y: -11.255}
|
||||
m_SizeDelta: {x: 15, y: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &972401127
|
||||
MonoBehaviour:
|
||||
@@ -122506,7 +122889,7 @@ RectTransform:
|
||||
m_Father: {fileID: 113544276}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 20, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
@@ -123520,7 +123903,7 @@ RectTransform:
|
||||
m_Father: {fileID: 1425392451}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0.1, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 10, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
@@ -140643,10 +141026,10 @@ RectTransform:
|
||||
- {fileID: 717768291}
|
||||
m_Father: {fileID: 683322975}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 417, y: -20}
|
||||
m_SizeDelta: {x: 150, y: 40}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1119022578
|
||||
MonoBehaviour:
|
||||
@@ -147583,10 +147966,10 @@ RectTransform:
|
||||
m_Children: []
|
||||
m_Father: {fileID: 683322975}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 135.5, y: -40}
|
||||
m_SizeDelta: {x: 51, y: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1184808158
|
||||
MonoBehaviour:
|
||||
@@ -153109,10 +153492,10 @@ RectTransform:
|
||||
m_Children: []
|
||||
m_Father: {fileID: 2130578398}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 108.5, y: -11.255}
|
||||
m_SizeDelta: {x: 150, y: 22.51}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1219993393
|
||||
MonoBehaviour:
|
||||
@@ -174192,10 +174575,10 @@ RectTransform:
|
||||
m_Children: []
|
||||
m_Father: {fileID: 2130578398}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 196, y: -11.255}
|
||||
m_SizeDelta: {x: 15, y: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1390131026
|
||||
MonoBehaviour:
|
||||
@@ -178285,10 +178668,10 @@ RectTransform:
|
||||
m_Children: []
|
||||
m_Father: {fileID: 2039520752}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 111, y: -11.255}
|
||||
m_SizeDelta: {x: 170, y: 22.51}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1415802613
|
||||
MonoBehaviour:
|
||||
@@ -178444,10 +178827,10 @@ RectTransform:
|
||||
m_Children: []
|
||||
m_Father: {fileID: 2130578398}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 448.5, y: -11.255}
|
||||
m_SizeDelta: {x: 20, y: 20}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1415887397
|
||||
MonoBehaviour:
|
||||
@@ -181904,10 +182287,10 @@ RectTransform:
|
||||
m_Children: []
|
||||
m_Father: {fileID: 1590704177}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 108.5, y: -11.255}
|
||||
m_SizeDelta: {x: 150, y: 22.51}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1448655991
|
||||
MonoBehaviour:
|
||||
@@ -196899,10 +197282,10 @@ RectTransform:
|
||||
- {fileID: 1122913432}
|
||||
m_Father: {fileID: 683322975}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 50, y: -20}
|
||||
m_SizeDelta: {x: 100, y: 40}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1557328375
|
||||
MonoBehaviour:
|
||||
@@ -198649,7 +199032,7 @@ GameObject:
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 2147483647
|
||||
m_IsActive: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &1566150938
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -198670,6 +199053,7 @@ RectTransform:
|
||||
- {fileID: 770361455}
|
||||
- {fileID: 548637656}
|
||||
- {fileID: 749366806}
|
||||
- {fileID: 673182551}
|
||||
- {fileID: 2039520752}
|
||||
- {fileID: 808870205}
|
||||
- {fileID: 683322975}
|
||||
@@ -200241,10 +200625,10 @@ RectTransform:
|
||||
- {fileID: 2123746368}
|
||||
m_Father: {fileID: 1566150938}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 256, y: -98.774994}
|
||||
m_SizeDelta: {x: 492, y: 22.51}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1590704178
|
||||
MonoBehaviour:
|
||||
@@ -219035,10 +219419,10 @@ RectTransform:
|
||||
- {fileID: 543540660}
|
||||
m_Father: {fileID: 683322975}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 221, y: -20}
|
||||
m_SizeDelta: {x: 100, y: 40}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1775111780
|
||||
MonoBehaviour:
|
||||
@@ -221290,7 +221674,7 @@ RectTransform:
|
||||
m_Father: {fileID: 1110049512}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 10, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
@@ -247406,10 +247790,10 @@ RectTransform:
|
||||
- {fileID: 806086201}
|
||||
m_Father: {fileID: 1566150938}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 256, y: -281.285}
|
||||
m_SizeDelta: {x: 492, y: 22.51}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &2039520753
|
||||
MonoBehaviour:
|
||||
@@ -252578,10 +252962,10 @@ RectTransform:
|
||||
m_Children: []
|
||||
m_Father: {fileID: 1590704177}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 218.5, y: -11.255}
|
||||
m_SizeDelta: {x: 20, y: 20}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &2096030393
|
||||
MonoBehaviour:
|
||||
@@ -255937,7 +256321,7 @@ GameObject:
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
m_IsActive: 0
|
||||
--- !u!224 &2109805906
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -255957,7 +256341,7 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 256, y: -145.03}
|
||||
m_AnchoredPosition: {x: 256, y: -142.85143}
|
||||
m_SizeDelta: {x: 492, y: 30}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &2109805907
|
||||
@@ -257369,10 +257753,10 @@ RectTransform:
|
||||
m_Children: []
|
||||
m_Father: {fileID: 1590704177}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 448.5, y: -11.255}
|
||||
m_SizeDelta: {x: 20, y: 20}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &2123746369
|
||||
MonoBehaviour:
|
||||
@@ -258672,10 +259056,10 @@ RectTransform:
|
||||
- {fileID: 1415887396}
|
||||
m_Father: {fileID: 1566150938}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 256, y: -71.265}
|
||||
m_SizeDelta: {x: 492, y: 22.51}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &2130578399
|
||||
MonoBehaviour:
|
||||
@@ -268781,6 +269165,7 @@ SceneRoots:
|
||||
m_Roots:
|
||||
- {fileID: 279614944}
|
||||
- {fileID: 815907755}
|
||||
- {fileID: 16227909}
|
||||
- {fileID: 122332320}
|
||||
- {fileID: 682560790}
|
||||
- {fileID: 1342510189}
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Diagnostics;
|
||||
using System.IO;
|
||||
using common;
|
||||
using eagle;
|
||||
using Eagle0.Tutorial;
|
||||
using Net.Eagle0.Shardok.Api;
|
||||
using Shardok;
|
||||
using TMPro;
|
||||
@@ -49,6 +50,8 @@ public class SettingsPanelController : MonoBehaviour {
|
||||
public GameObject ShardokCanvas;
|
||||
public ConnectionHandler connectionHandler;
|
||||
|
||||
public Button resetTutorialsButton;
|
||||
|
||||
void Start() {
|
||||
_active = false;
|
||||
panel.SetActive(false);
|
||||
@@ -148,6 +151,11 @@ public class SettingsPanelController : MonoBehaviour {
|
||||
Process.Start(Path.Combine(Application.persistentDataPath, "eagle0", "Resources"));
|
||||
}
|
||||
|
||||
public void OnResetTutorialsClick() {
|
||||
TutorialManager.Instance?.ResetAllProgress();
|
||||
UnityEngine.Debug.Log("Tutorial progress reset");
|
||||
}
|
||||
|
||||
private void HandleLobby() {
|
||||
eagleGameController.StopAll();
|
||||
eagleGameController.gameObject.SetActive(false);
|
||||
|
||||
@@ -68,14 +68,10 @@ namespace Shardok {
|
||||
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++) {
|
||||
|
||||
@@ -59,7 +59,7 @@ namespace Shardok {
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2? sourcePos = __hexGrid.GetCellLocalPosition(sourceCellIndex);
|
||||
Vector2? sourcePos = __hexGrid.GetCellCenterPosition(sourceCellIndex);
|
||||
Vector2? targetPos = __hexGrid.GetCellCenterPosition(targetCellIndex);
|
||||
|
||||
if (sourcePos == null || targetPos == null) {
|
||||
|
||||
@@ -135,8 +135,8 @@ namespace Shardok {
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2? attackerPos = __hexGrid.GetCellLocalPosition(attackerCellIndex);
|
||||
Vector2? defenderPos = __hexGrid.GetCellLocalPosition(defenderCellIndex);
|
||||
Vector2? attackerPos = __hexGrid.GetCellCenterPosition(attackerCellIndex);
|
||||
Vector2? defenderPos = __hexGrid.GetCellCenterPosition(defenderCellIndex);
|
||||
|
||||
if (attackerPos == null || defenderPos == null) {
|
||||
Debug.LogWarning(
|
||||
@@ -149,8 +149,6 @@ namespace Shardok {
|
||||
CleanupWeapons();
|
||||
}
|
||||
|
||||
Debug.Log(
|
||||
$"MeleeAnimator: Starting melee from cell {attackerCellIndex} ({attackerType}) to {defenderCellIndex} ({defenderType})");
|
||||
_activeAnimation = StartCoroutine(
|
||||
AnimateClash(attackerPos.Value, defenderPos.Value, attackerType, defenderType));
|
||||
}
|
||||
|
||||
@@ -65,8 +65,8 @@ namespace Shardok {
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2? sourcePos = _hexGrid.GetCellLocalPosition(sourceCellIndex);
|
||||
Vector2? targetPos = _hexGrid.GetCellLocalPosition(targetCellIndex);
|
||||
Vector2? sourcePos = _hexGrid.GetCellCenterPosition(sourceCellIndex);
|
||||
Vector2? targetPos = _hexGrid.GetCellCenterPosition(targetCellIndex);
|
||||
|
||||
if (sourcePos == null || targetPos == null) {
|
||||
Debug.LogWarning(
|
||||
|
||||
+6
-1
@@ -579,7 +579,12 @@ namespace Shardok {
|
||||
if (Model.UnitsById.TryGetValue(
|
||||
historyEntry.Actor.Value,
|
||||
out var actorUnit)) {
|
||||
int sourceIndex = MapCoordsToGridIndex(actorUnit.Location);
|
||||
// For Move actions, use stored source coords (unit's pre-move location)
|
||||
// For other actions, use current unit location
|
||||
int sourceIndex =
|
||||
Model.MoveSourceCoords.TryGetValue(i, out var sourceCoords)
|
||||
? MapCoordsToGridIndex(sourceCoords)
|
||||
: MapCoordsToGridIndex(actorUnit.Location);
|
||||
|
||||
// Get target - either coords (archery) or unit location (melee)
|
||||
int targetIndex = -1;
|
||||
|
||||
@@ -75,6 +75,13 @@ public class ShardokGameModel {
|
||||
ReserveUnitsById.Values.FirstOrDefault(u => u.Location.Equals(location));
|
||||
|
||||
public List<ActionResultView> History { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Tracks the source coordinates for Move actions, keyed by history index.
|
||||
/// Used for animations since the model is fully updated before animations play.
|
||||
/// </summary>
|
||||
public Dictionary<int, Coords> MoveSourceCoords { get; } = new();
|
||||
|
||||
public PlayerId PlayerId { get; private set; }
|
||||
public GameStatus GameStatus { get; private set; }
|
||||
public RoundId CurrentRound { get; private set; }
|
||||
@@ -402,6 +409,14 @@ public class ShardokGameModel {
|
||||
}
|
||||
|
||||
private void HandleNewHistoryEntry(ActionResultView entry) {
|
||||
// For Move actions, record the source coordinates BEFORE applying the diff
|
||||
// This is needed for animations since all diffs are applied before animations play
|
||||
if (entry.Type == ActionType.Move && entry.Actor != null) {
|
||||
if (UnitsById.TryGetValue(entry.Actor.Value, out var actorUnit)) {
|
||||
MoveSourceCoords[History.Count] = actorUnit.Location;
|
||||
}
|
||||
}
|
||||
|
||||
History.Add(entry);
|
||||
var gsvDiff = entry.GameStateViewDiff;
|
||||
if (gsvDiff == null) {
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
# Tutorial System for Eagle0
|
||||
|
||||
## Overview
|
||||
|
||||
A modular tutorial system for the Eagle0 Unity client supporting:
|
||||
- **Onboarding**: Full guided playthrough of first several turns
|
||||
- **Contextual tutorials**: First-encounter and first-attempt triggers with subtle hints
|
||||
- **Mixed UI**: Modals for concepts, overlays for UI guidance, hint indicators
|
||||
- **Local persistence**: PlayerPrefs-based state tracking
|
||||
|
||||
---
|
||||
|
||||
## Current Status
|
||||
|
||||
### Completed
|
||||
- [x] **Phase 1: Foundation** - TutorialState, TutorialManager, TutorialStep/Sequence
|
||||
- [x] **Phase 2: UI** - Canvas-based modal with runtime construction
|
||||
- TutorialCanvasBuilder creates full Canvas UI at runtime (no prefab needed)
|
||||
- Fantasy RPG color scheme (dark purple panel, gold accents)
|
||||
- Title, description, icon, progress bar, buttons
|
||||
- IMGUI fallback still available if needed
|
||||
- [x] **Phase 3: Triggers** - Event-based trigger system with game controller hooks
|
||||
- [x] **Test setup** - TutorialTestSetup with welcome intro, battle, command tutorials
|
||||
- [x] **Settings integration** - Reset Tutorials button in Settings panel
|
||||
- [x] **Game entry trigger** - Welcome tutorial shows when entering a game (not lobby)
|
||||
- [x] **Font assignment** - Stoke-Regular-SDF assigned to TutorialUIManager.CanvasFont
|
||||
|
||||
### Future Work
|
||||
- [ ] **Lobby tutorial helper** - Separate tutorial for lobby/game selection
|
||||
- [ ] **Overlay system** - TutorialOverlayController for highlighting UI elements
|
||||
- [ ] **Hint indicators** - TutorialHintIndicator for subtle pulsing dots
|
||||
- [ ] **Real tutorial content** - Replace test tutorials with actual onboarding sequence
|
||||
- [ ] **More contextual triggers** - Diplomacy, hero recruitment, spells, terrain, etc.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
TutorialManager (Singleton, DontDestroyOnLoad)
|
||||
├── TutorialState (PlayerPrefs persistence)
|
||||
├── TutorialTriggerRegistry
|
||||
│ ├── OnboardingTriggers
|
||||
│ └── ContextualTriggers
|
||||
└── TutorialUIManager
|
||||
├── TutorialCanvasBuilder (runtime Canvas construction)
|
||||
├── TutorialModalPanel (wired up by builder)
|
||||
├── TutorialOverlayController (stub)
|
||||
├── TutorialHintIndicator (stub)
|
||||
└── IMGUI Fallback (working)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
Assets/Tutorial/
|
||||
├── TutorialManager.cs ✓ Singleton, coordinates everything
|
||||
├── TutorialState.cs ✓ PlayerPrefs persistence
|
||||
├── TutorialTestSetup.cs ✓ Test tutorials (welcome, battle, command)
|
||||
├── TUTORIAL_PLAN.md ✓ This document
|
||||
├── Content/
|
||||
│ ├── TutorialStep.cs ✓ Step data structure
|
||||
│ └── TutorialSequence.cs ✓ Sequence ScriptableObject
|
||||
├── Triggers/
|
||||
│ └── TutorialTriggerRegistry.cs ✓ Event routing
|
||||
└── UI/
|
||||
├── TutorialUIManager.cs ✓ UI coordination + auto-build Canvas
|
||||
├── TutorialCanvasBuilder.cs ✓ Runtime Canvas UI construction
|
||||
├── TutorialModalPanel.cs ✓ Canvas-based modal panel controller
|
||||
├── TutorialOverlayController.cs Stub (future)
|
||||
└── TutorialHintIndicator.cs Stub (future)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Unity Setup
|
||||
|
||||
### Required Inspector Assignments
|
||||
1. **TutorialUIManager.CanvasFont** → `Stoke-Regular-SDF` (TextMeshPro font)
|
||||
2. **TutorialUIManager.FallbackFont** → `Stoke-Regular` (for IMGUI backup)
|
||||
|
||||
### No Prefab Required
|
||||
The `TutorialCanvasBuilder` creates the entire Canvas UI hierarchy at runtime:
|
||||
- Canvas with ScreenSpaceOverlay, sorting order 1000
|
||||
- Modal blocker (dark semi-transparent background)
|
||||
- Panel with vertical layout (title, description, progress, buttons)
|
||||
- All buttons wired to TutorialModalPanel handlers
|
||||
|
||||
---
|
||||
|
||||
## Integration Points
|
||||
|
||||
### EagleGameController.cs
|
||||
- `SetUpGame()`: Calls `TutorialManager.Instance.Initialize(this, null)` → triggers "game_started" event
|
||||
- `SwapModel()`: Calls `OnModelUpdated()`
|
||||
- `ProvinceWasSelected()`: Calls `OnProvinceSelected()`
|
||||
- `PostCommittedCommand()`: Calls `OnCommandIssued()`
|
||||
|
||||
### ShardokGameController.cs
|
||||
- `SetUpGame()`: Calls `TutorialManager.Instance.Initialize(null, this)`
|
||||
- `ModelUpdated()`: Calls `OnBattleAction()`
|
||||
- `OnTurnEnded()`: Calls `OnTurnEnded()`
|
||||
- Unit selection: Calls `OnUnitSelected()`
|
||||
|
||||
### SettingsPanelController.cs
|
||||
- Reset Tutorials button: Calls `TutorialManager.Instance.ResetAllProgress()`
|
||||
|
||||
---
|
||||
|
||||
## Test Tutorials (Current)
|
||||
|
||||
| ID | Trigger Event | Title | When |
|
||||
|----|---------------|-------|------|
|
||||
| `test_intro` | `game_started` | Welcome to Eagle0! | Entering a game |
|
||||
| `test_first_battle` | `battle_entered` | Battle Begins! | First tactical battle |
|
||||
| `test_first_command` | `command_issued` | Command Issued! | First strategic command |
|
||||
|
||||
---
|
||||
|
||||
## Onboarding Flow (Planned)
|
||||
|
||||
| Step | Type | Content | Completion |
|
||||
|------|------|---------|------------|
|
||||
| 1 | Modal | Welcome to Eagle0 | Button click |
|
||||
| 2 | Overlay | Map overview - "Select a province" | Province selected |
|
||||
| 3 | Overlay | Province info panel explanation | Button click |
|
||||
| 4 | Overlay | Command buttons - "Try March" | March issued |
|
||||
| 5 | Modal | Turn cycle explanation | Button click |
|
||||
| 6 | Hidden | Wait for battle available | Model update |
|
||||
| 7 | Modal | Battle introduction | Button click |
|
||||
| 8 | Overlay | "Battle!" button highlight | Battle entered |
|
||||
| 9 | Modal | Tactical combat overview | Button click |
|
||||
| 10 | Overlay | Select and move a unit | Move issued |
|
||||
| 11 | Overlay | Attack an enemy | Attack issued |
|
||||
| 12 | Overlay | End Turn button | Turn ended |
|
||||
| 13 | Modal | Onboarding complete! | Button click |
|
||||
|
||||
---
|
||||
|
||||
## Contextual Tutorials (Planned)
|
||||
|
||||
### Strategic (First Encounter)
|
||||
| ID | Trigger | Display |
|
||||
|----|---------|---------|
|
||||
| `diplomacy_offer` | First diplomacy command available | Modal |
|
||||
| `hero_recruitment` | First recruitment available | Modal |
|
||||
| `province_riot` | First riot in owned province | Modal |
|
||||
| `weather_control` | First weather command available | Overlay |
|
||||
| `prisoner_capture` | First prisoner captured | Modal |
|
||||
|
||||
### Tactical (First Encounter/Attempt)
|
||||
| ID | Trigger | Display |
|
||||
|----|---------|---------|
|
||||
| `spell_lightning` | Lightning spell available | Tooltip |
|
||||
| `spell_meteor` | Meteor spell available | Modal |
|
||||
| `spell_holywave` | Holy Wave available | Tooltip |
|
||||
| `spell_raisedead` | Raise Dead available | Modal |
|
||||
| `terrain_fire` | Fire hex encountered | Tooltip |
|
||||
| `terrain_water` | Water adjacent to unit | Tooltip |
|
||||
| `ability_charge` | Charge command available | Overlay |
|
||||
| `flanking` | Flanking opportunity | Hint |
|
||||
|
||||
### Lobby (Future)
|
||||
| ID | Trigger | Display |
|
||||
|----|---------|---------|
|
||||
| `lobby_welcome` | First time in lobby | Modal |
|
||||
| `lobby_join_game` | Viewing game list | Overlay |
|
||||
| `lobby_create_game` | Create game button | Tooltip |
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
1. Ensure TutorialManager GameObject exists with:
|
||||
- TutorialManager component
|
||||
- TutorialUIManager component (assign fonts!)
|
||||
- TutorialTestSetup component
|
||||
2. Enable `Debug Logging` on TutorialManager for console output
|
||||
3. Play game → enter a game → see "Welcome to Eagle0!" modal
|
||||
4. Use Settings → Reset Tutorials to test again
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 69183de61f7d440a29b4a4270395a180
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -83,6 +83,9 @@ namespace Eagle0.Tutorial {
|
||||
public void Initialize(EagleGameController eagle, ShardokGameController shardok) {
|
||||
_triggerRegistry?.Initialize(eagle, shardok);
|
||||
Log($"TutorialManager initialized with controllers - Eagle: {eagle != null}, Shardok: {shardok != null}");
|
||||
|
||||
// Trigger game_started event when entering a game (not lobby)
|
||||
if (eagle != null) { _triggerRegistry?.OnGameEvent("game_started"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -100,7 +103,7 @@ namespace Eagle0.Tutorial {
|
||||
}
|
||||
|
||||
if (OnboardingSequence == null) {
|
||||
Debug.LogWarning("TutorialManager: No onboarding sequence assigned");
|
||||
Log("StartOnboarding: No onboarding sequence assigned, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Eagle0.Tutorial {
|
||||
/// <summary>
|
||||
/// Test component that sets up a simple tutorial for end-to-end testing.
|
||||
/// Attach to TutorialManager GameObject to register test tutorials on startup.
|
||||
/// Can be removed once actual tutorial content is created in the Editor.
|
||||
/// </summary>
|
||||
public class TutorialTestSetup : MonoBehaviour {
|
||||
[Header("Test Configuration")]
|
||||
[Tooltip("Enable test tutorials")]
|
||||
public bool EnableTestTutorials = true;
|
||||
|
||||
[Tooltip("Reset tutorial progress on startup (for testing)")]
|
||||
public bool ResetProgressOnStart = false;
|
||||
|
||||
private void Start() {
|
||||
if (!EnableTestTutorials) return;
|
||||
|
||||
var manager = TutorialManager.Instance;
|
||||
if (manager == null) {
|
||||
Debug.LogWarning("TutorialTestSetup: TutorialManager not found");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ResetProgressOnStart) { manager.ResetAllProgress(); }
|
||||
|
||||
RegisterTestTutorials(manager);
|
||||
Debug.Log("TutorialTestSetup: Test tutorials registered");
|
||||
}
|
||||
|
||||
private void RegisterTestTutorials(TutorialManager manager) {
|
||||
// Create welcome/intro tutorial that shows on game start
|
||||
var introTutorial = CreateIntroTutorial();
|
||||
manager.TriggerRegistry.RegisterTutorial(introTutorial, "game_started");
|
||||
|
||||
// Create a simple "first battle" tutorial
|
||||
var firstBattleTutorial = CreateFirstBattleTutorial();
|
||||
manager.TriggerRegistry.RegisterTutorial(firstBattleTutorial, "battle_entered");
|
||||
|
||||
// Create a simple "first command" tutorial
|
||||
var firstCommandTutorial = CreateFirstCommandTutorial();
|
||||
manager.TriggerRegistry.RegisterTutorial(firstCommandTutorial, "command_issued");
|
||||
|
||||
// Note: "game_started" event is triggered by EagleGameController.SetUpGame()
|
||||
// via TutorialManager.Initialize(), not here
|
||||
}
|
||||
|
||||
private TutorialSequence CreateIntroTutorial() {
|
||||
var sequence = ScriptableObject.CreateInstance<TutorialSequence>();
|
||||
sequence.SequenceId = "test_intro";
|
||||
sequence.DisplayName = "Welcome";
|
||||
|
||||
sequence.Steps.Add(new TutorialStep {
|
||||
StepId = "welcome",
|
||||
Title = "Welcome to Eagle0!",
|
||||
Description =
|
||||
"Eagle0 is a strategy game where you command armies, manage provinces, and engage in tactical combat.\n\nClick on provinces to view their details and issue commands. When battles occur, you'll fight them out on a hex-based tactical map.\n\nGood luck, Commander!",
|
||||
DisplayMode = TutorialDisplayMode.Modal,
|
||||
CompletionType = TutorialCompletionType.ButtonClick,
|
||||
AllowSkip = false
|
||||
});
|
||||
|
||||
return sequence;
|
||||
}
|
||||
|
||||
private TutorialSequence CreateFirstBattleTutorial() {
|
||||
var sequence = ScriptableObject.CreateInstance<TutorialSequence>();
|
||||
sequence.SequenceId = "test_first_battle";
|
||||
sequence.DisplayName = "First Battle";
|
||||
|
||||
sequence.Steps.Add(new TutorialStep {
|
||||
StepId = "battle_intro",
|
||||
Title = "Battle Begins!",
|
||||
Description =
|
||||
"You've entered tactical combat. Command your units on the hex grid to defeat the enemy.\n\n(This is a test tutorial to verify the tutorial system is working.)",
|
||||
DisplayMode = TutorialDisplayMode.Modal,
|
||||
CompletionType = TutorialCompletionType.ButtonClick,
|
||||
AllowSkip = true
|
||||
});
|
||||
|
||||
return sequence;
|
||||
}
|
||||
|
||||
private TutorialSequence CreateFirstCommandTutorial() {
|
||||
var sequence = ScriptableObject.CreateInstance<TutorialSequence>();
|
||||
sequence.SequenceId = "test_first_command";
|
||||
sequence.DisplayName = "First Command";
|
||||
|
||||
sequence.Steps.Add(new TutorialStep {
|
||||
StepId = "command_issued_intro",
|
||||
Title = "Command Issued!",
|
||||
Description =
|
||||
"You've issued your first command. Commands are processed at the end of each turn.\n\n(This is a test tutorial to verify the tutorial system is working.)",
|
||||
DisplayMode = TutorialDisplayMode.Modal,
|
||||
CompletionType = TutorialCompletionType.ButtonClick,
|
||||
AllowSkip = true
|
||||
});
|
||||
|
||||
return sequence;
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 370b3269832442e196d1b432bdae2f1e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+475
@@ -0,0 +1,475 @@
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Eagle0.Tutorial {
|
||||
/// <summary>
|
||||
/// Builds the tutorial Canvas UI programmatically at runtime.
|
||||
/// Creates a complete TutorialModalPanel with all required UI elements.
|
||||
/// </summary>
|
||||
public static class TutorialCanvasBuilder {
|
||||
// Colors matching the game's fantasy RPG style
|
||||
private static readonly Color PanelBackgroundColor = new Color(0.15f, 0.12f, 0.2f, 0.95f);
|
||||
private static readonly Color BorderColor = new Color(0.6f, 0.5f, 0.3f, 1f);
|
||||
private static readonly Color TitleColor = new Color(1f, 0.9f, 0.7f, 1f);
|
||||
private static readonly Color TextColor = new Color(0.9f, 0.85f, 0.8f, 1f);
|
||||
private static readonly Color ButtonColor = new Color(0.4f, 0.3f, 0.2f, 1f);
|
||||
private static readonly Color ButtonHoverColor = new Color(0.5f, 0.4f, 0.25f, 1f);
|
||||
private static readonly Color ButtonTextColor = new Color(1f, 0.95f, 0.85f, 1f);
|
||||
private static readonly Color ModalBlockerColor = new Color(0f, 0f, 0f, 0.75f);
|
||||
private static readonly Color ProgressBarColor = new Color(0.6f, 0.5f, 0.3f, 1f);
|
||||
private static readonly Color ProgressBackgroundColor = new Color(0.2f, 0.15f, 0.1f, 1f);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a complete tutorial Canvas with modal panel.
|
||||
/// Returns the Canvas GameObject and sets up the TutorialModalPanel component.
|
||||
/// </summary>
|
||||
/// <param name="font">TextMeshPro font to use (assign Stoke-Regular-SDF in
|
||||
/// inspector)</param>
|
||||
public static Canvas BuildTutorialCanvas(TMP_FontAsset font) {
|
||||
// Create Canvas
|
||||
GameObject canvasObj = new GameObject("TutorialCanvas");
|
||||
Canvas canvas = canvasObj.AddComponent<Canvas>();
|
||||
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
canvas.sortingOrder = 1000;
|
||||
|
||||
CanvasScaler scaler = canvasObj.AddComponent<CanvasScaler>();
|
||||
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||
scaler.referenceResolution = new Vector2(1920, 1080);
|
||||
scaler.matchWidthOrHeight = 0.5f;
|
||||
|
||||
canvasObj.AddComponent<GraphicRaycaster>();
|
||||
|
||||
// Create Modal Blocker
|
||||
GameObject blockerObj = CreateModalBlocker(canvasObj.transform);
|
||||
|
||||
// Create Panel Container
|
||||
GameObject panelContainer = CreatePanelContainer(canvasObj.transform, font);
|
||||
|
||||
// Add TutorialModalPanel component and wire it up
|
||||
TutorialModalPanel modalPanel = canvasObj.AddComponent<TutorialModalPanel>();
|
||||
WireUpModalPanel(modalPanel, blockerObj, panelContainer, font);
|
||||
|
||||
// Set up button listeners (Awake runs before fields are wired, so do it here)
|
||||
SetupButtonListeners(modalPanel);
|
||||
|
||||
// Start with everything hidden
|
||||
blockerObj.SetActive(false);
|
||||
panelContainer.SetActive(false);
|
||||
|
||||
return canvas;
|
||||
}
|
||||
|
||||
private static GameObject CreateModalBlocker(Transform parent) {
|
||||
GameObject blocker = new GameObject("ModalBlocker");
|
||||
blocker.transform.SetParent(parent, false);
|
||||
|
||||
RectTransform rect = blocker.AddComponent<RectTransform>();
|
||||
rect.anchorMin = Vector2.zero;
|
||||
rect.anchorMax = Vector2.one;
|
||||
rect.offsetMin = Vector2.zero;
|
||||
rect.offsetMax = Vector2.zero;
|
||||
|
||||
Image image = blocker.AddComponent<Image>();
|
||||
image.color = ModalBlockerColor;
|
||||
image.raycastTarget = true;
|
||||
|
||||
return blocker;
|
||||
}
|
||||
|
||||
private static GameObject CreatePanelContainer(Transform parent, TMP_FontAsset font) {
|
||||
// Panel Container (centered)
|
||||
GameObject panel = new GameObject("PanelContainer");
|
||||
panel.transform.SetParent(parent, false);
|
||||
|
||||
RectTransform panelRect = panel.AddComponent<RectTransform>();
|
||||
panelRect.anchorMin = new Vector2(0.5f, 0.5f);
|
||||
panelRect.anchorMax = new Vector2(0.5f, 0.5f);
|
||||
panelRect.sizeDelta = new Vector2(800, 550);
|
||||
panelRect.anchoredPosition = Vector2.zero;
|
||||
|
||||
// Panel Background
|
||||
Image panelBg = panel.AddComponent<Image>();
|
||||
panelBg.color = PanelBackgroundColor;
|
||||
panelBg.raycastTarget = true;
|
||||
|
||||
// Add outline/border effect
|
||||
Outline outline = panel.AddComponent<Outline>();
|
||||
outline.effectColor = BorderColor;
|
||||
outline.effectDistance = new Vector2(3, 3);
|
||||
|
||||
// Add vertical layout group
|
||||
VerticalLayoutGroup layout = panel.AddComponent<VerticalLayoutGroup>();
|
||||
layout.padding = new RectOffset(40, 40, 30, 30);
|
||||
layout.spacing = 20;
|
||||
layout.childAlignment = TextAnchor.UpperCenter;
|
||||
layout.childControlHeight = false;
|
||||
layout.childControlWidth = true;
|
||||
layout.childForceExpandHeight = false;
|
||||
layout.childForceExpandWidth = true;
|
||||
|
||||
// Title
|
||||
CreateTitle(panel.transform, font);
|
||||
|
||||
// Icon (placeholder - starts hidden)
|
||||
CreateIcon(panel.transform);
|
||||
|
||||
// Description
|
||||
CreateDescription(panel.transform, font);
|
||||
|
||||
// Flexible spacer to push buttons to bottom
|
||||
CreateFlexibleSpacer(panel.transform);
|
||||
|
||||
// Progress section
|
||||
CreateProgressSection(panel.transform, font);
|
||||
|
||||
// Button row
|
||||
CreateButtonRow(panel.transform, font);
|
||||
|
||||
return panel;
|
||||
}
|
||||
|
||||
private static void CreateTitle(Transform parent, TMP_FontAsset font) {
|
||||
GameObject titleObj = new GameObject("TitleText");
|
||||
titleObj.transform.SetParent(parent, false);
|
||||
|
||||
RectTransform rect = titleObj.AddComponent<RectTransform>();
|
||||
rect.sizeDelta = new Vector2(720, 60);
|
||||
|
||||
TextMeshProUGUI text = titleObj.AddComponent<TextMeshProUGUI>();
|
||||
text.text = "Tutorial";
|
||||
text.fontSize = 42;
|
||||
text.fontStyle = FontStyles.Bold;
|
||||
text.color = TitleColor;
|
||||
text.alignment = TextAlignmentOptions.Center;
|
||||
if (font != null) text.font = font;
|
||||
|
||||
LayoutElement layoutElement = titleObj.AddComponent<LayoutElement>();
|
||||
layoutElement.preferredHeight = 60;
|
||||
}
|
||||
|
||||
private static void CreateIcon(Transform parent) {
|
||||
GameObject iconObj = new GameObject("IconImage");
|
||||
iconObj.transform.SetParent(parent, false);
|
||||
|
||||
RectTransform rect = iconObj.AddComponent<RectTransform>();
|
||||
rect.sizeDelta = new Vector2(100, 100);
|
||||
|
||||
Image image = iconObj.AddComponent<Image>();
|
||||
image.color = Color.white;
|
||||
image.preserveAspect = true;
|
||||
|
||||
LayoutElement layoutElement = iconObj.AddComponent<LayoutElement>();
|
||||
layoutElement.preferredHeight = 100;
|
||||
layoutElement.preferredWidth = 100;
|
||||
|
||||
// Start hidden
|
||||
iconObj.SetActive(false);
|
||||
}
|
||||
|
||||
private static void CreateDescription(Transform parent, TMP_FontAsset font) {
|
||||
GameObject descObj = new GameObject("DescriptionText");
|
||||
descObj.transform.SetParent(parent, false);
|
||||
|
||||
RectTransform rect = descObj.AddComponent<RectTransform>();
|
||||
rect.sizeDelta = new Vector2(720, 200);
|
||||
|
||||
TextMeshProUGUI text = descObj.AddComponent<TextMeshProUGUI>();
|
||||
text.text = "";
|
||||
text.fontSize = 26;
|
||||
text.color = TextColor;
|
||||
text.alignment = TextAlignmentOptions.TopLeft;
|
||||
text.enableWordWrapping = true;
|
||||
text.overflowMode = TextOverflowModes.Overflow; // Don't truncate
|
||||
if (font != null) text.font = font;
|
||||
|
||||
LayoutElement layoutElement = descObj.AddComponent<LayoutElement>();
|
||||
layoutElement.preferredHeight = 200;
|
||||
layoutElement.minHeight = 100;
|
||||
}
|
||||
|
||||
private static void CreateSpacer(Transform parent, float height) {
|
||||
GameObject spacer = new GameObject("Spacer");
|
||||
spacer.transform.SetParent(parent, false);
|
||||
|
||||
spacer.AddComponent<RectTransform>();
|
||||
|
||||
LayoutElement layoutElement = spacer.AddComponent<LayoutElement>();
|
||||
layoutElement.preferredHeight = height;
|
||||
}
|
||||
|
||||
private static void CreateFlexibleSpacer(Transform parent) {
|
||||
GameObject spacer = new GameObject("FlexibleSpacer");
|
||||
spacer.transform.SetParent(parent, false);
|
||||
|
||||
spacer.AddComponent<RectTransform>();
|
||||
|
||||
LayoutElement layoutElement = spacer.AddComponent<LayoutElement>();
|
||||
layoutElement.flexibleHeight = 1;
|
||||
}
|
||||
|
||||
private static void CreateProgressSection(Transform parent, TMP_FontAsset font) {
|
||||
GameObject progressSection = new GameObject("ProgressSection");
|
||||
progressSection.transform.SetParent(parent, false);
|
||||
|
||||
RectTransform rect = progressSection.AddComponent<RectTransform>();
|
||||
rect.sizeDelta = new Vector2(720, 50);
|
||||
|
||||
VerticalLayoutGroup layout = progressSection.AddComponent<VerticalLayoutGroup>();
|
||||
layout.spacing = 8;
|
||||
layout.childAlignment = TextAnchor.MiddleCenter;
|
||||
layout.childControlHeight = false;
|
||||
layout.childControlWidth = true;
|
||||
|
||||
LayoutElement layoutElement = progressSection.AddComponent<LayoutElement>();
|
||||
layoutElement.preferredHeight = 50;
|
||||
|
||||
// Progress Text
|
||||
GameObject progressTextObj = new GameObject("ProgressText");
|
||||
progressTextObj.transform.SetParent(progressSection.transform, false);
|
||||
|
||||
RectTransform textRect = progressTextObj.AddComponent<RectTransform>();
|
||||
textRect.sizeDelta = new Vector2(720, 24);
|
||||
|
||||
TextMeshProUGUI progressText = progressTextObj.AddComponent<TextMeshProUGUI>();
|
||||
progressText.text = "Step 1 of 1";
|
||||
progressText.fontSize = 20;
|
||||
progressText.color = TextColor;
|
||||
progressText.alignment = TextAlignmentOptions.Center;
|
||||
if (font != null) progressText.font = font;
|
||||
|
||||
LayoutElement textLayout = progressTextObj.AddComponent<LayoutElement>();
|
||||
textLayout.preferredHeight = 24;
|
||||
|
||||
// Progress Slider
|
||||
CreateProgressSlider(progressSection.transform);
|
||||
}
|
||||
|
||||
private static void CreateProgressSlider(Transform parent) {
|
||||
GameObject sliderObj = new GameObject("ProgressSlider");
|
||||
sliderObj.transform.SetParent(parent, false);
|
||||
|
||||
RectTransform sliderRect = sliderObj.AddComponent<RectTransform>();
|
||||
sliderRect.sizeDelta = new Vector2(400, 12);
|
||||
|
||||
Slider slider = sliderObj.AddComponent<Slider>();
|
||||
slider.minValue = 0;
|
||||
slider.maxValue = 1;
|
||||
slider.value = 0;
|
||||
slider.interactable = false;
|
||||
|
||||
LayoutElement sliderLayout = sliderObj.AddComponent<LayoutElement>();
|
||||
sliderLayout.preferredHeight = 12;
|
||||
sliderLayout.preferredWidth = 400;
|
||||
|
||||
// Background
|
||||
GameObject bgObj = new GameObject("Background");
|
||||
bgObj.transform.SetParent(sliderObj.transform, false);
|
||||
|
||||
RectTransform bgRect = bgObj.AddComponent<RectTransform>();
|
||||
bgRect.anchorMin = Vector2.zero;
|
||||
bgRect.anchorMax = Vector2.one;
|
||||
bgRect.offsetMin = Vector2.zero;
|
||||
bgRect.offsetMax = Vector2.zero;
|
||||
|
||||
Image bgImage = bgObj.AddComponent<Image>();
|
||||
bgImage.color = ProgressBackgroundColor;
|
||||
|
||||
// Fill Area
|
||||
GameObject fillArea = new GameObject("Fill Area");
|
||||
fillArea.transform.SetParent(sliderObj.transform, false);
|
||||
|
||||
RectTransform fillAreaRect = fillArea.AddComponent<RectTransform>();
|
||||
fillAreaRect.anchorMin = Vector2.zero;
|
||||
fillAreaRect.anchorMax = Vector2.one;
|
||||
fillAreaRect.offsetMin = Vector2.zero;
|
||||
fillAreaRect.offsetMax = Vector2.zero;
|
||||
|
||||
// Fill
|
||||
GameObject fill = new GameObject("Fill");
|
||||
fill.transform.SetParent(fillArea.transform, false);
|
||||
|
||||
RectTransform fillRect = fill.AddComponent<RectTransform>();
|
||||
fillRect.anchorMin = Vector2.zero;
|
||||
fillRect.anchorMax = new Vector2(0, 1);
|
||||
fillRect.offsetMin = Vector2.zero;
|
||||
fillRect.offsetMax = Vector2.zero;
|
||||
|
||||
Image fillImage = fill.AddComponent<Image>();
|
||||
fillImage.color = ProgressBarColor;
|
||||
|
||||
// Wire up slider
|
||||
slider.fillRect = fillRect;
|
||||
}
|
||||
|
||||
private static void SetupButtonListeners(TutorialModalPanel panel) {
|
||||
// Button listeners need to be set up after wiring since Awake runs before fields exist
|
||||
if (panel.ContinueButton != null) {
|
||||
panel.ContinueButton.onClick.RemoveAllListeners();
|
||||
panel.ContinueButton.onClick.AddListener(panel.OnContinueClicked);
|
||||
}
|
||||
if (panel.SkipButton != null) {
|
||||
panel.SkipButton.onClick.RemoveAllListeners();
|
||||
panel.SkipButton.onClick.AddListener(panel.OnSkipClicked);
|
||||
}
|
||||
if (panel.SkipAllButton != null) {
|
||||
panel.SkipAllButton.onClick.RemoveAllListeners();
|
||||
panel.SkipAllButton.onClick.AddListener(panel.OnSkipAllClicked);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CreateButtonRow(Transform parent, TMP_FontAsset font) {
|
||||
GameObject buttonRow = new GameObject("ButtonRow");
|
||||
buttonRow.transform.SetParent(parent, false);
|
||||
|
||||
RectTransform rect = buttonRow.AddComponent<RectTransform>();
|
||||
rect.sizeDelta = new Vector2(720, 60);
|
||||
|
||||
HorizontalLayoutGroup layout = buttonRow.AddComponent<HorizontalLayoutGroup>();
|
||||
layout.spacing = 20;
|
||||
layout.childAlignment = TextAnchor.MiddleCenter;
|
||||
layout.childControlHeight = false;
|
||||
layout.childControlWidth = false;
|
||||
layout.childForceExpandHeight = false;
|
||||
layout.childForceExpandWidth = false;
|
||||
|
||||
LayoutElement layoutElement = buttonRow.AddComponent<LayoutElement>();
|
||||
layoutElement.preferredHeight = 60;
|
||||
|
||||
// Skip Button (left)
|
||||
CreateButton(buttonRow.transform, "SkipButton", "Skip", 150, 50, font);
|
||||
|
||||
// Skip All Button
|
||||
CreateButton(buttonRow.transform, "SkipAllButton", "Skip All", 150, 50, font);
|
||||
|
||||
// Spacer
|
||||
GameObject spacer = new GameObject("ButtonSpacer");
|
||||
spacer.transform.SetParent(buttonRow.transform, false);
|
||||
spacer.AddComponent<RectTransform>();
|
||||
LayoutElement spacerLayout = spacer.AddComponent<LayoutElement>();
|
||||
spacerLayout.flexibleWidth = 1;
|
||||
|
||||
// Continue Button (right)
|
||||
CreateButton(buttonRow.transform, "ContinueButton", "Continue", 180, 50, font);
|
||||
}
|
||||
|
||||
private static GameObject CreateButton(
|
||||
Transform parent,
|
||||
string name,
|
||||
string text,
|
||||
float width,
|
||||
float height,
|
||||
TMP_FontAsset font) {
|
||||
GameObject buttonObj = new GameObject(name);
|
||||
buttonObj.transform.SetParent(parent, false);
|
||||
|
||||
RectTransform rect = buttonObj.AddComponent<RectTransform>();
|
||||
rect.sizeDelta = new Vector2(width, height);
|
||||
|
||||
Image image = buttonObj.AddComponent<Image>();
|
||||
image.color = ButtonColor;
|
||||
|
||||
Button button = buttonObj.AddComponent<Button>();
|
||||
ColorBlock colors = button.colors;
|
||||
colors.normalColor = ButtonColor;
|
||||
colors.highlightedColor = ButtonHoverColor;
|
||||
colors.pressedColor = ButtonHoverColor;
|
||||
colors.selectedColor = ButtonHoverColor;
|
||||
button.colors = colors;
|
||||
|
||||
// Add outline
|
||||
Outline outline = buttonObj.AddComponent<Outline>();
|
||||
outline.effectColor = BorderColor;
|
||||
outline.effectDistance = new Vector2(2, 2);
|
||||
|
||||
LayoutElement layoutElement = buttonObj.AddComponent<LayoutElement>();
|
||||
layoutElement.preferredWidth = width;
|
||||
layoutElement.preferredHeight = height;
|
||||
|
||||
// Button text
|
||||
GameObject textObj = new GameObject("Text");
|
||||
textObj.transform.SetParent(buttonObj.transform, false);
|
||||
|
||||
RectTransform textRect = textObj.AddComponent<RectTransform>();
|
||||
textRect.anchorMin = Vector2.zero;
|
||||
textRect.anchorMax = Vector2.one;
|
||||
textRect.offsetMin = Vector2.zero;
|
||||
textRect.offsetMax = Vector2.zero;
|
||||
|
||||
TextMeshProUGUI buttonText = textObj.AddComponent<TextMeshProUGUI>();
|
||||
buttonText.text = text;
|
||||
buttonText.fontSize = 24;
|
||||
buttonText.color = ButtonTextColor;
|
||||
buttonText.alignment = TextAlignmentOptions.Center;
|
||||
if (font != null) buttonText.font = font;
|
||||
|
||||
return buttonObj;
|
||||
}
|
||||
|
||||
private static void WireUpModalPanel(
|
||||
TutorialModalPanel panel,
|
||||
GameObject blocker,
|
||||
GameObject panelContainer,
|
||||
TMP_FontAsset font) {
|
||||
panel.ModalBlocker = blocker;
|
||||
panel.PanelContainer = panelContainer;
|
||||
|
||||
// Find and wire up components
|
||||
Transform containerTransform = panelContainer.transform;
|
||||
|
||||
// Title
|
||||
Transform titleTransform = containerTransform.Find("TitleText");
|
||||
if (titleTransform != null) {
|
||||
panel.TitleText = titleTransform.GetComponent<TextMeshProUGUI>();
|
||||
}
|
||||
|
||||
// Icon
|
||||
Transform iconTransform = containerTransform.Find("IconImage");
|
||||
if (iconTransform != null) { panel.IconImage = iconTransform.GetComponent<Image>(); }
|
||||
|
||||
// Description
|
||||
Transform descTransform = containerTransform.Find("DescriptionText");
|
||||
if (descTransform != null) {
|
||||
panel.DescriptionText = descTransform.GetComponent<TextMeshProUGUI>();
|
||||
}
|
||||
|
||||
// Progress Section
|
||||
Transform progressSection = containerTransform.Find("ProgressSection");
|
||||
if (progressSection != null) {
|
||||
Transform progressTextTransform = progressSection.Find("ProgressText");
|
||||
if (progressTextTransform != null) {
|
||||
panel.ProgressText = progressTextTransform.GetComponent<TextMeshProUGUI>();
|
||||
}
|
||||
|
||||
Transform sliderTransform = progressSection.Find("ProgressSlider");
|
||||
if (sliderTransform != null) {
|
||||
panel.ProgressSlider = sliderTransform.GetComponent<Slider>();
|
||||
}
|
||||
}
|
||||
|
||||
// Button Row
|
||||
Transform buttonRow = containerTransform.Find("ButtonRow");
|
||||
if (buttonRow != null) {
|
||||
Transform skipTransform = buttonRow.Find("SkipButton");
|
||||
if (skipTransform != null) {
|
||||
panel.SkipButton = skipTransform.GetComponent<Button>();
|
||||
}
|
||||
|
||||
Transform skipAllTransform = buttonRow.Find("SkipAllButton");
|
||||
if (skipAllTransform != null) {
|
||||
panel.SkipAllButton = skipAllTransform.GetComponent<Button>();
|
||||
}
|
||||
|
||||
Transform continueTransform = buttonRow.Find("ContinueButton");
|
||||
if (continueTransform != null) {
|
||||
panel.ContinueButton = continueTransform.GetComponent<Button>();
|
||||
Transform continueTextTransform = continueTransform.Find("Text");
|
||||
if (continueTextTransform != null) {
|
||||
panel.ContinueButtonText =
|
||||
continueTextTransform.GetComponent<TextMeshProUGUI>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 98467fdbaf584fbb928a373340b542d7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+15
-3
@@ -135,19 +135,31 @@ namespace Eagle0.Tutorial {
|
||||
_onSkip = null;
|
||||
}
|
||||
|
||||
private void OnContinueClicked() {
|
||||
/// <summary>
|
||||
/// Called when Continue button is clicked.
|
||||
/// Public to allow external setup of button listeners.
|
||||
/// </summary>
|
||||
public void OnContinueClicked() {
|
||||
var callback = _onContinue;
|
||||
Hide();
|
||||
callback?.Invoke();
|
||||
}
|
||||
|
||||
private void OnSkipClicked() {
|
||||
/// <summary>
|
||||
/// Called when Skip button is clicked.
|
||||
/// Public to allow external setup of button listeners.
|
||||
/// </summary>
|
||||
public void OnSkipClicked() {
|
||||
var callback = _onSkip;
|
||||
Hide();
|
||||
callback?.Invoke();
|
||||
}
|
||||
|
||||
private void OnSkipAllClicked() {
|
||||
/// <summary>
|
||||
/// Called when Skip All button is clicked.
|
||||
/// Public to allow external setup of button listeners.
|
||||
/// </summary>
|
||||
public void OnSkipAllClicked() {
|
||||
Hide();
|
||||
TutorialManager.Instance?.SkipAllOnboarding();
|
||||
}
|
||||
|
||||
+184
-5
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Eagle0.Tutorial {
|
||||
@@ -22,17 +23,189 @@ namespace Eagle0.Tutorial {
|
||||
[Tooltip("Canvas for tutorial UI (should be above game UI)")]
|
||||
public Canvas TutorialCanvas;
|
||||
|
||||
[Header("Fonts")]
|
||||
[Tooltip("TextMeshPro font for Canvas UI (assign Stoke-Regular-SDF)")]
|
||||
public TMP_FontAsset CanvasFont;
|
||||
|
||||
[Header("Fallback UI")]
|
||||
[Tooltip("Use IMGUI fallback when Canvas UI cannot be built")]
|
||||
public bool UseFallbackUI = true;
|
||||
|
||||
[Tooltip("Font for IMGUI fallback (assign Stoke-Regular)")]
|
||||
public Font FallbackFont;
|
||||
|
||||
[Header("Runtime Options")]
|
||||
[Tooltip("Auto-build Canvas UI at runtime if ModalPanel is not assigned")]
|
||||
public bool AutoBuildCanvasUI = true;
|
||||
|
||||
// Active hints
|
||||
private Dictionary<string, TutorialHintIndicator> _activeHints =
|
||||
new Dictionary<string, TutorialHintIndicator>();
|
||||
|
||||
// Fallback modal state
|
||||
private bool _fallbackModalVisible;
|
||||
private TutorialStep _fallbackStep;
|
||||
private Action _fallbackOnContinue;
|
||||
private Action _fallbackOnSkip;
|
||||
private int _fallbackCurrentIndex;
|
||||
private int _fallbackTotalSteps;
|
||||
private bool _fallbackIsOnboarding;
|
||||
|
||||
private void Awake() {
|
||||
// Auto-build Canvas UI if needed
|
||||
if (ModalPanel == null && AutoBuildCanvasUI) { BuildCanvasUI(); }
|
||||
|
||||
// Ensure canvas is set up correctly
|
||||
if (TutorialCanvas != null) {
|
||||
TutorialCanvas.sortingOrder = 1000; // Above most game UI
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the Canvas-based tutorial UI at runtime.
|
||||
/// Called automatically if AutoBuildCanvasUI is true and ModalPanel is not assigned.
|
||||
/// </summary>
|
||||
private void BuildCanvasUI() {
|
||||
TutorialCanvas = TutorialCanvasBuilder.BuildTutorialCanvas(CanvasFont);
|
||||
if (TutorialCanvas != null) {
|
||||
// Make canvas a child of this object so it persists with TutorialManager
|
||||
TutorialCanvas.transform.SetParent(transform, false);
|
||||
|
||||
// Get the modal panel that was created
|
||||
ModalPanel = TutorialCanvas.GetComponent<TutorialModalPanel>();
|
||||
|
||||
Debug.Log("TutorialUIManager: Built Canvas UI at runtime");
|
||||
} else {
|
||||
Debug.LogWarning("TutorialUIManager: Failed to build Canvas UI");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnGUI() {
|
||||
if (!_fallbackModalVisible || _fallbackStep == null) return;
|
||||
|
||||
// Semi-transparent background
|
||||
GUI.color = new Color(0, 0, 0, 0.7f);
|
||||
GUI.DrawTexture(new Rect(0, 0, Screen.width, Screen.height), Texture2D.whiteTexture);
|
||||
GUI.color = Color.white;
|
||||
|
||||
// Modal window
|
||||
float windowWidth = Mathf.Min(1200, Screen.width - 40);
|
||||
float windowHeight = 600;
|
||||
float windowX = (Screen.width - windowWidth) / 2;
|
||||
float windowY = (Screen.height - windowHeight) / 2;
|
||||
|
||||
GUIStyle windowStyle = new GUIStyle(GUI.skin.window);
|
||||
windowStyle.fontSize = 48;
|
||||
if (FallbackFont != null) windowStyle.font = FallbackFont;
|
||||
|
||||
GUI.Window(
|
||||
12345,
|
||||
new Rect(windowX, windowY, windowWidth, windowHeight),
|
||||
DrawFallbackModal,
|
||||
_fallbackStep.Title ?? "Tutorial",
|
||||
windowStyle);
|
||||
}
|
||||
|
||||
private void DrawFallbackModal(int windowId) {
|
||||
GUILayout.Space(60);
|
||||
|
||||
// Description
|
||||
GUIStyle descStyle = new GUIStyle(GUI.skin.label);
|
||||
descStyle.wordWrap = true;
|
||||
descStyle.fontSize = 40;
|
||||
if (FallbackFont != null) descStyle.font = FallbackFont;
|
||||
GUILayout.Label(
|
||||
_fallbackStep.Description ?? "",
|
||||
descStyle,
|
||||
GUILayout.ExpandHeight(true));
|
||||
|
||||
GUILayout.FlexibleSpace();
|
||||
|
||||
// Progress
|
||||
if (_fallbackTotalSteps > 1) {
|
||||
GUIStyle progressStyle = new GUIStyle(GUI.skin.label);
|
||||
progressStyle.fontSize = 32;
|
||||
if (FallbackFont != null) progressStyle.font = FallbackFont;
|
||||
GUILayout.Label(
|
||||
$"Step {_fallbackCurrentIndex + 1} of {_fallbackTotalSteps}",
|
||||
progressStyle,
|
||||
GUILayout.ExpandWidth(true));
|
||||
}
|
||||
|
||||
GUILayout.Space(30);
|
||||
|
||||
// Button style
|
||||
GUIStyle buttonStyle = new GUIStyle(GUI.skin.button);
|
||||
buttonStyle.fontSize = 36;
|
||||
if (FallbackFont != null) buttonStyle.font = FallbackFont;
|
||||
|
||||
// Buttons
|
||||
GUILayout.BeginHorizontal();
|
||||
|
||||
if (_fallbackStep.AllowSkip) {
|
||||
if (GUILayout.Button(
|
||||
"Skip",
|
||||
buttonStyle,
|
||||
GUILayout.Width(200),
|
||||
GUILayout.Height(80))) {
|
||||
var callback = _fallbackOnSkip;
|
||||
HideFallbackModal();
|
||||
callback?.Invoke();
|
||||
}
|
||||
|
||||
if (_fallbackIsOnboarding) {
|
||||
if (GUILayout.Button(
|
||||
"Skip All",
|
||||
buttonStyle,
|
||||
GUILayout.Width(200),
|
||||
GUILayout.Height(80))) {
|
||||
HideFallbackModal();
|
||||
TutorialManager.Instance?.SkipAllOnboarding();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GUILayout.FlexibleSpace();
|
||||
|
||||
string buttonText =
|
||||
_fallbackCurrentIndex >= _fallbackTotalSteps - 1 ? "Got it!" : "Continue";
|
||||
if (GUILayout.Button(
|
||||
buttonText,
|
||||
buttonStyle,
|
||||
GUILayout.Width(240),
|
||||
GUILayout.Height(80))) {
|
||||
var callback = _fallbackOnContinue;
|
||||
HideFallbackModal();
|
||||
callback?.Invoke();
|
||||
}
|
||||
|
||||
GUILayout.EndHorizontal();
|
||||
GUILayout.Space(30);
|
||||
}
|
||||
|
||||
private void ShowFallbackModal(
|
||||
TutorialStep step,
|
||||
int currentIndex,
|
||||
int totalSteps,
|
||||
Action onContinue,
|
||||
Action onSkip,
|
||||
bool isOnboarding) {
|
||||
_fallbackStep = step;
|
||||
_fallbackCurrentIndex = currentIndex;
|
||||
_fallbackTotalSteps = totalSteps;
|
||||
_fallbackOnContinue = onContinue;
|
||||
_fallbackOnSkip = onSkip;
|
||||
_fallbackIsOnboarding = isOnboarding;
|
||||
_fallbackModalVisible = true;
|
||||
}
|
||||
|
||||
private void HideFallbackModal() {
|
||||
_fallbackModalVisible = false;
|
||||
_fallbackStep = null;
|
||||
_fallbackOnContinue = null;
|
||||
_fallbackOnSkip = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows a modal dialog for a tutorial step.
|
||||
/// </summary>
|
||||
@@ -43,13 +216,18 @@ namespace Eagle0.Tutorial {
|
||||
Action onComplete,
|
||||
Action onSkip,
|
||||
bool isOnboarding) {
|
||||
if (ModalPanel == null) {
|
||||
Debug.LogWarning("TutorialUIManager: No ModalPanel assigned");
|
||||
if (ModalPanel != null) {
|
||||
// Ensure canvas is active
|
||||
if (TutorialCanvas != null && !TutorialCanvas.gameObject.activeSelf) {
|
||||
TutorialCanvas.gameObject.SetActive(true);
|
||||
}
|
||||
ModalPanel.Show(step, currentIndex, totalSteps, onComplete, onSkip, isOnboarding);
|
||||
} else if (UseFallbackUI) {
|
||||
ShowFallbackModal(step, currentIndex, totalSteps, onComplete, onSkip, isOnboarding);
|
||||
} else {
|
||||
Debug.LogWarning("TutorialUIManager: No ModalPanel assigned and fallback disabled");
|
||||
onComplete?.Invoke();
|
||||
return;
|
||||
}
|
||||
|
||||
ModalPanel.Show(step, currentIndex, totalSteps, onComplete, onSkip, isOnboarding);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -148,6 +326,7 @@ namespace Eagle0.Tutorial {
|
||||
public void HideAll() {
|
||||
ModalPanel?.Hide();
|
||||
OverlayController?.HideOverlay();
|
||||
HideFallbackModal();
|
||||
// Note: Don't hide hints automatically - they persist until dismissed
|
||||
}
|
||||
|
||||
|
||||
@@ -277,6 +277,7 @@ namespace EagleInstaller {
|
||||
async Task<FetchAttempt>
|
||||
FetchAndWriteOne(string remotePath, string localDir, string expectedSha) {
|
||||
string writePath = WritePathFromRemotePath(remotePath, localDir);
|
||||
string tempPath = writePath + ".downloading";
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(writePath));
|
||||
|
||||
@@ -288,41 +289,67 @@ namespace EagleInstaller {
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
await using Stream responseStream = await response.Content.ReadAsStreamAsync();
|
||||
await using FileStream fileStream = File.Create(writePath);
|
||||
using MemoryStream memStream = new MemoryStream();
|
||||
await responseStream.CopyToAsync(memStream);
|
||||
memStream.Position = 0;
|
||||
|
||||
var sha = System.Security.Cryptography.SHA256.Create();
|
||||
string computedSha = BitConverter.ToString(sha.ComputeHash(memStream))
|
||||
.Replace("-", string.Empty)
|
||||
.ToLower();
|
||||
// Stream directly to disk while computing SHA256 incrementally
|
||||
using var sha = System.Security.Cryptography.SHA256.Create();
|
||||
await using (FileStream fileStream = File.Create(tempPath)) {
|
||||
var buffer = new byte[81920]; // 80KB buffer
|
||||
int bytesRead;
|
||||
while ((bytesRead = await responseStream.ReadAsync(buffer, 0, buffer.Length)) >
|
||||
0) {
|
||||
// Write to disk
|
||||
await fileStream.WriteAsync(buffer, 0, bytesRead);
|
||||
// Update hash incrementally
|
||||
sha.TransformBlock(buffer, 0, bytesRead, null, 0);
|
||||
}
|
||||
}
|
||||
// Finalize hash computation
|
||||
sha.TransformFinalBlock(Array.Empty<byte>(), 0, 0);
|
||||
string computedSha =
|
||||
BitConverter.ToString(sha.Hash).Replace("-", string.Empty).ToLower();
|
||||
|
||||
if (computedSha.Equals(expectedSha)) {
|
||||
// SHA matches - rename temp file to final location
|
||||
if (File.Exists(writePath)) { File.Delete(writePath); }
|
||||
File.Move(tempPath, writePath);
|
||||
Logger.WriteLine($"✓ Verified and saved: {remotePath}");
|
||||
memStream.Position = 0;
|
||||
await memStream.CopyToAsync(fileStream);
|
||||
return new FetchAttempt {
|
||||
Success = true,
|
||||
Kvp = new KeyValuePair<String, String>(remotePath, computedSha)
|
||||
};
|
||||
} else {
|
||||
// SHA mismatch - delete temp file
|
||||
try {
|
||||
File.Delete(tempPath);
|
||||
} catch {}
|
||||
Logger.WriteError($"✗ SHA mismatch for {remotePath}");
|
||||
return new FetchAttempt() { Success = false };
|
||||
}
|
||||
} catch (TaskCanceledException e) {
|
||||
// Clean up temp file on failure
|
||||
try {
|
||||
File.Delete(tempPath);
|
||||
} catch {}
|
||||
Logger.WriteError($"Download cancelled: {e.Message}");
|
||||
return new FetchAttempt() { Success = false };
|
||||
} catch (HttpRequestException e) {
|
||||
try {
|
||||
File.Delete(tempPath);
|
||||
} catch {}
|
||||
Logger.WriteError($"Network error: {e.Message}");
|
||||
return new FetchAttempt() { Success = false };
|
||||
} catch (Exception e) {
|
||||
try {
|
||||
File.Delete(tempPath);
|
||||
} catch {}
|
||||
Logger.WriteError($"Download failed: {e.Message}");
|
||||
return new FetchAttempt() { Success = false };
|
||||
} finally { Pool.Release(); }
|
||||
}
|
||||
|
||||
public async Task<bool> DownloadAndLaunchNewInstaller(string installerUrl) {
|
||||
public async Task<bool> DownloadAndLaunchNewInstaller(
|
||||
string installerUrl,
|
||||
string expectedSha) {
|
||||
try {
|
||||
Logger.UpdateStatus("Downloading new installer...");
|
||||
|
||||
@@ -372,6 +399,28 @@ namespace EagleInstaller {
|
||||
}
|
||||
}
|
||||
|
||||
// Verify SHA256 before launching
|
||||
Logger.UpdateStatus("Verifying installer integrity...");
|
||||
string computedSha;
|
||||
using (var sha256 = System.Security.Cryptography.SHA256.Create()) using (
|
||||
var stream = File.OpenRead(newInstallerPath)) {
|
||||
var hashBytes = sha256.ComputeHash(stream);
|
||||
computedSha =
|
||||
BitConverter.ToString(hashBytes).Replace("-", string.Empty).ToLower();
|
||||
}
|
||||
|
||||
if (!computedSha.Equals(expectedSha, StringComparison.OrdinalIgnoreCase)) {
|
||||
Logger.WriteError($"SECURITY ERROR: Installer SHA mismatch!");
|
||||
Logger.WriteError($" Expected: {expectedSha}");
|
||||
Logger.WriteError($" Got: {computedSha}");
|
||||
// Delete the corrupted/tampered file
|
||||
try {
|
||||
File.Delete(newInstallerPath);
|
||||
} catch {}
|
||||
return false;
|
||||
}
|
||||
Logger.WriteLine($"✓ Installer verified: SHA256 matches");
|
||||
|
||||
// Wait a moment to ensure file handles are released
|
||||
await Task.Delay(500);
|
||||
|
||||
|
||||
@@ -110,7 +110,8 @@ namespace EagleInstaller {
|
||||
if (installerUpdateInfo.InstallerNeedsUpdate) {
|
||||
Logger.UpdateStatus("Installer update required...");
|
||||
bool downloadSuccess = await updater.DownloadAndLaunchNewInstaller(
|
||||
installerUpdateInfo.NewInstallerUrl);
|
||||
installerUpdateInfo.NewInstallerUrl,
|
||||
installerUpdateInfo.NewInstallerVersion);
|
||||
if (downloadSuccess) {
|
||||
Logger.UpdateStatus("New installer launched. Exiting current version.");
|
||||
|
||||
|
||||
@@ -1666,6 +1666,8 @@ func handleUserRoutes(w http.ResponseWriter, r *http.Request) {
|
||||
handleSetUserDisplayName(w, r, userID)
|
||||
case "set-admin":
|
||||
handleSetUserAdmin(w, r, userID)
|
||||
case "delete":
|
||||
handleDeleteUser(w, r, userID)
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
@@ -1818,6 +1820,38 @@ func handleSetUserAdmin(w http.ResponseWriter, r *http.Request, userID string) {
|
||||
fmt.Fprintf(w, `<div class="alert alert-success">Admin status %s</div>`, status)
|
||||
}
|
||||
|
||||
func handleDeleteUser(w http.ResponseWriter, r *http.Request, userID string) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := createAdminContext(r)
|
||||
defer cancel()
|
||||
|
||||
resp, err := adminClient.DeleteUser(ctx, &adminpb.DeleteUserRequest{
|
||||
UserId: userID,
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("Failed to delete user: %v", err)
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
fmt.Fprintf(w, `<div class="alert alert-error">Error: %v</div>`, err)
|
||||
return
|
||||
}
|
||||
|
||||
if !resp.Success {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
fmt.Fprintf(w, `<div class="alert alert-error">%s</div>`, resp.ErrorMessage)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("HX-Trigger", "userUpdated")
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
fmt.Fprint(w, `<div class="alert alert-success">User deleted</div>`)
|
||||
}
|
||||
|
||||
// Invitation management handlers
|
||||
|
||||
// InvitationInfo represents an invitation for display
|
||||
@@ -1971,6 +2005,8 @@ func handleInvitationRoutes(w http.ResponseWriter, r *http.Request) {
|
||||
handleResendInvitation(w, r, code)
|
||||
case "revoke":
|
||||
handleRevokeInvitation(w, r, code)
|
||||
case "delete":
|
||||
handleDeleteInvitation(w, r, code)
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
@@ -2141,6 +2177,36 @@ func handleRevokeInvitation(w http.ResponseWriter, r *http.Request, code string)
|
||||
fmt.Fprint(w, `<div class="alert alert-success">Invitation revoked</div>`)
|
||||
}
|
||||
|
||||
func handleDeleteInvitation(w http.ResponseWriter, r *http.Request, code string) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := createAdminContext(r)
|
||||
defer cancel()
|
||||
|
||||
resp, err := adminClient.DeleteInvitation(ctx, &adminpb.DeleteInvitationRequest{
|
||||
InvitationCode: code,
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("Failed to delete invitation: %v", err)
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
fmt.Fprintf(w, `<div class="alert alert-error">Error: %v</div>`, err)
|
||||
return
|
||||
}
|
||||
|
||||
if !resp.Success {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
fmt.Fprintf(w, `<div class="alert alert-error">%s</div>`, resp.ErrorMessage)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("HX-Trigger", "invitationUpdated")
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
fmt.Fprint(w, `<div class="alert alert-success">Invitation deleted</div>`)
|
||||
}
|
||||
|
||||
// Authentication handlers
|
||||
|
||||
const (
|
||||
|
||||
@@ -83,7 +83,13 @@
|
||||
Copy Code
|
||||
</button>
|
||||
{{else}}
|
||||
<span class="no-actions">-</span>
|
||||
<button class="btn-small btn-danger"
|
||||
hx-post="/invitations/{{.InvitationCode}}/delete"
|
||||
hx-target="#feedback"
|
||||
hx-swap="innerHTML"
|
||||
hx-confirm="Are you sure you want to permanently delete this invitation?">
|
||||
Delete
|
||||
</button>
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -152,7 +158,8 @@ document.body.addEventListener('htmx:afterRequest', function(evt) {
|
||||
}, 1500);
|
||||
}
|
||||
} else if (evt.detail.pathInfo.requestPath.includes('/revoke') ||
|
||||
evt.detail.pathInfo.requestPath.includes('/resend')) {
|
||||
evt.detail.pathInfo.requestPath.includes('/resend') ||
|
||||
evt.detail.pathInfo.requestPath.includes('/delete')) {
|
||||
if (evt.detail.successful) {
|
||||
htmx.trigger(document.getElementById('invitations-table-body'), 'invitationUpdated');
|
||||
}
|
||||
|
||||
@@ -32,7 +32,13 @@
|
||||
Copy Code
|
||||
</button>
|
||||
{{else}}
|
||||
<span class="no-actions">-</span>
|
||||
<button class="btn-small btn-danger"
|
||||
hx-post="/invitations/{{.InvitationCode}}/delete"
|
||||
hx-target="#feedback"
|
||||
hx-swap="innerHTML"
|
||||
hx-confirm="Are you sure you want to permanently delete this invitation?">
|
||||
Delete
|
||||
</button>
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -65,6 +65,13 @@
|
||||
onclick="openEditModal('{{.UserID}}', '{{.DisplayName}}', {{.IsAdmin}})">
|
||||
Edit
|
||||
</button>
|
||||
<button class="btn-small btn-danger"
|
||||
hx-post="/users/{{.UserID}}/delete"
|
||||
hx-target="#feedback"
|
||||
hx-swap="innerHTML"
|
||||
hx-confirm="Are you sure you want to permanently delete this user? This cannot be undone.">
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
@@ -259,6 +266,22 @@ function saveUserChanges() {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: #dc3545;
|
||||
border-color: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background-color: #c82333;
|
||||
border-color: #bd2130;
|
||||
}
|
||||
|
||||
.user-actions {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
#edit-modal article {
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
@@ -30,6 +30,13 @@
|
||||
onclick="openEditModal('{{.UserID}}', '{{.DisplayName}}', {{.IsAdmin}})">
|
||||
Edit
|
||||
</button>
|
||||
<button class="btn-small btn-danger"
|
||||
hx-post="/users/{{.UserID}}/delete"
|
||||
hx-target="#feedback"
|
||||
hx-swap="innerHTML"
|
||||
hx-confirm="Are you sure you want to permanently delete this user? This cannot be undone.">
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
|
||||
@@ -5,6 +5,7 @@ go_library(
|
||||
srcs = [
|
||||
"admin_handlers.go",
|
||||
"handlers.go",
|
||||
"invitation_handlers.go",
|
||||
"invitations.go",
|
||||
"jwt.go",
|
||||
"main.go",
|
||||
|
||||
@@ -350,3 +350,51 @@ func (h *AdminHandler) internalStatusToAdmin(status userpb.InvitationStatus) adm
|
||||
return adminpb.InvitationStatus_INVITATION_STATUS_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteUser permanently deletes a user
|
||||
func (h *AdminHandler) DeleteUser(ctx context.Context, req *adminpb.DeleteUserRequest) (*adminpb.DeleteUserResponse, error) {
|
||||
adminUserID, err := h.requireAdmin(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Printf("[Admin] DeleteUser called by %s for user %s", adminUserID, req.UserId)
|
||||
|
||||
// Prevent self-deletion
|
||||
if req.UserId == adminUserID {
|
||||
return &adminpb.DeleteUserResponse{
|
||||
Success: false,
|
||||
ErrorMessage: "cannot delete your own account",
|
||||
}, nil
|
||||
}
|
||||
|
||||
if err := h.userService.Delete(req.UserId); err != nil {
|
||||
return &adminpb.DeleteUserResponse{
|
||||
Success: false,
|
||||
ErrorMessage: err.Error(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &adminpb.DeleteUserResponse{
|
||||
Success: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DeleteInvitation permanently deletes an invitation (only non-pending)
|
||||
func (h *AdminHandler) DeleteInvitation(ctx context.Context, req *adminpb.DeleteInvitationRequest) (*adminpb.DeleteInvitationResponse, error) {
|
||||
adminUserID, err := h.requireAdmin(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Printf("[Admin] DeleteInvitation called by %s for code=%s...", adminUserID, req.InvitationCode[:8])
|
||||
|
||||
if err := h.invitationService.Delete(req.InvitationCode); err != nil {
|
||||
return &adminpb.DeleteInvitationResponse{
|
||||
Success: false,
|
||||
ErrorMessage: err.Error(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &adminpb.DeleteInvitationResponse{
|
||||
Success: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,481 @@
|
||||
// Package main provides HTTP handlers for invitation landing pages.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
userpb "github.com/nolen777/eagle0/src/main/protobuf/net/eagle0/eagle/internal/user"
|
||||
)
|
||||
|
||||
// InvitationHTTPHandler handles HTTP requests for invitation pages
|
||||
type InvitationHTTPHandler struct {
|
||||
invitationService *InvitationService
|
||||
installerURL string
|
||||
macInstallerURL string
|
||||
}
|
||||
|
||||
// NewInvitationHTTPHandler creates a new invitation HTTP handler
|
||||
func NewInvitationHTTPHandler(invitationService *InvitationService) *InvitationHTTPHandler {
|
||||
installerURL := os.Getenv("INSTALLER_DOWNLOAD_URL")
|
||||
if installerURL == "" {
|
||||
installerURL = "https://assets.eagle0.net/installer/EagleInstaller.exe"
|
||||
}
|
||||
macInstallerURL := os.Getenv("MAC_INSTALLER_URL")
|
||||
if macInstallerURL == "" {
|
||||
macInstallerURL = "https://assets.eagle0.net/mac/builds/eagle0-latest.zip"
|
||||
}
|
||||
return &InvitationHTTPHandler{
|
||||
invitationService: invitationService,
|
||||
installerURL: installerURL,
|
||||
macInstallerURL: macInstallerURL,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes registers the invitation HTTP routes
|
||||
func (h *InvitationHTTPHandler) RegisterRoutes() {
|
||||
http.HandleFunc("/invite/", h.handleInvite)
|
||||
}
|
||||
|
||||
// handleInvite routes requests to the appropriate handler
|
||||
func (h *InvitationHTTPHandler) handleInvite(w http.ResponseWriter, r *http.Request) {
|
||||
// Parse path: /invite/{code} or /invite/{code}/install.bat or /invite/{code}/install.sh
|
||||
path := strings.TrimPrefix(r.URL.Path, "/invite/")
|
||||
parts := strings.Split(path, "/")
|
||||
|
||||
if len(parts) == 0 || parts[0] == "" {
|
||||
http.Error(w, "Invalid invitation URL", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
code := parts[0]
|
||||
|
||||
if len(parts) == 2 && parts[1] == "install.bat" {
|
||||
h.handleInstallBat(w, r, code)
|
||||
} else if len(parts) == 2 && parts[1] == "install.sh" {
|
||||
h.handleInstallSh(w, r, code)
|
||||
} else if len(parts) == 1 {
|
||||
h.handleLandingPage(w, r, code)
|
||||
} else {
|
||||
http.Error(w, "Not found", http.StatusNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
// isMacUserAgent returns true if the User-Agent indicates a Mac browser
|
||||
func isMacUserAgent(userAgent string) bool {
|
||||
ua := strings.ToLower(userAgent)
|
||||
return strings.Contains(ua, "macintosh") || strings.Contains(ua, "mac os x")
|
||||
}
|
||||
|
||||
// handleLandingPage serves the invitation landing page
|
||||
func (h *InvitationHTTPHandler) handleLandingPage(w http.ResponseWriter, r *http.Request, code string) {
|
||||
invitation := h.invitationService.FindByCode(code)
|
||||
isMac := isMacUserAgent(r.UserAgent())
|
||||
|
||||
data := struct {
|
||||
Valid bool
|
||||
Code string
|
||||
ExpiresAt string
|
||||
ErrorMessage string
|
||||
InstallBatURL string
|
||||
InstallerURL string
|
||||
IsMac bool
|
||||
InstallShURL string
|
||||
MacInstallerURL string
|
||||
}{
|
||||
Code: code,
|
||||
InstallBatURL: fmt.Sprintf("/invite/%s/install.bat", code),
|
||||
InstallerURL: h.installerURL,
|
||||
IsMac: isMac,
|
||||
InstallShURL: fmt.Sprintf("/invite/%s/install.sh", code),
|
||||
MacInstallerURL: h.macInstallerURL,
|
||||
}
|
||||
|
||||
if invitation == nil {
|
||||
data.Valid = false
|
||||
data.ErrorMessage = "This invitation code is invalid or does not exist."
|
||||
} else {
|
||||
switch invitation.Status {
|
||||
case userpb.InvitationStatus_INVITATION_STATUS_PENDING:
|
||||
// Check if expired by date
|
||||
if invitation.ExpiresAt != nil && invitation.ExpiresAt.AsTime().Before(time.Now()) {
|
||||
data.Valid = false
|
||||
data.ErrorMessage = "This invitation has expired."
|
||||
} else {
|
||||
data.Valid = true
|
||||
if invitation.ExpiresAt != nil {
|
||||
data.ExpiresAt = invitation.ExpiresAt.AsTime().Format("January 2, 2006")
|
||||
}
|
||||
}
|
||||
case userpb.InvitationStatus_INVITATION_STATUS_REDEEMED:
|
||||
data.Valid = false
|
||||
data.ErrorMessage = "This invitation has already been used."
|
||||
case userpb.InvitationStatus_INVITATION_STATUS_EXPIRED:
|
||||
data.Valid = false
|
||||
data.ErrorMessage = "This invitation has expired."
|
||||
case userpb.InvitationStatus_INVITATION_STATUS_REVOKED:
|
||||
data.Valid = false
|
||||
data.ErrorMessage = "This invitation has been revoked."
|
||||
default:
|
||||
data.Valid = false
|
||||
data.ErrorMessage = "This invitation is not valid."
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
if err := landingPageTemplate.Execute(w, data); err != nil {
|
||||
log.Printf("[InviteHTTP] Failed to render landing page: %v", err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// handleInstallBat serves the Windows batch installer script
|
||||
func (h *InvitationHTTPHandler) handleInstallBat(w http.ResponseWriter, r *http.Request, code string) {
|
||||
// Validate the code exists and is valid
|
||||
if !h.invitationService.IsValidCode(code) {
|
||||
http.Error(w, "Invalid or expired invitation code", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Generate batch script
|
||||
script := fmt.Sprintf(`@echo off
|
||||
echo.
|
||||
echo ====================================
|
||||
echo Eagle0 Installer
|
||||
echo ====================================
|
||||
echo.
|
||||
echo Downloading Eagle0 installer...
|
||||
echo.
|
||||
|
||||
curl -L -o "%%TEMP%%\EagleInstaller.exe" "%s"
|
||||
if %%ERRORLEVEL%% neq 0 (
|
||||
echo.
|
||||
echo Failed to download installer. Please check your internet connection.
|
||||
echo You can also download manually from: %s
|
||||
echo.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo Starting installer...
|
||||
echo.
|
||||
start "" "%%TEMP%%\EagleInstaller.exe" --code=%s
|
||||
|
||||
echo.
|
||||
echo The installer should now be running.
|
||||
echo You can close this window.
|
||||
echo.
|
||||
timeout /t 5
|
||||
`, h.installerURL, h.installerURL, code)
|
||||
|
||||
w.Header().Set("Content-Type", "application/x-bat")
|
||||
w.Header().Set("Content-Disposition", "attachment; filename=eagle0-install.bat")
|
||||
w.Write([]byte(script))
|
||||
|
||||
log.Printf("[InviteHTTP] Served install.bat for code %s...", code[:8])
|
||||
}
|
||||
|
||||
// handleInstallSh serves the Mac shell installer script
|
||||
func (h *InvitationHTTPHandler) handleInstallSh(w http.ResponseWriter, r *http.Request, code string) {
|
||||
// Validate the code exists and is valid
|
||||
if !h.invitationService.IsValidCode(code) {
|
||||
http.Error(w, "Invalid or expired invitation code", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Generate shell script
|
||||
script := fmt.Sprintf(`#!/bin/bash
|
||||
#
|
||||
# Eagle0 Mac Installer
|
||||
# This script downloads Eagle0 and sets up your invitation code.
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
echo ""
|
||||
echo "===================================="
|
||||
echo " Eagle0 Mac Installer"
|
||||
echo "===================================="
|
||||
echo ""
|
||||
|
||||
# Create app support directory
|
||||
APP_SUPPORT_DIR="$HOME/Library/Application Support/eagle0"
|
||||
mkdir -p "$APP_SUPPORT_DIR"
|
||||
|
||||
# Write invitation code
|
||||
echo "Setting up invitation code..."
|
||||
cat > "$APP_SUPPORT_DIR/invitation.json" << 'INVITATION_EOF'
|
||||
{"invitationCode": "%s"}
|
||||
INVITATION_EOF
|
||||
|
||||
echo "Invitation code saved."
|
||||
echo ""
|
||||
|
||||
# Download location
|
||||
DOWNLOAD_DIR="$HOME/Downloads"
|
||||
ZIP_PATH="$DOWNLOAD_DIR/eagle0.zip"
|
||||
APP_PATH="/Applications/eagle0.app"
|
||||
|
||||
echo "Downloading Eagle0..."
|
||||
curl -L -o "$ZIP_PATH" "%s"
|
||||
|
||||
echo ""
|
||||
echo "Extracting to /Applications..."
|
||||
|
||||
# Remove old version if exists
|
||||
if [ -d "$APP_PATH" ]; then
|
||||
echo "Removing previous version..."
|
||||
rm -rf "$APP_PATH"
|
||||
fi
|
||||
|
||||
# Unzip to Applications
|
||||
unzip -q "$ZIP_PATH" -d /Applications/
|
||||
|
||||
# Clean up zip
|
||||
rm "$ZIP_PATH"
|
||||
|
||||
# Remove quarantine attribute (app is notarized but downloaded via curl)
|
||||
xattr -dr com.apple.quarantine "$APP_PATH" 2>/dev/null || true
|
||||
|
||||
echo ""
|
||||
echo "===================================="
|
||||
echo " Installation Complete!"
|
||||
echo "===================================="
|
||||
echo ""
|
||||
echo "Eagle0 has been installed to /Applications/eagle0.app"
|
||||
echo "Your invitation code has been saved."
|
||||
echo ""
|
||||
echo "Starting Eagle0..."
|
||||
open "$APP_PATH"
|
||||
|
||||
`, code, h.macInstallerURL)
|
||||
|
||||
w.Header().Set("Content-Type", "application/x-sh")
|
||||
w.Header().Set("Content-Disposition", "attachment; filename=eagle0-install.sh")
|
||||
w.Write([]byte(script))
|
||||
|
||||
log.Printf("[InviteHTTP] Served install.sh for code %s...", code[:8])
|
||||
}
|
||||
|
||||
var landingPageTemplate = template.Must(template.New("landing").Parse(`<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Eagle0 Invitation</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.container {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 40px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
margin-top: 40px;
|
||||
}
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.logo {
|
||||
font-size: 48px;
|
||||
font-weight: bold;
|
||||
color: #1a5f7a;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
}
|
||||
.error-box {
|
||||
background: #fee;
|
||||
border: 1px solid #fcc;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #c00;
|
||||
}
|
||||
.content {
|
||||
text-align: center;
|
||||
}
|
||||
.button {
|
||||
display: inline-block;
|
||||
background: #1a5f7a;
|
||||
color: white !important;
|
||||
text-decoration: none;
|
||||
padding: 16px 32px;
|
||||
border-radius: 8px;
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
margin: 20px 0;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.button:hover {
|
||||
background: #154d62;
|
||||
}
|
||||
.instructions {
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
text-align: left;
|
||||
}
|
||||
.instructions h3 {
|
||||
margin-top: 0;
|
||||
color: #1a5f7a;
|
||||
}
|
||||
.instructions ol {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
.instructions li {
|
||||
margin: 10px 0;
|
||||
}
|
||||
.code-box {
|
||||
background: #e9ecef;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
word-break: break-all;
|
||||
margin: 15px 0;
|
||||
}
|
||||
.manual-section {
|
||||
margin-top: 30px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
.manual-section h3 {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
.expires {
|
||||
color: #856404;
|
||||
background: #fff3cd;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin-top: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.footer {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
code {
|
||||
background: #e9ecef;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
.other-platform {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
}
|
||||
.other-platform a {
|
||||
color: #1a5f7a;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<div class="logo">Eagle0</div>
|
||||
{{if .Valid}}
|
||||
<h1>You've been invited!</h1>
|
||||
{{else}}
|
||||
<h1>Invitation Error</h1>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{if .Valid}}
|
||||
<div class="content">
|
||||
<p>You've been invited to play Eagle0, a strategic turn-based game with tactical hex-based combat.</p>
|
||||
|
||||
{{if .IsMac}}
|
||||
<a href="{{.InstallShURL}}" class="button">Download for Mac</a>
|
||||
|
||||
<div class="instructions">
|
||||
<h3>How to install on Mac:</h3>
|
||||
<ol>
|
||||
<li>Click the "Download for Mac" button above</li>
|
||||
<li>Open Terminal and navigate to your Downloads folder:<br>
|
||||
<code>cd ~/Downloads</code></li>
|
||||
<li>Run the installer script:<br>
|
||||
<code>bash eagle0-install.sh</code></li>
|
||||
<li>Eagle0 will be installed to /Applications and launched automatically</li>
|
||||
</ol>
|
||||
</div>
|
||||
{{else}}
|
||||
<a href="{{.InstallBatURL}}" class="button">Download for Windows</a>
|
||||
|
||||
<div class="instructions">
|
||||
<h3>How to install on Windows:</h3>
|
||||
<ol>
|
||||
<li>Click the "Download for Windows" button above</li>
|
||||
<li>Open the downloaded <strong>eagle0-install.bat</strong> file</li>
|
||||
<li>If Windows shows a security prompt, click "More info" then "Run anyway"</li>
|
||||
<li>The installer will download and start automatically with your invitation code</li>
|
||||
</ol>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .ExpiresAt}}
|
||||
<div class="expires">
|
||||
This invitation expires on {{.ExpiresAt}}.
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="manual-section">
|
||||
{{if .IsMac}}
|
||||
<h3>Alternative: Manual Installation</h3>
|
||||
<p>If the script doesn't work, you can <a href="{{.MacInstallerURL}}">download the ZIP manually</a>, extract it to /Applications, and enter this code when prompted:</p>
|
||||
{{else}}
|
||||
<h3>Alternative: Manual Installation</h3>
|
||||
<p>If the automatic installer doesn't work, you can <a href="{{.InstallerURL}}">download the installer manually</a> and enter this code when prompted:</p>
|
||||
{{end}}
|
||||
<div class="code-box">{{.Code}}</div>
|
||||
</div>
|
||||
|
||||
<div class="other-platform">
|
||||
{{if .IsMac}}
|
||||
<p><small>Looking for Windows? <a href="{{.InstallBatURL}}">Download Windows installer</a></small></p>
|
||||
{{else}}
|
||||
<p><small>Looking for Mac? <a href="{{.InstallShURL}}">Download Mac installer</a></small></p>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="error-box">
|
||||
<p>{{.ErrorMessage}}</p>
|
||||
<p>Please contact the person who invited you for a new invitation.</p>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="footer">
|
||||
<p>© 2026 Eagle0</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`))
|
||||
@@ -362,3 +362,44 @@ func (is *InvitationService) GetStats() (pending, redeemed, expired, revoked int
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Delete permanently removes an invitation from the database.
|
||||
// Only revoked, expired, or redeemed invitations can be deleted.
|
||||
func (is *InvitationService) Delete(code string) error {
|
||||
is.mu.Lock()
|
||||
defer is.mu.Unlock()
|
||||
|
||||
idx, ok := is.database.CodeIndex[code]
|
||||
if !ok || int(idx) >= len(is.database.Invitations) {
|
||||
return fmt.Errorf("invitation not found")
|
||||
}
|
||||
|
||||
invitation := is.database.Invitations[idx]
|
||||
|
||||
// Only allow deletion of non-pending invitations
|
||||
if invitation.Status == userpb.InvitationStatus_INVITATION_STATUS_PENDING {
|
||||
// Check if actually expired
|
||||
if invitation.ExpiresAt == nil || !invitation.ExpiresAt.AsTime().Before(time.Now()) {
|
||||
return fmt.Errorf("cannot delete a pending invitation (revoke it first)")
|
||||
}
|
||||
}
|
||||
|
||||
// Remove from code index
|
||||
delete(is.database.CodeIndex, code)
|
||||
|
||||
// Remove from invitations slice
|
||||
is.database.Invitations = append(is.database.Invitations[:idx], is.database.Invitations[idx+1:]...)
|
||||
|
||||
// Rebuild code index since array indices have shifted
|
||||
is.database.CodeIndex = make(map[string]int32)
|
||||
for i, inv := range is.database.Invitations {
|
||||
is.database.CodeIndex[inv.InvitationCode] = int32(i)
|
||||
}
|
||||
|
||||
if err := is.save(); err != nil {
|
||||
return fmt.Errorf("failed to save: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("[Invitation] Deleted invitation for %s (code %s...)", invitation.Email, code[:8])
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -144,13 +144,17 @@ func main() {
|
||||
}
|
||||
}()
|
||||
|
||||
// Start HTTP server for OAuth callbacks
|
||||
// Start HTTP server for OAuth callbacks and invitation pages
|
||||
http.HandleFunc("/oauth/callback", oauthSvc.HandleCallback)
|
||||
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprintf(w, "OK")
|
||||
})
|
||||
|
||||
// Register invitation landing page routes
|
||||
inviteHandler := NewInvitationHTTPHandler(invitationService)
|
||||
inviteHandler.RegisterRoutes()
|
||||
|
||||
log.Printf("HTTP server listening on port %d", actualHTTPPort)
|
||||
if err := http.ListenAndServe(fmt.Sprintf(":%d", actualHTTPPort), nil); err != nil {
|
||||
log.Fatalf("HTTP server failed: %v", err)
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
@@ -23,12 +22,13 @@ const (
|
||||
|
||||
// EmailService handles sending emails via Fastmail JMAP API
|
||||
type EmailService struct {
|
||||
apiToken string
|
||||
fromEmail string
|
||||
fromName string
|
||||
installerURL string
|
||||
httpClient *http.Client
|
||||
enabled bool
|
||||
apiToken string
|
||||
fromEmail string
|
||||
fromName string
|
||||
installerURL string
|
||||
serverBaseURL string
|
||||
httpClient *http.Client
|
||||
enabled bool
|
||||
|
||||
// JMAP session info (populated on first send)
|
||||
accountID string
|
||||
@@ -43,6 +43,7 @@ func NewEmailService() *EmailService {
|
||||
fromEmail := os.Getenv("FASTMAIL_FROM_EMAIL")
|
||||
fromName := os.Getenv("FASTMAIL_FROM_NAME")
|
||||
installerURL := os.Getenv("INSTALLER_DOWNLOAD_URL")
|
||||
serverBaseURL := os.Getenv("SERVER_BASE_URL")
|
||||
|
||||
if fromEmail == "" {
|
||||
fromEmail = "noreply@eagle0.net"
|
||||
@@ -53,6 +54,9 @@ func NewEmailService() *EmailService {
|
||||
if installerURL == "" {
|
||||
installerURL = "https://assets.eagle0.net/installer/EagleInstaller.exe"
|
||||
}
|
||||
if serverBaseURL == "" {
|
||||
serverBaseURL = "https://prod.eagle0.net"
|
||||
}
|
||||
|
||||
enabled := apiToken != ""
|
||||
if !enabled {
|
||||
@@ -62,10 +66,11 @@ func NewEmailService() *EmailService {
|
||||
}
|
||||
|
||||
return &EmailService{
|
||||
apiToken: apiToken,
|
||||
fromEmail: fromEmail,
|
||||
fromName: fromName,
|
||||
installerURL: installerURL,
|
||||
apiToken: apiToken,
|
||||
fromEmail: fromEmail,
|
||||
fromName: fromName,
|
||||
installerURL: installerURL,
|
||||
serverBaseURL: serverBaseURL,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
@@ -92,20 +97,17 @@ func (e *EmailService) SendInvitation(invitation *userpb.Invitation) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Build download URL with invitation code
|
||||
downloadURL, err := e.buildDownloadURL(invitation.InvitationCode)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to build download URL: %w", err)
|
||||
}
|
||||
// Build invite page URL
|
||||
invitePageURL := fmt.Sprintf("%s/invite/%s", e.serverBaseURL, invitation.InvitationCode)
|
||||
|
||||
// Build email content
|
||||
subject := "You've been invited to play Eagle0!"
|
||||
htmlContent, err := e.buildInvitationHTML(invitation, downloadURL)
|
||||
htmlContent, err := e.buildInvitationHTML(invitation, invitePageURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to build email content: %w", err)
|
||||
}
|
||||
|
||||
textContent := e.buildInvitationText(invitation, downloadURL)
|
||||
textContent := e.buildInvitationText(invitation, invitePageURL)
|
||||
|
||||
// Send via JMAP
|
||||
if err := e.sendEmail(invitation.Email, subject, htmlContent, textContent); err != nil {
|
||||
@@ -243,18 +245,7 @@ func (e *EmailService) fetchIdentityAndDrafts() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *EmailService) buildDownloadURL(code string) (string, error) {
|
||||
u, err := url.Parse(e.installerURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
q := u.Query()
|
||||
q.Set("code", code)
|
||||
u.RawQuery = q.Encode()
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
func (e *EmailService) buildInvitationHTML(invitation *userpb.Invitation, downloadURL string) (string, error) {
|
||||
func (e *EmailService) buildInvitationHTML(invitation *userpb.Invitation, invitePageURL string) (string, error) {
|
||||
const htmlTemplate = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
@@ -268,11 +259,8 @@ func (e *EmailService) buildInvitationHTML(invitation *userpb.Invitation, downlo
|
||||
.content { background: #f8f9fa; border-radius: 8px; padding: 30px; margin: 20px 0; }
|
||||
.button { display: inline-block; background: #1a5f7a; color: white !important; text-decoration: none; padding: 14px 28px; border-radius: 6px; font-weight: bold; margin: 20px 0; }
|
||||
.button:hover { background: #154d62; }
|
||||
.code-box { background: #e9ecef; border: 1px solid #ced4da; border-radius: 4px; padding: 12px; font-family: monospace; font-size: 14px; word-break: break-all; margin: 15px 0; }
|
||||
.footer { text-align: center; color: #6c757d; font-size: 12px; margin-top: 30px; }
|
||||
.expires { color: #856404; background: #fff3cd; padding: 10px; border-radius: 4px; margin-top: 15px; }
|
||||
.section-title { font-weight: bold; margin-top: 20px; margin-bottom: 10px; }
|
||||
.option { margin-bottom: 20px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -283,24 +271,20 @@ func (e *EmailService) buildInvitationHTML(invitation *userpb.Invitation, downlo
|
||||
<h2>You've been invited!</h2>
|
||||
<p>Someone has invited you to join Eagle0, a strategic turn-based game with tactical hex-based combat.</p>
|
||||
|
||||
<div class="option">
|
||||
<p class="section-title">Option 1: One-Click Install (Recommended)</p>
|
||||
<p>Copy and paste this command into PowerShell to download and run the installer with your invitation code:</p>
|
||||
<div class="code-box">powershell -Command "Invoke-WebRequest '{{.InstallerURL}}' -OutFile EagleInstaller.exe; .\EagleInstaller.exe --code={{.InvitationCode}}"</div>
|
||||
</div>
|
||||
<p style="text-align: center;">
|
||||
<a href="{{.InvitePageURL}}" class="button">Accept Invitation</a>
|
||||
</p>
|
||||
|
||||
<div class="option">
|
||||
<p class="section-title">Option 2: Manual Download</p>
|
||||
<p style="text-align: center;">
|
||||
<a href="{{.DownloadURL}}" class="button">Download Eagle0</a>
|
||||
</p>
|
||||
<p>After downloading, enter this invitation code when prompted in the game:</p>
|
||||
<div class="code-box">{{.InvitationCode}}</div>
|
||||
</div>
|
||||
<p style="text-align: center; color: #666;">Click the button above to download and install Eagle0.</p>
|
||||
|
||||
<div class="expires">
|
||||
This invitation expires on {{.ExpiresAt}}.
|
||||
</div>
|
||||
|
||||
<p style="margin-top: 20px; font-size: 12px; color: #999;">
|
||||
If the button doesn't work, copy this link into your browser:<br>
|
||||
<a href="{{.InvitePageURL}}" style="color: #1a5f7a;">{{.InvitePageURL}}</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>This is an automated message. If you didn't expect this invitation, you can safely ignore it.</p>
|
||||
@@ -321,10 +305,8 @@ func (e *EmailService) buildInvitationHTML(invitation *userpb.Invitation, downlo
|
||||
}
|
||||
|
||||
data := map[string]string{
|
||||
"DownloadURL": downloadURL,
|
||||
"InstallerURL": e.installerURL,
|
||||
"InvitationCode": invitation.InvitationCode,
|
||||
"ExpiresAt": expiresAt,
|
||||
"InvitePageURL": invitePageURL,
|
||||
"ExpiresAt": expiresAt,
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
@@ -335,7 +317,7 @@ func (e *EmailService) buildInvitationHTML(invitation *userpb.Invitation, downlo
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
func (e *EmailService) buildInvitationText(invitation *userpb.Invitation, downloadURL string) string {
|
||||
func (e *EmailService) buildInvitationText(invitation *userpb.Invitation, invitePageURL string) string {
|
||||
var expiresAt string
|
||||
if invitation.ExpiresAt != nil {
|
||||
expiresAt = invitation.ExpiresAt.AsTime().Format("January 2, 2006")
|
||||
@@ -347,22 +329,14 @@ func (e *EmailService) buildInvitationText(invitation *userpb.Invitation, downlo
|
||||
|
||||
Someone has invited you to join Eagle0, a strategic turn-based game with tactical hex-based combat.
|
||||
|
||||
OPTION 1: One-Click Install (Recommended)
|
||||
Copy and paste this command into PowerShell:
|
||||
|
||||
powershell -Command "Invoke-WebRequest '%s' -OutFile EagleInstaller.exe; .\EagleInstaller.exe --code=%s"
|
||||
|
||||
OPTION 2: Manual Download
|
||||
Download the game: %s
|
||||
|
||||
After downloading, enter this invitation code when prompted in the game:
|
||||
Click here to accept your invitation and download the game:
|
||||
%s
|
||||
|
||||
This invitation expires on %s.
|
||||
|
||||
---
|
||||
This is an automated message. If you didn't expect this invitation, you can safely ignore it.
|
||||
`, e.installerURL, invitation.InvitationCode, downloadURL, invitation.InvitationCode, expiresAt)
|
||||
`, invitePageURL, expiresAt)
|
||||
}
|
||||
|
||||
// sendEmail sends an email via Fastmail JMAP API
|
||||
|
||||
@@ -466,3 +466,45 @@ func (us *UserService) SetAdmin(userID string, isAdmin bool) error {
|
||||
}
|
||||
return fmt.Errorf("user not found")
|
||||
}
|
||||
|
||||
// Delete permanently removes a user from the database
|
||||
func (us *UserService) Delete(userID string) error {
|
||||
us.mu.Lock()
|
||||
defer us.mu.Unlock()
|
||||
|
||||
// Find user index
|
||||
var userIndex = -1
|
||||
var user *userpb.User
|
||||
for i, u := range us.database.Users {
|
||||
if u.UserId == userID {
|
||||
userIndex = i
|
||||
user = u
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if userIndex == -1 {
|
||||
return fmt.Errorf("user not found")
|
||||
}
|
||||
|
||||
// Remove from OAuth index
|
||||
for _, identity := range user.OauthIdentities {
|
||||
oauthKey := fmt.Sprintf("%s:%s", identity.Provider, identity.ProviderUserId)
|
||||
delete(us.database.OauthIndex, oauthKey)
|
||||
}
|
||||
|
||||
// Remove from display name index
|
||||
if user.DisplayNameLower != "" {
|
||||
delete(us.database.DisplayNameIndex, user.DisplayNameLower)
|
||||
}
|
||||
|
||||
// Remove from users slice
|
||||
us.database.Users = append(us.database.Users[:userIndex], us.database.Users[userIndex+1:]...)
|
||||
|
||||
if err := us.save(); err != nil {
|
||||
return fmt.Errorf("failed to save: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("[UserService] Deleted user %s (%s)", userID, user.DisplayName)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
|
||||
|
||||
go_library(
|
||||
name = "mac_build_handler_lib",
|
||||
srcs = ["mac_build_handler.go"],
|
||||
importpath = "github.com/nolen777/eagle0/src/main/go/net/eagle0/build/mac_build_handler",
|
||||
visibility = ["//visibility:private"],
|
||||
deps = ["//src/main/go/net/eagle0/util/aws"],
|
||||
)
|
||||
|
||||
go_binary(
|
||||
name = "mac_build_handler",
|
||||
embed = [":mac_build_handler_lib"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
@@ -0,0 +1,351 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"crypto/sha256"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/nolen777/eagle0/src/main/go/net/eagle0/util/aws"
|
||||
)
|
||||
|
||||
var bucketName = "eagle0-windows" // Using existing bucket with /mac/ prefix
|
||||
var appcastPath = "mac/appcast.xml"
|
||||
var buildsRoot = "mac/builds/"
|
||||
|
||||
// Sparkle Appcast XML structures
|
||||
type Appcast struct {
|
||||
XMLName xml.Name `xml:"rss"`
|
||||
Version string `xml:"version,attr"`
|
||||
Xmlns string `xml:"xmlns:sparkle,attr"`
|
||||
Channel Channel `xml:"channel"`
|
||||
}
|
||||
|
||||
type Channel struct {
|
||||
Title string `xml:"title"`
|
||||
Link string `xml:"link"`
|
||||
Description string `xml:"description"`
|
||||
Language string `xml:"language"`
|
||||
Items []Item `xml:"item"`
|
||||
}
|
||||
|
||||
type Item struct {
|
||||
Title string `xml:"title"`
|
||||
PubDate string `xml:"pubDate"`
|
||||
SparkleVersion string `xml:"sparkle:version"`
|
||||
SparkleShortVersion string `xml:"sparkle:shortVersionString"`
|
||||
Description string `xml:"description,omitempty"`
|
||||
Enclosure Enclosure `xml:"enclosure"`
|
||||
}
|
||||
|
||||
type Enclosure struct {
|
||||
URL string `xml:"url,attr"`
|
||||
Length int64 `xml:"length,attr"`
|
||||
Type string `xml:"type,attr"`
|
||||
EdSig string `xml:"sparkle:edSignature,attr"`
|
||||
}
|
||||
|
||||
func zipApp(appPath string, zipPath string) error {
|
||||
zipFile, err := os.Create(zipPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create zip file: %w", err)
|
||||
}
|
||||
defer zipFile.Close()
|
||||
|
||||
zipWriter := zip.NewWriter(zipFile)
|
||||
defer zipWriter.Close()
|
||||
|
||||
appBase := filepath.Base(appPath)
|
||||
|
||||
// Use WalkDir with Lstat to properly handle symlinks
|
||||
err = filepath.WalkDir(appPath, func(path string, d os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get the relative path from the app's parent directory
|
||||
relPath, err := filepath.Rel(filepath.Dir(appPath), path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Skip if it's the root
|
||||
if relPath == appBase && d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Use Lstat to get info without following symlinks
|
||||
info, err := os.Lstat(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if it's a symlink
|
||||
if info.Mode()&os.ModeSymlink != 0 {
|
||||
// Read the symlink target
|
||||
linkTarget, err := os.Readlink(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create symlink entry in zip
|
||||
header, err := zip.FileInfoHeader(info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
header.Name = relPath
|
||||
header.Method = zip.Store
|
||||
|
||||
writer, err := zipWriter.CreateHeader(header)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = writer.Write([]byte(linkTarget))
|
||||
return err
|
||||
}
|
||||
|
||||
header, err := zip.FileInfoHeader(info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Use forward slashes and preserve the .app directory structure
|
||||
header.Name = relPath
|
||||
if info.IsDir() {
|
||||
header.Name += "/"
|
||||
} else {
|
||||
header.Method = zip.Deflate
|
||||
}
|
||||
|
||||
// Preserve executable permissions
|
||||
header.SetMode(info.Mode())
|
||||
|
||||
writer, err := zipWriter.CreateHeader(header)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !info.IsDir() {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
_, err = io.Copy(writer, file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func signWithSparkle(filePath string, privateKeyPath string) (string, error) {
|
||||
// Use Sparkle's sign_update tool
|
||||
// First, try to find it in the Sparkle cache
|
||||
sparkleSignTool := "/tmp/sparkle-cache/Sparkle-2.6.4/bin/sign_update"
|
||||
|
||||
// If not found, download Sparkle
|
||||
if _, err := os.Stat(sparkleSignTool); os.IsNotExist(err) {
|
||||
log.Println("Sparkle sign_update not found, downloading...")
|
||||
cmd := exec.Command("bash", "-c", `
|
||||
mkdir -p /tmp/sparkle-cache
|
||||
curl -sL https://github.com/sparkle-project/Sparkle/releases/download/2.6.4/Sparkle-2.6.4.tar.xz | tar -xJ -C /tmp/sparkle-cache
|
||||
`)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return "", fmt.Errorf("failed to download Sparkle: %s: %w", output, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Sign the file using -f to specify private key file path
|
||||
// Note: -s is deprecated and expects key as string argument, not file path
|
||||
cmd := exec.Command(sparkleSignTool, filePath, "-f", privateKeyPath)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to sign file: %s: %w", string(output), err)
|
||||
}
|
||||
|
||||
// sign_update outputs: sparkle:edSignature="<signature>" length="<length>"
|
||||
// We need to extract just the signature
|
||||
signature := strings.TrimSpace(string(output))
|
||||
// Parse out the signature from the output
|
||||
if strings.Contains(signature, "sparkle:edSignature=\"") {
|
||||
start := strings.Index(signature, "sparkle:edSignature=\"") + len("sparkle:edSignature=\"")
|
||||
end := strings.Index(signature[start:], "\"")
|
||||
if end > 0 {
|
||||
signature = signature[start : start+end]
|
||||
}
|
||||
}
|
||||
|
||||
return signature, nil
|
||||
}
|
||||
|
||||
func getFileSize(path string) (int64, error) {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return info.Size(), nil
|
||||
}
|
||||
|
||||
func sha256File(path string) (string, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
h := sha256.New()
|
||||
if _, err := io.Copy(h, f); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%x", h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
func fetchAppcast(bb aws.BucketBasics) (*Appcast, error) {
|
||||
content, err := bb.FetchString(bucketName, appcastPath)
|
||||
if err != nil {
|
||||
// Return empty appcast if doesn't exist
|
||||
return &Appcast{
|
||||
Version: "2.0",
|
||||
Xmlns: "http://www.andymatuschak.org/xml-namespaces/sparkle",
|
||||
Channel: Channel{
|
||||
Title: "Eagle0",
|
||||
Link: "https://assets.eagle0.net/mac/appcast.xml",
|
||||
Description: "Eagle0 game updates",
|
||||
Language: "en",
|
||||
Items: []Item{},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
var appcast Appcast
|
||||
err = xml.Unmarshal([]byte(content), &appcast)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse appcast: %w", err)
|
||||
}
|
||||
|
||||
return &appcast, nil
|
||||
}
|
||||
|
||||
func uploadAppcast(bb aws.BucketBasics, appcast *Appcast) error {
|
||||
output, err := xml.MarshalIndent(appcast, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal appcast: %w", err)
|
||||
}
|
||||
|
||||
xmlContent := xml.Header + string(output)
|
||||
return bb.UploadBytesPublic(bucketName, appcastPath, []byte(xmlContent))
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 5 {
|
||||
fmt.Println("Usage: mac_build_handler <app_path> <version> <build_number> <sparkle_private_key_path>")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
appPath := os.Args[1]
|
||||
version := os.Args[2]
|
||||
buildNumber := os.Args[3]
|
||||
privateKeyPath := os.Args[4]
|
||||
|
||||
if _, err := os.Stat(appPath); os.IsNotExist(err) {
|
||||
log.Fatalf("App not found: %s", appPath)
|
||||
}
|
||||
|
||||
bb, err := aws.NewBucketBasics()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Create ZIP of the app
|
||||
zipFileName := fmt.Sprintf("eagle0-%s.zip", version)
|
||||
zipPath := filepath.Join("/tmp", zipFileName)
|
||||
|
||||
log.Printf("Creating ZIP: %s", zipPath)
|
||||
if err := zipApp(appPath, zipPath); err != nil {
|
||||
log.Fatalf("Failed to create ZIP: %v", err)
|
||||
}
|
||||
|
||||
// Get file size
|
||||
fileSize, err := getFileSize(zipPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to get file size: %v", err)
|
||||
}
|
||||
log.Printf("ZIP size: %d bytes", fileSize)
|
||||
|
||||
// Sign the ZIP with Sparkle EdDSA
|
||||
log.Println("Signing ZIP with Sparkle...")
|
||||
signature, err := signWithSparkle(zipPath, privateKeyPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to sign: %v", err)
|
||||
}
|
||||
log.Printf("Signature: %s", signature)
|
||||
|
||||
// Upload ZIP to S3
|
||||
remotePath := buildsRoot + zipFileName
|
||||
log.Printf("Uploading to S3: %s", remotePath)
|
||||
if err := bb.UploadFilePublic(bucketName, remotePath, zipPath); err != nil {
|
||||
log.Fatalf("Failed to upload: %v", err)
|
||||
}
|
||||
|
||||
// Also upload as "latest"
|
||||
latestPath := buildsRoot + "eagle0-latest.zip"
|
||||
log.Printf("Copying to: %s", latestPath)
|
||||
if err := bb.UploadFilePublic(bucketName, latestPath, zipPath); err != nil {
|
||||
log.Fatalf("Failed to upload latest: %v", err)
|
||||
}
|
||||
|
||||
// Update appcast.xml
|
||||
log.Println("Updating appcast.xml...")
|
||||
appcast, err := fetchAppcast(bb)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to fetch appcast: %v", err)
|
||||
}
|
||||
|
||||
// Create new item
|
||||
downloadURL := fmt.Sprintf("https://assets.eagle0.net/%s%s", buildsRoot, zipFileName)
|
||||
newItem := Item{
|
||||
Title: fmt.Sprintf("Version %s", version),
|
||||
PubDate: time.Now().Format(time.RFC1123Z),
|
||||
SparkleVersion: buildNumber,
|
||||
SparkleShortVersion: version,
|
||||
Description: "",
|
||||
Enclosure: Enclosure{
|
||||
URL: downloadURL,
|
||||
Length: fileSize,
|
||||
Type: "application/octet-stream",
|
||||
EdSig: signature,
|
||||
},
|
||||
}
|
||||
|
||||
// Prepend new item (most recent first)
|
||||
appcast.Channel.Items = append([]Item{newItem}, appcast.Channel.Items...)
|
||||
|
||||
// Keep only last 10 versions
|
||||
if len(appcast.Channel.Items) > 10 {
|
||||
appcast.Channel.Items = appcast.Channel.Items[:10]
|
||||
}
|
||||
|
||||
if err := uploadAppcast(bb, appcast); err != nil {
|
||||
log.Fatalf("Failed to upload appcast: %v", err)
|
||||
}
|
||||
|
||||
// Clean up local ZIP
|
||||
os.Remove(zipPath)
|
||||
|
||||
log.Printf("=== Mac build deployed successfully ===")
|
||||
log.Printf("Version: %s (build %s)", version, buildNumber)
|
||||
log.Printf("Download URL: %s", downloadURL)
|
||||
log.Printf("Appcast URL: https://assets.eagle0.net/%s", appcastPath)
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"github.com/nolen777/eagle0/src/main/go/net/eagle0/util/aws"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/nolen777/eagle0/src/main/go/net/eagle0/util/aws"
|
||||
)
|
||||
|
||||
const bucketName = "eagle0-windows"
|
||||
@@ -64,7 +67,7 @@ func (mm *ManifestManager) parseManifest(content string) map[string]string {
|
||||
return sections
|
||||
}
|
||||
|
||||
func (mm *ManifestManager) buildManifest(sections map[string]string) string {
|
||||
func (mm *ManifestManager) buildManifest(sections map[string]string, privateKey ed25519.PrivateKey) string {
|
||||
var manifest strings.Builder
|
||||
|
||||
manifest.WriteString(fmt.Sprintf("# Generated: %s\n", time.Now().Format(time.RFC3339)))
|
||||
@@ -90,10 +93,21 @@ func (mm *ManifestManager) buildManifest(sections map[string]string) string {
|
||||
}
|
||||
}
|
||||
|
||||
return manifest.String()
|
||||
// The content to sign is everything we've built so far
|
||||
contentToSign := manifest.String()
|
||||
|
||||
// If we have a private key, sign the manifest
|
||||
if privateKey != nil {
|
||||
signature := ed25519.Sign(privateKey, []byte(contentToSign))
|
||||
signatureB64 := base64.StdEncoding.EncodeToString(signature)
|
||||
// Prepend signature line at the very beginning
|
||||
return fmt.Sprintf("# signature=%s\n%s", signatureB64, contentToSign)
|
||||
}
|
||||
|
||||
return contentToSign
|
||||
}
|
||||
|
||||
func (mm *ManifestManager) UpdateSection(sectionName, sectionContent string) error {
|
||||
func (mm *ManifestManager) UpdateSection(sectionName, sectionContent string, privateKey ed25519.PrivateKey) error {
|
||||
log.Printf("Updating manifest section: %s", sectionName)
|
||||
|
||||
// Fetch existing manifest
|
||||
@@ -103,14 +117,14 @@ func (mm *ManifestManager) UpdateSection(sectionName, sectionContent string) err
|
||||
existing = ""
|
||||
}
|
||||
|
||||
// Parse sections
|
||||
// Parse sections (strips out old signature line if present)
|
||||
sections := mm.parseManifest(existing)
|
||||
|
||||
// Update the specified section
|
||||
sections[sectionName] = sectionContent
|
||||
|
||||
// Build new manifest
|
||||
newManifest := mm.buildManifest(sections)
|
||||
// Build new manifest (with signature if key provided)
|
||||
newManifest := mm.buildManifest(sections, privateKey)
|
||||
|
||||
// Upload updated manifest with public ACL
|
||||
err = mm.bucketBasics.UploadBytesPublic(bucketName, manifestPath, []byte(newManifest))
|
||||
@@ -118,13 +132,36 @@ func (mm *ManifestManager) UpdateSection(sectionName, sectionContent string) err
|
||||
return fmt.Errorf("failed to upload manifest: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("Successfully updated manifest section: %s", sectionName)
|
||||
if privateKey != nil {
|
||||
log.Printf("Successfully updated and signed manifest section: %s", sectionName)
|
||||
} else {
|
||||
log.Printf("Successfully updated manifest section: %s (unsigned)", sectionName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadPrivateKey(keyPath string) (ed25519.PrivateKey, error) {
|
||||
keyData, err := os.ReadFile(keyPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read private key file: %v", err)
|
||||
}
|
||||
|
||||
// Key should be base64-encoded 64-byte Ed25519 private key
|
||||
keyBytes, err := base64.StdEncoding.DecodeString(strings.TrimSpace(string(keyData)))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode private key: %v", err)
|
||||
}
|
||||
|
||||
if len(keyBytes) != ed25519.PrivateKeySize {
|
||||
return nil, fmt.Errorf("invalid private key size: expected %d bytes, got %d", ed25519.PrivateKeySize, len(keyBytes))
|
||||
}
|
||||
|
||||
return ed25519.PrivateKey(keyBytes), nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 3 {
|
||||
log.Fatal("Usage: manifest_manager <section_name> <section_content_file>")
|
||||
if len(os.Args) < 3 || len(os.Args) > 4 {
|
||||
log.Fatal("Usage: manifest_manager <section_name> <section_content_file> [private_key_file]")
|
||||
}
|
||||
|
||||
sectionName := os.Args[1]
|
||||
@@ -136,6 +173,16 @@ func main() {
|
||||
log.Fatalf("Failed to read content file: %v", err)
|
||||
}
|
||||
|
||||
// Load private key if provided
|
||||
var privateKey ed25519.PrivateKey
|
||||
if len(os.Args) == 4 {
|
||||
privateKey, err = loadPrivateKey(os.Args[3])
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load private key: %v", err)
|
||||
}
|
||||
log.Println("Ed25519 signing key loaded")
|
||||
}
|
||||
|
||||
// Create manifest manager
|
||||
mm, err := NewManifestManager()
|
||||
if err != nil {
|
||||
@@ -143,7 +190,7 @@ func main() {
|
||||
}
|
||||
|
||||
// Update the section
|
||||
err = mm.UpdateSection(sectionName, string(content))
|
||||
err = mm.UpdateSection(sectionName, string(content), privateKey)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to update manifest: %v", err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
|
||||
|
||||
go_library(
|
||||
name = "warmup_lib",
|
||||
srcs = ["main.go"],
|
||||
importpath = "github.com/nolen777/eagle0/src/main/go/net/eagle0/warmup",
|
||||
visibility = ["//visibility:private"],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:api_go_proto", # keep
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:common_go_proto",
|
||||
"@org_golang_google_grpc//:grpc",
|
||||
"@org_golang_google_grpc//credentials/insecure",
|
||||
"@org_golang_google_grpc//metadata",
|
||||
],
|
||||
)
|
||||
|
||||
go_binary(
|
||||
name = "warmup",
|
||||
embed = [":warmup_lib"],
|
||||
pure = "on",
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
# Linux x86_64 target for Docker deployment
|
||||
go_binary(
|
||||
name = "warmup_linux_amd64",
|
||||
embed = [":warmup_lib"],
|
||||
goarch = "amd64",
|
||||
goos = "linux",
|
||||
pure = "on",
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
@@ -0,0 +1,470 @@
|
||||
// Package main implements a warmup tool for Eagle server.
|
||||
//
|
||||
// This tool warms up the JIT by:
|
||||
// 1. Entering the lobby to get available leaders
|
||||
// 2. Creating a new game with 2 players (1 human, 1 AI)
|
||||
// 3. Starting a bidirectional stream for that game
|
||||
// 4. Posting an Improve command when available
|
||||
// 5. Verifying that we get ActionResults including NEW_ROUND_ACTION
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// warmup --address=localhost:40032
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
eagle "github.com/nolen777/eagle0/src/main/protobuf/net/eagle0/eagle/api/eagle"
|
||||
eaglecommon "github.com/nolen777/eagle0/src/main/protobuf/net/eagle0/eagle/common"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
var (
|
||||
address = flag.String("address", "localhost:40032", "Eagle server address (host:port)")
|
||||
timeout = flag.Duration("timeout", 60*time.Second, "Overall timeout for warmup")
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
log.SetFlags(log.Ltime | log.Lmicroseconds)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
|
||||
defer cancel()
|
||||
|
||||
if err := runWarmup(ctx); err != nil {
|
||||
log.Fatalf("Warmup failed: %v", err)
|
||||
}
|
||||
|
||||
log.Println("Warmup completed successfully!")
|
||||
}
|
||||
|
||||
func runWarmup(ctx context.Context) error {
|
||||
log.Printf("Connecting to Eagle server at %s...", *address)
|
||||
|
||||
conn, err := grpc.NewClient(*address, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
client := eagle.NewEagleClient(conn)
|
||||
|
||||
// Add warmup user header to context
|
||||
md := metadata.Pairs("x-warmup-user", "warmup-tool")
|
||||
ctx = metadata.NewOutgoingContext(ctx, md)
|
||||
|
||||
// Start the bidirectional stream
|
||||
log.Println("Starting StreamUpdates...")
|
||||
stream, err := client.StreamUpdates(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start stream: %w", err)
|
||||
}
|
||||
|
||||
// Step 1: Enter the lobby to get available leaders
|
||||
log.Println("Step 1: Entering lobby...")
|
||||
if err := stream.Send(&eagle.UpdateStreamRequest{
|
||||
RequestDetails: &eagle.UpdateStreamRequest_EnterLobbyRequest{
|
||||
EnterLobbyRequest: &eagle.EnterLobbyRequest{},
|
||||
},
|
||||
}); err != nil {
|
||||
return fmt.Errorf("failed to send lobby request: %w", err)
|
||||
}
|
||||
|
||||
lobbyResp, err := waitForResponse(stream, func(resp *eagle.UpdateStreamResponse) bool {
|
||||
return resp.GetLobbyResponse() != nil
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get lobby response: %w", err)
|
||||
}
|
||||
|
||||
lobby := lobbyResp.GetLobbyResponse()
|
||||
newGameOptions := lobby.GetNewGameOptions()
|
||||
if newGameOptions == nil || len(newGameOptions.GetAvailableLeaders()) == 0 {
|
||||
return fmt.Errorf("no available leaders in lobby response")
|
||||
}
|
||||
|
||||
leader := newGameOptions.GetAvailableLeaders()[0]
|
||||
log.Printf(" Found leader: %s", leader.GetNameTextId())
|
||||
|
||||
// Step 2: Create a new game
|
||||
log.Println("Step 2: Creating game...")
|
||||
if err := stream.Send(&eagle.UpdateStreamRequest{
|
||||
RequestDetails: &eagle.UpdateStreamRequest_CreateGameRequest{
|
||||
CreateGameRequest: &eagle.CreateGameRequest{
|
||||
DesiredLeaderTextId: leader.GetNameTextId(),
|
||||
TotalPlayerCount: 2,
|
||||
HumanPlayerCount: 1,
|
||||
},
|
||||
},
|
||||
}); err != nil {
|
||||
return fmt.Errorf("failed to send create game request: %w", err)
|
||||
}
|
||||
|
||||
createResp, err := waitForResponse(stream, func(resp *eagle.UpdateStreamResponse) bool {
|
||||
return resp.GetCreateGameResponse() != nil
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get create game response: %w", err)
|
||||
}
|
||||
|
||||
gameResp := createResp.GetCreateGameResponse()
|
||||
if gameResp.GetResult() != eagle.JoinGameResult_SUCCESS_JOIN_GAME_RESULT {
|
||||
return fmt.Errorf("failed to create game: %v", gameResp.GetResult())
|
||||
}
|
||||
|
||||
gameID := gameResp.GetGameId()
|
||||
log.Printf(" Created game: %d", gameID)
|
||||
|
||||
// Step 3: Subscribe to the game stream
|
||||
log.Println("Step 3: Subscribing to game...")
|
||||
if err := stream.Send(&eagle.UpdateStreamRequest{
|
||||
RequestDetails: &eagle.UpdateStreamRequest_StreamGameRequest_{
|
||||
StreamGameRequest: &eagle.UpdateStreamRequest_StreamGameRequest{
|
||||
GameId: gameID,
|
||||
UnfilteredResultCount: 0,
|
||||
},
|
||||
},
|
||||
}); err != nil {
|
||||
return fmt.Errorf("failed to send stream game request: %w", err)
|
||||
}
|
||||
|
||||
// Wait for initial game state with available commands
|
||||
// Note: ActionResultResponse arrives BEFORE SubscriptionAck, so we can't wait for ack first
|
||||
streamingTextCount := 0
|
||||
gotSubscriptionAck := false
|
||||
gameUpdateResp, err := waitForResponse(stream, func(resp *eagle.UpdateStreamResponse) bool {
|
||||
// Track subscription ack but don't require it before ActionResultResponse
|
||||
if resp.GetSubscriptionAck() != nil {
|
||||
gotSubscriptionAck = true
|
||||
log.Println(" Subscription acknowledged")
|
||||
}
|
||||
gu := resp.GetGameUpdate()
|
||||
if gu == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check what type of update this is
|
||||
switch {
|
||||
case gu.GetStreamingTextResponse() != nil:
|
||||
streamingTextCount++
|
||||
if streamingTextCount%100 == 0 {
|
||||
log.Printf(" Received %d streaming text updates so far...", streamingTextCount)
|
||||
}
|
||||
case gu.GetShardokActionResultResponse() != nil:
|
||||
log.Printf(" Got ShardokActionResultResponse")
|
||||
case gu.GetErrorResponse() != nil:
|
||||
log.Printf(" Got ErrorResponse: %v", gu.GetErrorResponse())
|
||||
case gu.GetActionResultResponse() != nil:
|
||||
log.Printf(" Got ActionResultResponse!")
|
||||
default:
|
||||
log.Printf(" Got GameUpdate with no recognized content")
|
||||
}
|
||||
|
||||
ar := gu.GetActionResultResponse()
|
||||
if ar == nil {
|
||||
return false
|
||||
}
|
||||
if ar.GetAvailableCommands() == nil {
|
||||
log.Printf(" ActionResultResponse has no AvailableCommands (has %d results)", len(ar.GetActionResultViews()))
|
||||
return false
|
||||
}
|
||||
log.Printf(" Found AvailableCommands with %d provinces", len(ar.GetAvailableCommands().GetCommandsByProvince()))
|
||||
return true
|
||||
})
|
||||
log.Printf(" Total streaming text updates received: %d, got subscription ack: %v", streamingTextCount, gotSubscriptionAck)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get initial game state: %w", err)
|
||||
}
|
||||
|
||||
gameUpdate := gameUpdateResp.GetGameUpdate()
|
||||
actionResult := gameUpdate.GetActionResultResponse()
|
||||
availableCommands := actionResult.GetAvailableCommands()
|
||||
|
||||
// Get the token from available commands
|
||||
eagleToken := availableCommands.GetToken()
|
||||
|
||||
// Get commands by province from map
|
||||
commandsByProvince := availableCommands.GetCommandsByProvince()
|
||||
log.Printf(" Got game state with %d provinces having commands", len(commandsByProvince))
|
||||
|
||||
// Step 4: Find an Improve command and post it
|
||||
log.Println("Step 4: Looking for Improve command...")
|
||||
|
||||
var improveCmd *eagle.AvailableCommand
|
||||
var provinceID int32
|
||||
var actingHeroID int32
|
||||
|
||||
for pid, provCmds := range commandsByProvince {
|
||||
for _, cmd := range provCmds.GetCommands() {
|
||||
if ic := cmd.GetImproveCommand(); ic != nil {
|
||||
improveCmd = cmd
|
||||
provinceID = pid
|
||||
actingHeroID = ic.GetRecommendedHeroId()
|
||||
break
|
||||
}
|
||||
}
|
||||
if improveCmd != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if improveCmd == nil {
|
||||
log.Println(" No Improve command available, looking for any command...")
|
||||
// Try to find any province with commands
|
||||
for pid, provCmds := range commandsByProvince {
|
||||
if len(provCmds.GetCommands()) > 0 {
|
||||
provinceID = pid
|
||||
log.Printf(" Found command group for province %d with %d commands", pid, len(provCmds.GetCommands()))
|
||||
break
|
||||
}
|
||||
}
|
||||
// We'll still try to post an Improve command even if not explicitly available
|
||||
} else {
|
||||
log.Printf(" Found Improve command for province %d with recommended hero %d", provinceID, actingHeroID)
|
||||
}
|
||||
|
||||
// Post the Improve command
|
||||
log.Printf(" Posting Improve command with token=%d province=%d heroId=%d...", eagleToken, provinceID, actingHeroID)
|
||||
if err := stream.Send(&eagle.UpdateStreamRequest{
|
||||
RequestDetails: &eagle.UpdateStreamRequest_PostCommandRequest{
|
||||
PostCommandRequest: &eagle.PostCommandRequest{
|
||||
GameId: gameID,
|
||||
Command: &eagle.UniversalCommand{
|
||||
SealedValue: &eagle.UniversalCommand_EagleCommand{
|
||||
EagleCommand: &eagle.EagleCommand{
|
||||
ProvinceId: provinceID,
|
||||
EagleToken: eagleToken,
|
||||
Command: &eagle.SelectedCommand{
|
||||
SealedValue: &eagle.SelectedCommand_ImproveCommand{
|
||||
ImproveCommand: &eagle.ImproveSelectedCommand{
|
||||
ImprovementType: eaglecommon.ImprovementType_ECONOMY,
|
||||
ActingHeroId: actingHeroID,
|
||||
LockType: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}); err != nil {
|
||||
return fmt.Errorf("failed to send post command request: %w", err)
|
||||
}
|
||||
|
||||
// Step 5: Verify we get ActionResults
|
||||
log.Println("Step 5: Waiting for command response and collecting results...")
|
||||
|
||||
// Track results while waiting for PostCommandResponse
|
||||
// ActionResultResponse arrives as GameUpdate BEFORE PostCommandResponse
|
||||
foundNewRound := false
|
||||
resultCount := 0
|
||||
commandCount := 0
|
||||
postStatus := eagle.PostCommandResponse_UNKNOWN
|
||||
|
||||
_, err = waitForResponse(stream, func(resp *eagle.UpdateStreamResponse) bool {
|
||||
if resp.GetPostCommandResponse() != nil {
|
||||
log.Printf(" Got PostCommandResponse: %v", resp.GetPostCommandResponse().GetStatus())
|
||||
postStatus = resp.GetPostCommandResponse().GetStatus()
|
||||
return true
|
||||
}
|
||||
// Process GameUpdates while waiting - they contain our action results!
|
||||
if gu := resp.GetGameUpdate(); gu != nil {
|
||||
if ar := gu.GetActionResultResponse(); ar != nil {
|
||||
for _, arv := range ar.GetActionResultViews() {
|
||||
resultCount++
|
||||
if arv.GetType() == eaglecommon.ActionResultType_NEW_ROUND_ACTION {
|
||||
foundNewRound = true
|
||||
log.Printf(" Found NEW_ROUND_ACTION (date change)!")
|
||||
}
|
||||
}
|
||||
if ar.GetAvailableCommands() != nil {
|
||||
commandCount = len(ar.GetAvailableCommands().GetCommandsByProvince())
|
||||
log.Printf(" Got %d action results, %d provinces with commands available", len(ar.GetActionResultViews()), commandCount)
|
||||
}
|
||||
}
|
||||
} else if resp.GetSubscriptionAck() != nil {
|
||||
log.Println(" (skipping late SubscriptionAck)")
|
||||
}
|
||||
return false
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get post command response: %w", err)
|
||||
}
|
||||
|
||||
if postStatus == eagle.PostCommandResponse_SUCCESS {
|
||||
log.Println(" Command accepted!")
|
||||
} else if postStatus == eagle.PostCommandResponse_BAD_TOKEN {
|
||||
log.Printf(" Command rejected: bad token")
|
||||
} else {
|
||||
log.Printf(" Command status: %v (continuing anyway - JIT warming up)", postStatus)
|
||||
}
|
||||
|
||||
// Continue collecting any remaining action results with a timeout
|
||||
log.Println(" Collecting any remaining action results...")
|
||||
resultCtx, resultCancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer resultCancel()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-resultCtx.Done():
|
||||
log.Println(" Timeout waiting for more results (this is OK)")
|
||||
goto done
|
||||
default:
|
||||
}
|
||||
|
||||
resp, err := receiveWithTimeout(stream, 2*time.Second)
|
||||
if err != nil {
|
||||
if err == errTimeout {
|
||||
log.Println(" No more results received")
|
||||
goto done
|
||||
}
|
||||
return fmt.Errorf("error receiving: %w", err)
|
||||
}
|
||||
|
||||
gu := resp.GetGameUpdate()
|
||||
if gu == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
ar := gu.GetActionResultResponse()
|
||||
if ar == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, arv := range ar.GetActionResultViews() {
|
||||
resultCount++
|
||||
if arv.GetType() == eaglecommon.ActionResultType_NEW_ROUND_ACTION {
|
||||
foundNewRound = true
|
||||
log.Printf(" Found NEW_ROUND_ACTION (date change)!")
|
||||
}
|
||||
}
|
||||
|
||||
if ar.GetAvailableCommands() != nil {
|
||||
commandCount = len(ar.GetAvailableCommands().GetCommandsByProvince())
|
||||
log.Printf(" Got %d action results, %d provinces with commands available", len(ar.GetActionResultViews()), commandCount)
|
||||
}
|
||||
}
|
||||
|
||||
done:
|
||||
// Verify results
|
||||
log.Println("Verification:")
|
||||
log.Printf(" Total action results received: %d", resultCount)
|
||||
log.Printf(" Found NEW_ROUND_ACTION (date change): %v", foundNewRound)
|
||||
log.Printf(" New commands available: %d provinces", commandCount)
|
||||
log.Printf(" Post command status: %v", postStatus)
|
||||
|
||||
// Step 6: Drop the game to clean up
|
||||
log.Println("Step 6: Dropping test game...")
|
||||
if err := stream.Send(&eagle.UpdateStreamRequest{
|
||||
RequestDetails: &eagle.UpdateStreamRequest_DropGameRequest{
|
||||
DropGameRequest: &eagle.DropGameRequest{
|
||||
GameId: gameID,
|
||||
},
|
||||
},
|
||||
}); err != nil {
|
||||
log.Printf("Warning: failed to send drop game request: %v", err)
|
||||
} else {
|
||||
// Wait briefly for drop response
|
||||
_, err := waitForResponseWithTimeout(stream, func(resp *eagle.UpdateStreamResponse) bool {
|
||||
return resp.GetDropGameResponse() != nil
|
||||
}, 5*time.Second)
|
||||
if err != nil {
|
||||
log.Printf("Warning: failed to get drop game response: %v", err)
|
||||
} else {
|
||||
log.Println(" Game dropped successfully")
|
||||
}
|
||||
}
|
||||
|
||||
// Report success based on what we achieved
|
||||
if resultCount == 0 {
|
||||
return fmt.Errorf("no action results received")
|
||||
}
|
||||
if commandCount == 0 {
|
||||
return fmt.Errorf("no new commands received after posting command")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// waitForResponse waits for a response matching the predicate
|
||||
func waitForResponse(stream eagle.Eagle_StreamUpdatesClient, matches func(*eagle.UpdateStreamResponse) bool) (*eagle.UpdateStreamResponse, error) {
|
||||
return waitForResponseWithTimeout(stream, matches, 30*time.Second)
|
||||
}
|
||||
|
||||
func waitForResponseWithTimeout(stream eagle.Eagle_StreamUpdatesClient, matches func(*eagle.UpdateStreamResponse) bool, timeout time.Duration) (*eagle.UpdateStreamResponse, error) {
|
||||
deadline := time.Now().Add(timeout)
|
||||
|
||||
for time.Now().Before(deadline) {
|
||||
remaining := time.Until(deadline)
|
||||
resp, err := receiveWithTimeout(stream, remaining)
|
||||
if err != nil {
|
||||
if err == errTimeout {
|
||||
return nil, fmt.Errorf("timeout waiting for response")
|
||||
}
|
||||
log.Printf(" [DEBUG] Error receiving: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Log what type of response we got
|
||||
switch resp.GetResponseDetails().(type) {
|
||||
case *eagle.UpdateStreamResponse_LobbyResponse:
|
||||
log.Printf(" [DEBUG] Got LobbyResponse")
|
||||
case *eagle.UpdateStreamResponse_CreateGameResponse:
|
||||
log.Printf(" [DEBUG] Got CreateGameResponse")
|
||||
case *eagle.UpdateStreamResponse_GameUpdate:
|
||||
log.Printf(" [DEBUG] Got GameUpdate")
|
||||
case *eagle.UpdateStreamResponse_SubscriptionAck:
|
||||
log.Printf(" [DEBUG] Got SubscriptionAck")
|
||||
case *eagle.UpdateStreamResponse_PostCommandResponse:
|
||||
log.Printf(" [DEBUG] Got PostCommandResponse")
|
||||
case *eagle.UpdateStreamResponse_DropGameResponse:
|
||||
log.Printf(" [DEBUG] Got DropGameResponse")
|
||||
default:
|
||||
log.Printf(" [DEBUG] Got unknown response type: %T", resp.GetResponseDetails())
|
||||
}
|
||||
|
||||
if matches(resp) {
|
||||
return resp, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("timeout waiting for response")
|
||||
}
|
||||
|
||||
var errTimeout = fmt.Errorf("receive timeout")
|
||||
|
||||
func receiveWithTimeout(stream eagle.Eagle_StreamUpdatesClient, timeout time.Duration) (*eagle.UpdateStreamResponse, error) {
|
||||
type result struct {
|
||||
resp *eagle.UpdateStreamResponse
|
||||
err error
|
||||
}
|
||||
|
||||
ch := make(chan result, 1)
|
||||
|
||||
go func() {
|
||||
resp, err := stream.Recv()
|
||||
ch <- result{resp, err}
|
||||
}()
|
||||
|
||||
select {
|
||||
case r := <-ch:
|
||||
if r.err != nil {
|
||||
if r.err == io.EOF {
|
||||
return nil, fmt.Errorf("stream closed by server")
|
||||
}
|
||||
return nil, r.err
|
||||
}
|
||||
return r.resp, nil
|
||||
case <-time.After(timeout):
|
||||
return nil, errTimeout
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
load("@build_bazel_rules_swift//proto:proto.bzl", "swift_proto_library")
|
||||
load("@grpc//bazel:cc_grpc_library.bzl", "cc_grpc_library")
|
||||
load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
|
||||
load("@rules_cc//cc:defs.bzl", "cc_proto_library")
|
||||
@@ -57,16 +56,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "hostility_swift_proto",
|
||||
protos = [":hostility_proto"],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:__subpackages__",
|
||||
"//src/main/protobuf/net/eagle0/eagle/views:__pkg__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "hostility_scala_proto",
|
||||
visibility = [
|
||||
@@ -183,15 +172,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "victory_condition_swift_proto",
|
||||
protos = [":victory_condition_proto"],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0:__subpackages__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "victory_condition_scala_proto",
|
||||
visibility = ["//visibility:public"],
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
load("@build_bazel_rules_swift//proto:proto.bzl", "swift_proto_library")
|
||||
load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
|
||||
load("@rules_proto//proto:defs.bzl", "proto_library")
|
||||
load("@rules_scala//scala_proto:scala_proto.bzl", "scala_proto_library")
|
||||
@@ -50,26 +49,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "available_command_swift_proto",
|
||||
protos = [":available_command_proto"],
|
||||
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/api/command/util:armed_battalion_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api/command/util:attack_decision_type_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api/command/util:captured_hero_option_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api/command/util:control_weather_type_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api/command/util:diplomacy_option_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api/command/util:prisoner_management_type_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api/command/util:province_orders_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:battalion_type_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:combat_unit_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:diplomacy_offer_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:improvement_type_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:tribute_amount_swift_proto",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "available_command_scala_proto",
|
||||
visibility = [
|
||||
@@ -108,16 +87,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "command_swift_proto",
|
||||
protos = [":command_proto"],
|
||||
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
|
||||
deps = [
|
||||
":available_command_swift_proto",
|
||||
":selected_command_swift_proto",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "command_scala_proto",
|
||||
visibility = [
|
||||
@@ -153,30 +122,6 @@ proto_library(
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "selected_command_swift_proto",
|
||||
protos = [":selected_command_proto"],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0/eagle:__subpackages__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/api/command/util:armed_battalion_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api/command/util:attack_decision_type_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api/command/util:captured_hero_option_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api/command/util:control_weather_type_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api/command/util:diplomacy_option_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api/command/util:prisoner_management_type_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api/command/util:province_orders_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:battalion_type_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:combat_unit_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:diplomacy_offer_status_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:diplomacy_offer_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:improvement_type_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:tribute_amount_swift_proto",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "selected_command_scala_proto",
|
||||
visibility = [
|
||||
|
||||
@@ -39,6 +39,12 @@ service Admin {
|
||||
|
||||
// Resend invitation email
|
||||
rpc ResendInvitation(ResendInvitationRequest) returns (ResendInvitationResponse) {}
|
||||
|
||||
// Delete a user permanently
|
||||
rpc DeleteUser(DeleteUserRequest) returns (DeleteUserResponse) {}
|
||||
|
||||
// Delete an invitation (only revoked/expired/redeemed invitations can be deleted)
|
||||
rpc DeleteInvitation(DeleteInvitationRequest) returns (DeleteInvitationResponse) {}
|
||||
}
|
||||
|
||||
// Request to list users
|
||||
@@ -173,3 +179,23 @@ message ResendInvitationResponse {
|
||||
bool success = 1;
|
||||
string error_message = 2;
|
||||
}
|
||||
|
||||
// Delete user request
|
||||
message DeleteUserRequest {
|
||||
string user_id = 1;
|
||||
}
|
||||
|
||||
message DeleteUserResponse {
|
||||
bool success = 1;
|
||||
string error_message = 2;
|
||||
}
|
||||
|
||||
// Delete invitation request (only revoked/expired/redeemed can be deleted)
|
||||
message DeleteInvitationRequest {
|
||||
string invitation_code = 1;
|
||||
}
|
||||
|
||||
message DeleteInvitationResponse {
|
||||
bool success = 1;
|
||||
string error_message = 2;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,7 @@
|
||||
load("@build_bazel_rules_swift//proto:proto.bzl", "swift_proto_library")
|
||||
load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
|
||||
load("@rules_proto//proto:defs.bzl", "proto_library")
|
||||
load("@rules_scala//scala_proto:scala_proto.bzl", "scala_proto_library")
|
||||
|
||||
swift_proto_library(
|
||||
name = "appropriate_battalions_swift_proto",
|
||||
protos = [":appropriate_battalions_proto"],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:__subpackages__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
deps = [],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "appropriate_battalions_scala_proto",
|
||||
visibility = [
|
||||
@@ -32,16 +21,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "armed_battalion_swift_proto",
|
||||
protos = [":armed_battalion_proto"],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:__subpackages__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
deps = [],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "armed_battalion_scala_proto",
|
||||
visibility = [
|
||||
@@ -63,16 +42,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "army_stats_swift_proto",
|
||||
protos = [":army_stats_proto"],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:__subpackages__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
deps = [],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "army_stats_scala_proto",
|
||||
visibility = [
|
||||
@@ -93,18 +62,6 @@ proto_library(
|
||||
deps = ["//src/main/protobuf/net/eagle0/common:hostility_proto"],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "attack_decision_type_swift_proto",
|
||||
protos = [":attack_decision_type_proto"],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:__subpackages__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:tribute_amount_swift_proto",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "attack_decision_type_scala_proto",
|
||||
visibility = [
|
||||
@@ -127,16 +84,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "battalion_with_food_cost_swift_proto",
|
||||
protos = [":battalion_with_food_cost_proto"],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:__subpackages__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
deps = [],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "battalion_with_food_cost_scala_proto",
|
||||
visibility = [
|
||||
@@ -158,16 +105,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "captured_hero_option_swift_proto",
|
||||
protos = [":captured_hero_option_proto"],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:__subpackages__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
deps = [],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "captured_hero_option_scala_proto",
|
||||
visibility = [
|
||||
@@ -189,16 +126,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "control_weather_type_swift_proto",
|
||||
protos = [":control_weather_type_proto"],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:__subpackages__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
deps = [],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "control_weather_type_scala_proto",
|
||||
visibility = [
|
||||
@@ -220,18 +147,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "diplomacy_option_swift_proto",
|
||||
protos = [":diplomacy_option_proto"],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:__subpackages__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:diplomacy_offer_swift_proto",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "diplomacy_option_scala_proto",
|
||||
visibility = [
|
||||
@@ -254,19 +169,6 @@ proto_library(
|
||||
deps = ["//src/main/protobuf/net/eagle0/eagle/common:diplomacy_offer_proto"],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "expanded_combat_unit_swift_proto",
|
||||
protos = [":expanded_combat_unit_proto"],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:__subpackages__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/views:battalion_view_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/views:hero_view_swift_proto",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "expanded_combat_unit_scala_proto",
|
||||
visibility = [
|
||||
@@ -292,20 +194,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "expanded_unaffiliated_hero_swift_proto",
|
||||
protos = [":expanded_unaffiliated_hero_proto"],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:__subpackages__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:unaffiliated_hero_quest_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:unaffiliated_hero_type_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/views:hero_view_swift_proto",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "expanded_unaffiliated_hero_scala_proto",
|
||||
visibility = [
|
||||
@@ -332,16 +220,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "prisoner_management_type_swift_proto",
|
||||
protos = [":prisoner_management_type_proto"],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:__subpackages__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
deps = [],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "prisoner_management_type_scala_proto",
|
||||
visibility = [
|
||||
@@ -363,18 +241,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "province_orders_swift_proto",
|
||||
protos = [":province_orders_proto"],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:__subpackages__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:province_order_type_swift_proto",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "province_orders_scala_proto",
|
||||
visibility = [
|
||||
|
||||
@@ -45,6 +45,9 @@ service Eagle {
|
||||
rpc DeleteGame(DeleteGameRequest) returns (DeleteGameResponse) {}
|
||||
rpc CheckGameExists(CheckGameExistsRequest) returns (CheckGameExistsResponse) {}
|
||||
rpc DownloadGameSave(DownloadGameSaveRequest) returns (stream DownloadGameSaveResponse) {}
|
||||
|
||||
// Server lifecycle management (for blue-green deployments)
|
||||
rpc ReloadGames(ReloadGamesRequest) returns (ReloadGamesResponse) {}
|
||||
}
|
||||
|
||||
message PostCommandRequest {
|
||||
@@ -605,3 +608,12 @@ message DownloadGameSaveRequest {
|
||||
message DownloadGameSaveResponse {
|
||||
bytes chunk = 1; // Chunk of zip file data
|
||||
}
|
||||
|
||||
// Server lifecycle management (for blue-green deployments)
|
||||
message ReloadGamesRequest {}
|
||||
|
||||
message ReloadGamesResponse {
|
||||
bool success = 1;
|
||||
string error_message = 2;
|
||||
int32 games_reloaded = 3; // Number of games that were reloaded from disk
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
load("@build_bazel_rules_swift//proto:proto.bzl", "swift_proto_library")
|
||||
load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
|
||||
load("@rules_proto//proto:defs.bzl", "proto_library")
|
||||
load("@rules_scala//scala_proto:scala_proto.bzl", "scala_proto_library")
|
||||
@@ -8,19 +7,6 @@ package(default_visibility = [
|
||||
"//src/test/scala/net/eagle0:__subpackages__",
|
||||
])
|
||||
|
||||
swift_proto_library(
|
||||
name = "action_result_notification_details_swift_proto",
|
||||
protos = [":action_result_notification_details_proto"],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0:__subpackages__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
":profession_swift_proto",
|
||||
":unaffiliated_hero_quest_swift_proto",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "action_result_notification_details_scala_proto",
|
||||
deps = [":action_result_notification_details_proto"],
|
||||
@@ -41,15 +27,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "action_result_type_swift_proto",
|
||||
protos = [":action_result_type_proto"],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0:__subpackages__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "action_result_type_scala_proto",
|
||||
deps = [":action_result_type_proto"],
|
||||
@@ -65,15 +42,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "battalion_type_swift_proto",
|
||||
protos = [":battalion_type_proto"],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0:__subpackages__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "battalion_type_scala_proto",
|
||||
deps = [":battalion_type_proto"],
|
||||
@@ -89,15 +57,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "beast_info_swift_proto",
|
||||
protos = [":beast_info_proto"],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0:__subpackages__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "beast_info_scala_proto",
|
||||
deps = [":beast_info_proto"],
|
||||
@@ -113,16 +72,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "chronicle_entry_swift_proto",
|
||||
protos = [":chronicle_entry_proto"],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0:__subpackages__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
deps = [":date_swift_proto"],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "chronicle_entry_scala_proto",
|
||||
deps = ["chronicle_entry_proto"],
|
||||
@@ -141,12 +90,18 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "combat_unit_swift_proto",
|
||||
protos = [":combat_unit_proto"],
|
||||
scala_proto_library(
|
||||
name = "command_type_scala_proto",
|
||||
deps = [":command_type_proto"],
|
||||
)
|
||||
|
||||
proto_library(
|
||||
name = "command_type_proto",
|
||||
srcs = [
|
||||
"command_type.proto",
|
||||
],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0:__subpackages__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -166,15 +121,6 @@ proto_library(
|
||||
deps = ["@com_google_protobuf//:wrappers_proto"],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "date_swift_proto",
|
||||
protos = [":date_proto"],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0:__subpackages__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "date_scala_proto",
|
||||
deps = [":date_proto"],
|
||||
@@ -190,19 +136,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "diplomacy_offer_swift_proto",
|
||||
protos = [":diplomacy_offer_proto"],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0:__subpackages__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
":date_swift_proto",
|
||||
":diplomacy_offer_status_swift_proto",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "diplomacy_offer_scala_proto",
|
||||
visibility = [
|
||||
@@ -226,15 +159,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "diplomacy_offer_status_swift_proto",
|
||||
protos = [":diplomacy_offer_status_proto"],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0:__subpackages__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "diplomacy_offer_status_scala_proto",
|
||||
deps = [":diplomacy_offer_status_proto"],
|
||||
@@ -250,15 +174,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "gender_swift_proto",
|
||||
protos = [":gender_proto"],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0:__subpackages__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "gender_scala_proto",
|
||||
deps = [":gender_proto"],
|
||||
@@ -272,18 +187,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "hero_backstory_version_swift_proto",
|
||||
protos = [":hero_backstory_version_proto"],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0:__subpackages__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
":date_swift_proto",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "hero_backstory_version_scala_proto",
|
||||
deps = [":hero_backstory_version_proto"],
|
||||
@@ -300,15 +203,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "improvement_type_swift_proto",
|
||||
protos = [":improvement_type_proto"],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0:__subpackages__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "improvement_type_scala_proto",
|
||||
visibility = [
|
||||
@@ -330,15 +224,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "profession_swift_proto",
|
||||
protos = [":profession_proto"],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0:__subpackages__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "profession_scala_proto",
|
||||
deps = [":profession_proto"],
|
||||
@@ -354,19 +239,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "province_event_swift_proto",
|
||||
protos = [":province_event_proto"],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0:__subpackages__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
":beast_info_swift_proto",
|
||||
":date_swift_proto",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "province_event_scala_proto",
|
||||
deps = [":province_event_proto"],
|
||||
@@ -386,15 +258,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "province_order_type_swift_proto",
|
||||
protos = [":province_order_type_proto"],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0:__subpackages__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "province_order_type_scala_proto",
|
||||
deps = [":province_order_type_proto"],
|
||||
@@ -410,18 +273,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "recruitment_info_swift_proto",
|
||||
protos = [":recruitment_info_proto"],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0:__subpackages__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
":unaffiliated_hero_quest_swift_proto",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "recruitment_info_scala_proto",
|
||||
deps = [":recruitment_info_proto"],
|
||||
@@ -438,15 +289,6 @@ proto_library(
|
||||
deps = [":unaffiliated_hero_quest_proto"],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "round_phase_swift_proto",
|
||||
protos = [":round_phase_proto"],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0:__subpackages__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "round_phase_scala_proto",
|
||||
deps = [":round_phase_proto"],
|
||||
@@ -462,15 +304,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "tribute_amount_swift_proto",
|
||||
protos = [":tribute_amount_proto"],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0:__subpackages__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "tribute_amount_scala_proto",
|
||||
visibility = [
|
||||
@@ -494,16 +327,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "unaffiliated_hero_quest_swift_proto",
|
||||
protos = [":unaffiliated_hero_quest_proto"],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0:__subpackages__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
deps = [":battalion_type_swift_proto"],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "unaffiliated_hero_quest_scala_proto",
|
||||
deps = [":unaffiliated_hero_quest_proto"],
|
||||
@@ -520,16 +343,6 @@ proto_library(
|
||||
deps = [":battalion_type_proto"],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "unaffiliated_hero_type_swift_proto",
|
||||
protos = [":unaffiliated_hero_type_proto"],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0:__subpackages__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
deps = [],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "unaffiliated_hero_type_scala_proto",
|
||||
deps = [":unaffiliated_hero_type_proto"],
|
||||
@@ -555,6 +368,7 @@ go_proto_library(
|
||||
":beast_info_proto",
|
||||
":chronicle_entry_proto",
|
||||
":combat_unit_proto",
|
||||
":command_type_proto",
|
||||
":date_proto",
|
||||
":diplomacy_offer_proto",
|
||||
":diplomacy_offer_status_proto",
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package net.eagle0.eagle.common.command_type;
|
||||
|
||||
option java_multiple_files = true;
|
||||
option java_package = "net.eagle0.eagle.common.command_type";
|
||||
|
||||
// Enum representing the type of a command, without any command-specific data.
|
||||
// Used to track which command type was last executed on a province.
|
||||
enum CommandType {
|
||||
COMMAND_TYPE_UNKNOWN = 0;
|
||||
COMMAND_TYPE_ALMS = 1;
|
||||
COMMAND_TYPE_APPREHEND_OUTLAW = 2;
|
||||
COMMAND_TYPE_ARM_TROOPS = 3;
|
||||
COMMAND_TYPE_ATTACK_DECISION = 4;
|
||||
COMMAND_TYPE_CONTROL_WEATHER = 5;
|
||||
COMMAND_TYPE_DECLINE_QUEST = 6;
|
||||
COMMAND_TYPE_DEFEND = 7;
|
||||
COMMAND_TYPE_DIPLOMACY = 8;
|
||||
COMMAND_TYPE_DIVINE = 9;
|
||||
COMMAND_TYPE_EXILE_VASSAL = 10;
|
||||
COMMAND_TYPE_FEAST = 11;
|
||||
COMMAND_TYPE_FREE_FOR_ALL_DECISION = 12;
|
||||
COMMAND_TYPE_HANDLE_CAPTURED_HERO = 13;
|
||||
COMMAND_TYPE_HANDLE_RIOT_CRACK_DOWN = 14;
|
||||
COMMAND_TYPE_HANDLE_RIOT_DO_NOTHING = 15;
|
||||
COMMAND_TYPE_HANDLE_RIOT_GIVE = 16;
|
||||
COMMAND_TYPE_HERO_GIFT = 17;
|
||||
COMMAND_TYPE_IMPROVE = 18;
|
||||
COMMAND_TYPE_ISSUE_ORDERS = 19;
|
||||
COMMAND_TYPE_MANAGE_PRISONERS = 20;
|
||||
COMMAND_TYPE_MARCH = 21;
|
||||
COMMAND_TYPE_ORGANIZE_TROOPS = 22;
|
||||
COMMAND_TYPE_PLEASE_RECRUIT_ME = 23;
|
||||
COMMAND_TYPE_RECON = 24;
|
||||
COMMAND_TYPE_RECRUIT_HEROES = 25;
|
||||
COMMAND_TYPE_RESOLVE_ALLIANCE_OFFER = 26;
|
||||
COMMAND_TYPE_RESOLVE_BREAK_ALLIANCE = 27;
|
||||
COMMAND_TYPE_RESOLVE_INVITATION = 28;
|
||||
COMMAND_TYPE_RESOLVE_RANSOM_OFFER = 29;
|
||||
COMMAND_TYPE_RESOLVE_TRUCE_OFFER = 30;
|
||||
COMMAND_TYPE_RESOLVE_TRIBUTE = 31;
|
||||
COMMAND_TYPE_REST = 32;
|
||||
COMMAND_TYPE_RETURN = 33;
|
||||
COMMAND_TYPE_SEND_SUPPLIES = 34;
|
||||
COMMAND_TYPE_START_EPIDEMIC = 35;
|
||||
COMMAND_TYPE_SUPPRESS_BEASTS = 36;
|
||||
COMMAND_TYPE_SWEAR_BROTHERHOOD = 37;
|
||||
COMMAND_TYPE_TRADE = 38;
|
||||
COMMAND_TYPE_TRAIN = 39;
|
||||
COMMAND_TYPE_TRAVEL = 40;
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
load("@build_bazel_rules_swift//proto:proto.bzl", "swift_proto_library")
|
||||
load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
|
||||
load("@rules_proto//proto:defs.bzl", "proto_library")
|
||||
load("@rules_scala//scala_proto:scala_proto.bzl", "scala_proto_library")
|
||||
@@ -8,31 +7,6 @@ package(default_visibility = [
|
||||
"//src/test/scala/net/eagle0:__subpackages__",
|
||||
])
|
||||
|
||||
swift_proto_library(
|
||||
name = "action_result_swift_proto",
|
||||
protos = [":action_result_proto"],
|
||||
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
|
||||
deps = [
|
||||
":battalion_swift_proto",
|
||||
":changed_faction_swift_proto",
|
||||
":changed_hero_swift_proto",
|
||||
":changed_province_swift_proto",
|
||||
":faction_swift_proto",
|
||||
":hero_swift_proto",
|
||||
":llm_request_swift_proto",
|
||||
":llm_response_swift_proto",
|
||||
":province_swift_proto",
|
||||
":shardok_battle_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:action_result_notification_details_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:action_result_type_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:battalion_type_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:chronicle_entry_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:date_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:round_phase_swift_proto",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "action_result_scala_proto",
|
||||
deps = [":action_result_proto"],
|
||||
@@ -58,24 +32,13 @@ proto_library(
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:action_result_type_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:battalion_type_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:chronicle_entry_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:command_type_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:date_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:round_phase_proto",
|
||||
"@com_google_protobuf//:wrappers_proto",
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "army_swift_proto",
|
||||
protos = [":army_proto"],
|
||||
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
|
||||
deps = [
|
||||
":supplies_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:combat_unit_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:province_order_type_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:tribute_amount_swift_proto",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "army_scala_proto",
|
||||
deps = [":army_proto"],
|
||||
@@ -94,15 +57,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "battalion_swift_proto",
|
||||
protos = [":battalion_proto"],
|
||||
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:battalion_type_swift_proto",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "battalion_scala_proto",
|
||||
deps = [":battalion_proto"],
|
||||
@@ -116,12 +70,6 @@ proto_library(
|
||||
deps = ["//src/main/protobuf/net/eagle0/eagle/common:battalion_type_proto"],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "battle_revelation_swift_proto",
|
||||
protos = [":battle_revelation_proto"],
|
||||
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "battle_revelation_scala_proto",
|
||||
deps = [":battle_revelation_proto"],
|
||||
@@ -134,20 +82,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "changed_faction_swift_proto",
|
||||
protos = [":changed_faction_proto"],
|
||||
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
|
||||
deps = [
|
||||
":changed_hero_swift_proto",
|
||||
":changed_province_swift_proto",
|
||||
":faction_relationship_swift_proto",
|
||||
":faction_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:diplomacy_offer_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/views:province_view_swift_proto",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "changed_faction_scala_proto",
|
||||
deps = [
|
||||
@@ -169,16 +103,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "changed_hero_swift_proto",
|
||||
protos = [":changed_hero_proto"],
|
||||
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
|
||||
deps = [
|
||||
":event_for_hero_backstory_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:profession_swift_proto",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "changed_hero_scala_proto",
|
||||
deps = ["changed_hero_proto"],
|
||||
@@ -196,23 +120,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "changed_province_swift_proto",
|
||||
protos = [":changed_province_proto"],
|
||||
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
|
||||
deps = [
|
||||
":deferred_change_swift_proto",
|
||||
":province_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:improvement_type_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:province_event_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:province_order_type_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:army_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:battle_revelation_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:supplies_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:unaffiliated_hero_swift_proto",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "changed_province_scala_proto",
|
||||
deps = ["changed_province_proto"],
|
||||
@@ -237,15 +144,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "client_text_swift_proto",
|
||||
protos = [":client_text_proto"],
|
||||
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_swift_proto",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "client_text_scala_proto",
|
||||
deps = ["client_text_proto"],
|
||||
@@ -259,12 +157,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "deferred_change_swift_proto",
|
||||
protos = [":deferred_change_proto"],
|
||||
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "deferred_change_scala_proto",
|
||||
deps = [":deferred_change_proto"],
|
||||
@@ -275,15 +167,6 @@ proto_library(
|
||||
srcs = ["deferred_change.proto"],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "event_for_chronicle_swift_proto",
|
||||
protos = [":event_for_chronicle_proto"],
|
||||
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:date_swift_proto",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "event_for_chronicle_scala_proto",
|
||||
deps = [":event_for_chronicle_proto"],
|
||||
@@ -298,19 +181,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "event_for_hero_backstory_swift_proto",
|
||||
protos = [":event_for_hero_backstory_proto"],
|
||||
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:date_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:diplomacy_offer_status_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:profession_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:unaffiliated_hero_quest_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/views:battalion_view_swift_proto",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "event_for_hero_backstory_scala_proto",
|
||||
deps = [":event_for_hero_backstory_proto"],
|
||||
@@ -328,19 +198,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "faction_swift_proto",
|
||||
protos = [":faction_proto"],
|
||||
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
|
||||
deps = [
|
||||
":faction_relationship_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:action_result_notification_details_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:date_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:diplomacy_offer_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/views:province_view_swift_proto",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "faction_scala_proto",
|
||||
deps = [":faction_proto"],
|
||||
@@ -359,15 +216,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "faction_relationship_swift_proto",
|
||||
protos = [":faction_relationship_proto"],
|
||||
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:date_swift_proto",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "faction_relationship_scala_proto",
|
||||
deps = [":faction_relationship_proto"],
|
||||
@@ -384,17 +232,6 @@ proto_library(
|
||||
deps = ["//src/main/protobuf/net/eagle0/eagle/common:date_proto"],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "game_parameters_swift_proto",
|
||||
protos = [":game_parameters_proto"],
|
||||
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:battalion_type_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:province_order_type_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:battalion_swift_proto",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "game_parameters_scala_proto",
|
||||
visibility = ["//visibility:public"],
|
||||
@@ -413,18 +250,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "game_swift_proto",
|
||||
protos = [":game_proto"],
|
||||
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
|
||||
deps = [
|
||||
":action_result_swift_proto",
|
||||
":game_state_swift_proto",
|
||||
":shardok_results_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:date_swift_proto",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "game_scala_proto",
|
||||
deps = [":game_proto"],
|
||||
@@ -442,27 +267,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "game_state_swift_proto",
|
||||
protos = [":game_state_proto"],
|
||||
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
|
||||
deps = [
|
||||
":battalion_swift_proto",
|
||||
":faction_swift_proto",
|
||||
":hero_swift_proto",
|
||||
":llm_request_swift_proto",
|
||||
":llm_response_swift_proto",
|
||||
":province_swift_proto",
|
||||
":run_status_swift_proto",
|
||||
":shardok_battle_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:action_result_notification_details_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:battalion_type_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:chronicle_entry_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:date_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:round_phase_swift_proto",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "game_state_scala_proto",
|
||||
deps = [":game_state_proto"],
|
||||
@@ -489,18 +293,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "hero_swift_proto",
|
||||
protos = [":hero_proto"],
|
||||
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
|
||||
deps = [
|
||||
":event_for_hero_backstory_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:gender_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:hero_backstory_version_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:profession_swift_proto",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "hero_scala_proto",
|
||||
deps = [":hero_proto"],
|
||||
@@ -518,13 +310,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "llm_response_swift_proto",
|
||||
protos = [":llm_response_proto"],
|
||||
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
|
||||
deps = [":llm_request_swift_proto"],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "llm_response_scala_proto",
|
||||
deps = [":llm_response_proto"],
|
||||
@@ -540,21 +325,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "llm_request_swift_proto",
|
||||
protos = [":llm_request_proto"],
|
||||
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
|
||||
deps = [
|
||||
":event_for_chronicle_swift_proto",
|
||||
":event_for_hero_backstory_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:date_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:diplomacy_offer_status_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:unaffiliated_hero_quest_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:hero_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/views:battalion_view_swift_proto",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "llm_request_scala_proto",
|
||||
deps = [":llm_request_proto"],
|
||||
@@ -578,27 +348,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "province_swift_proto",
|
||||
protos = [":province_proto"],
|
||||
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
|
||||
deps = [
|
||||
":army_swift_proto",
|
||||
":battle_revelation_swift_proto",
|
||||
":deferred_change_swift_proto",
|
||||
":supplies_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:battalion_type_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:date_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:diplomacy_offer_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:improvement_type_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:province_event_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:province_order_type_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:tribute_amount_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:unaffiliated_hero_swift_proto",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "province_scala_proto",
|
||||
visibility = ["//visibility:public"],
|
||||
@@ -616,8 +365,8 @@ proto_library(
|
||||
":deferred_change_proto",
|
||||
":supplies_proto",
|
||||
":unaffiliated_hero_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:battalion_type_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:command_type_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:date_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:improvement_type_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:province_event_proto",
|
||||
@@ -626,13 +375,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "run_status_swift_proto",
|
||||
protos = [":run_status_proto"],
|
||||
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
|
||||
deps = [],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "run_status_scala_proto",
|
||||
deps = [":run_status_proto"],
|
||||
@@ -645,13 +387,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "running_games_swift_proto",
|
||||
protos = [":running_games_proto"],
|
||||
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
|
||||
deps = [],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "running_games_scala_proto",
|
||||
deps = [":running_games_proto"],
|
||||
@@ -662,16 +397,6 @@ proto_library(
|
||||
srcs = ["running_games.proto"],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "shardok_battle_swift_proto",
|
||||
protos = [":shardok_battle_proto"],
|
||||
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
|
||||
deps = [
|
||||
":army_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/common:victory_condition_swift_proto",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "shardok_battle_scala_proto",
|
||||
deps = [":shardok_battle_proto"],
|
||||
@@ -688,17 +413,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "shardok_results_swift_proto",
|
||||
protos = [":shardok_results_proto"],
|
||||
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/shardok/api:action_result_view_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/shardok/api:command_descriptor_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/shardok/storage:action_result_swift_proto",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "shardok_results_scala_proto",
|
||||
deps = [":shardok_results_proto"],
|
||||
@@ -716,14 +430,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "supplies_swift_proto",
|
||||
protos = [":supplies_proto"],
|
||||
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
|
||||
deps = [
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "supplies_scala_proto",
|
||||
deps = [":supplies_proto"],
|
||||
@@ -737,20 +443,6 @@ proto_library(
|
||||
deps = ["@com_google_protobuf//:wrappers_proto"],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "unaffiliated_hero_swift_proto",
|
||||
protos = [":unaffiliated_hero_proto"],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0:__subpackages__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:date_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:recruitment_info_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:unaffiliated_hero_type_swift_proto",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "unaffiliated_hero_scala_proto",
|
||||
deps = [":unaffiliated_hero_proto"],
|
||||
|
||||
@@ -12,6 +12,7 @@ import "src/main/protobuf/net/eagle0/eagle/common/action_result_notification_det
|
||||
import "src/main/protobuf/net/eagle0/eagle/common/action_result_type.proto";
|
||||
import "src/main/protobuf/net/eagle0/eagle/common/battalion_type.proto";
|
||||
import "src/main/protobuf/net/eagle0/eagle/common/chronicle_entry.proto";
|
||||
import "src/main/protobuf/net/eagle0/eagle/common/command_type.proto";
|
||||
import "src/main/protobuf/net/eagle0/eagle/common/date.proto";
|
||||
import "src/main/protobuf/net/eagle0/eagle/common/round_phase.proto";
|
||||
import "src/main/protobuf/net/eagle0/eagle/internal/battalion.proto";
|
||||
@@ -63,7 +64,9 @@ message ActionResult {
|
||||
|
||||
.google.protobuf.Int32Value province_acted = 17;
|
||||
|
||||
.net.eagle0.eagle.api.SelectedCommand last_command_type_for_acting_province = 28;
|
||||
// Deprecated: Use last_command_type_for_acting_province instead. Kept for backwards compatibility with saved games.
|
||||
.net.eagle0.eagle.api.SelectedCommand deprecated_last_command_type_for_acting_province = 28;
|
||||
.net.eagle0.eagle.common.command_type.CommandType last_command_type_for_acting_province = 43;
|
||||
|
||||
.google.protobuf.Int32Value leader = 18;
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ syntax = "proto3";
|
||||
package net.eagle0.eagle.internal;
|
||||
|
||||
import "google/protobuf/wrappers.proto";
|
||||
import "src/main/protobuf/net/eagle0/eagle/api/selected_command.proto";
|
||||
import "src/main/protobuf/net/eagle0/eagle/common/battalion_type.proto";
|
||||
import "src/main/protobuf/net/eagle0/eagle/common/command_type.proto";
|
||||
import "src/main/protobuf/net/eagle0/eagle/common/date.proto";
|
||||
import "src/main/protobuf/net/eagle0/eagle/common/improvement_type.proto";
|
||||
import "src/main/protobuf/net/eagle0/eagle/common/province_event.proto";
|
||||
@@ -104,7 +104,7 @@ message Province {
|
||||
|
||||
.net.eagle0.eagle.common.Date last_beasts_date = 25;
|
||||
.net.eagle0.eagle.common.Date last_riot_date = 35;
|
||||
.net.eagle0.eagle.api.SelectedCommand last_command = 26;
|
||||
.net.eagle0.eagle.common.command_type.CommandType last_command = 26;
|
||||
|
||||
int32 castle_count = 33;
|
||||
int32 hero_cap = 39;
|
||||
|
||||
@@ -1,18 +1,7 @@
|
||||
load("@build_bazel_rules_swift//proto:proto.bzl", "swift_proto_library")
|
||||
load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
|
||||
load("@rules_proto//proto:defs.bzl", "proto_library")
|
||||
load("@rules_scala//scala_proto:scala_proto.bzl", "scala_proto_library")
|
||||
|
||||
swift_proto_library(
|
||||
name = "action_result_view_swift_proto",
|
||||
protos = [":action_result_view_proto"],
|
||||
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:action_result_notification_details_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:action_result_type_swift_proto",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "action_result_view_scala_proto",
|
||||
visibility = [
|
||||
@@ -36,15 +25,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "army_view_swift_proto",
|
||||
protos = [":army_view_proto"],
|
||||
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:combat_unit_swift_proto",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "army_view_scala_proto",
|
||||
visibility = [
|
||||
@@ -65,19 +45,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "battalion_view_swift_proto",
|
||||
protos = [":battalion_view_proto"],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:__subpackages__",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:__pkg__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:battalion_type_swift_proto",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "battalion_view_scala_proto",
|
||||
visibility = [
|
||||
@@ -102,15 +69,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "faction_relationship_view_swift_proto",
|
||||
protos = [":faction_relationship_view_proto"],
|
||||
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:date_swift_proto",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "faction_relationship_view_scala_proto",
|
||||
visibility = [
|
||||
@@ -131,16 +89,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "faction_view_swift_proto",
|
||||
protos = [":faction_view_proto"],
|
||||
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
|
||||
deps = [
|
||||
":faction_relationship_view_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:diplomacy_offer_swift_proto",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "faction_view_scala_proto",
|
||||
visibility = [
|
||||
@@ -163,23 +111,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "game_state_view_swift_proto",
|
||||
protos = [":game_state_view_proto"],
|
||||
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
|
||||
deps = [
|
||||
":battalion_view_swift_proto",
|
||||
":faction_view_swift_proto",
|
||||
":hero_view_swift_proto",
|
||||
":province_view_swift_proto",
|
||||
":shardok_battle_view_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:battalion_type_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:chronicle_entry_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:date_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:round_phase_swift_proto",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "game_state_view_scala_proto",
|
||||
visibility = [
|
||||
@@ -208,20 +139,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "hero_view_swift_proto",
|
||||
protos = [":hero_view_proto"],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:__subpackages__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
":stat_with_condition_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:gender_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:profession_swift_proto",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "hero_view_scala_proto",
|
||||
visibility = [
|
||||
@@ -247,13 +164,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "incoming_army_view_swift_proto",
|
||||
protos = [":incoming_army_view_proto"],
|
||||
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
|
||||
deps = [":battalion_view_swift_proto"],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "incoming_army_view_scala_proto",
|
||||
visibility = [
|
||||
@@ -270,27 +180,6 @@ proto_library(
|
||||
deps = [":battalion_view_proto"],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "province_view_swift_proto",
|
||||
protos = [":province_view_proto"],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0/eagle:__subpackages__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
":army_view_swift_proto",
|
||||
":battalion_view_swift_proto",
|
||||
":incoming_army_view_swift_proto",
|
||||
":stat_with_condition_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:date_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:profession_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:province_event_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:province_order_type_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:recruitment_info_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:unaffiliated_hero_swift_proto",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "province_view_scala_proto",
|
||||
visibility = [
|
||||
@@ -323,13 +212,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "stat_with_condition_swift_proto",
|
||||
protos = [":stat_with_condition_proto"],
|
||||
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
|
||||
deps = [],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "stat_with_condition_scala_proto",
|
||||
visibility = [
|
||||
@@ -349,15 +231,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "shardok_battle_view_swift_proto",
|
||||
protos = [":shardok_battle_view_proto"],
|
||||
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/common:hostility_swift_proto",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "shardok_battle_view_scala_proto",
|
||||
visibility = [
|
||||
|
||||
@@ -1,23 +1,8 @@
|
||||
load("@build_bazel_rules_swift//proto:proto.bzl", "swift_proto_library")
|
||||
load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
|
||||
load("@rules_cc//cc:defs.bzl", "cc_proto_library")
|
||||
load("@rules_proto//proto:defs.bzl", "proto_library")
|
||||
load("@rules_scala//scala_proto:scala_proto.bzl", "scala_proto_library")
|
||||
|
||||
swift_proto_library(
|
||||
name = "action_result_view_swift_proto",
|
||||
protos = [":action_result_view_proto"],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
":game_state_view_swift_proto",
|
||||
":odds_view_swift_proto",
|
||||
":unit_view_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/shardok/common:action_type_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/shardok/common:coords_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/shardok/common:game_status_swift_proto",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "action_result_view_scala_proto",
|
||||
visibility = ["//visibility:public"],
|
||||
@@ -42,18 +27,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "command_descriptor_swift_proto",
|
||||
protos = [":command_descriptor_proto"],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
":odds_view_swift_proto",
|
||||
":roll_request_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/shardok/common:command_type_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/shardok/common:coords_swift_proto",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "command_descriptor_scala_proto",
|
||||
visibility = ["//visibility:public"],
|
||||
@@ -82,20 +55,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "game_state_view_swift_proto",
|
||||
protos = [":game_state_view_proto"],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
":unit_view_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/shardok/common:game_status_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/shardok/common:hex_map_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/shardok/common:player_info_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/shardok/common:tile_modifier_with_coords_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/shardok/common:weather_swift_proto",
|
||||
],
|
||||
)
|
||||
|
||||
cc_proto_library(
|
||||
name = "game_state_view_cc_proto",
|
||||
visibility = ["//visibility:public"],
|
||||
@@ -119,13 +78,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "odds_view_swift_proto",
|
||||
protos = [":odds_view_proto"],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [":unit_view_swift_proto"],
|
||||
)
|
||||
|
||||
cc_proto_library(
|
||||
name = "odds_view_cc_proto",
|
||||
visibility = ["//visibility:public"],
|
||||
@@ -153,16 +105,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "roll_request_swift_proto",
|
||||
protos = [":roll_request_proto"],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
":odds_view_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/shardok/common:command_type_swift_proto",
|
||||
],
|
||||
)
|
||||
|
||||
cc_proto_library(
|
||||
name = "roll_request_cc_proto",
|
||||
visibility = ["//visibility:public"],
|
||||
@@ -179,17 +121,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "unit_view_swift_proto",
|
||||
protos = [":unit_view_proto"],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/shardok/common:command_type_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/shardok/common:coords_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/shardok/common:hostility_swift_proto",
|
||||
],
|
||||
)
|
||||
|
||||
cc_proto_library(
|
||||
name = "unit_view_cc_proto",
|
||||
visibility = ["//visibility:public"],
|
||||
|
||||
@@ -1,19 +1,8 @@
|
||||
load("@build_bazel_rules_swift//proto:proto.bzl", "swift_proto_library")
|
||||
load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
|
||||
load("@rules_cc//cc:defs.bzl", "cc_proto_library")
|
||||
load("@rules_proto//proto:defs.bzl", "proto_library")
|
||||
load("@rules_scala//scala_proto:scala_proto.bzl", "scala_proto_library")
|
||||
|
||||
swift_proto_library(
|
||||
name = "action_type_swift_proto",
|
||||
protos = [":action_type_proto"],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0:__subpackages__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
deps = [],
|
||||
)
|
||||
|
||||
cc_proto_library(
|
||||
name = "action_type_cc_proto",
|
||||
visibility = ["//visibility:public"],
|
||||
@@ -26,16 +15,6 @@ proto_library(
|
||||
visibility = ["//src/main/protobuf/net/eagle0/shardok:__subpackages__"],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "command_type_swift_proto",
|
||||
protos = [":command_type_proto"],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0:__subpackages__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
deps = [],
|
||||
)
|
||||
|
||||
cc_proto_library(
|
||||
name = "command_type_cc_proto",
|
||||
visibility = ["//visibility:public"],
|
||||
@@ -48,16 +27,6 @@ proto_library(
|
||||
visibility = ["//src/main/protobuf/net/eagle0/shardok:__subpackages__"],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "coords_swift_proto",
|
||||
protos = [":coords_proto"],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0:__subpackages__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
deps = [],
|
||||
)
|
||||
|
||||
cc_proto_library(
|
||||
name = "coords_cc_proto",
|
||||
visibility = ["//visibility:public"],
|
||||
@@ -70,16 +39,6 @@ proto_library(
|
||||
visibility = ["//src/main/protobuf/net/eagle0/shardok:__subpackages__"],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "game_status_swift_proto",
|
||||
protos = [":game_status_proto"],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0:__subpackages__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
deps = [":victory_condition_swift_proto"],
|
||||
)
|
||||
|
||||
cc_proto_library(
|
||||
name = "game_status_cc_proto",
|
||||
visibility = ["//visibility:public"],
|
||||
@@ -93,21 +52,6 @@ proto_library(
|
||||
deps = [":victory_condition_proto"],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "hex_map_swift_proto",
|
||||
protos = [":hex_map_proto"],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0:__subpackages__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
":coords_swift_proto",
|
||||
":hex_map_direction_swift_proto",
|
||||
":terrain_swift_proto",
|
||||
":tile_modifier_swift_proto",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "hex_map_scala_proto",
|
||||
visibility = ["//visibility:public"],
|
||||
@@ -134,16 +78,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "hex_map_direction_swift_proto",
|
||||
protos = [":hex_map_direction_proto"],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0:__subpackages__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
deps = [],
|
||||
)
|
||||
|
||||
cc_proto_library(
|
||||
name = "hex_map_direction_cc_proto",
|
||||
visibility = ["//visibility:public"],
|
||||
@@ -156,16 +90,6 @@ proto_library(
|
||||
visibility = ["//src/main/protobuf/net/eagle0/shardok:__subpackages__"],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "hostility_swift_proto",
|
||||
protos = [":hostility_proto"],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0:__subpackages__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
deps = [],
|
||||
)
|
||||
|
||||
cc_proto_library(
|
||||
name = "hostility_cc_proto",
|
||||
visibility = ["//visibility:public"],
|
||||
@@ -178,16 +102,6 @@ proto_library(
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "player_info_swift_proto",
|
||||
protos = [":player_info_proto"],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0:__subpackages__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
deps = [":victory_condition_swift_proto"],
|
||||
)
|
||||
|
||||
cc_proto_library(
|
||||
name = "player_info_cc_proto",
|
||||
visibility = ["//visibility:public"],
|
||||
@@ -205,16 +119,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "terrain_swift_proto",
|
||||
protos = [":terrain_proto"],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0:__subpackages__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
deps = [":tile_modifier_swift_proto"],
|
||||
)
|
||||
|
||||
cc_proto_library(
|
||||
name = "terrain_cc_proto",
|
||||
visibility = ["//visibility:public"],
|
||||
@@ -234,16 +138,6 @@ proto_library(
|
||||
deps = [":tile_modifier_proto"],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "tile_modifier_swift_proto",
|
||||
protos = [":tile_modifier_proto"],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0:__subpackages__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
deps = [],
|
||||
)
|
||||
|
||||
cc_proto_library(
|
||||
name = "tile_modifier_cc_proto",
|
||||
visibility = ["//visibility:public"],
|
||||
@@ -265,19 +159,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "tile_modifier_with_coords_swift_proto",
|
||||
protos = [":tile_modifier_with_coords_proto"],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0:__subpackages__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
":coords_swift_proto",
|
||||
":tile_modifier_swift_proto",
|
||||
],
|
||||
)
|
||||
|
||||
cc_proto_library(
|
||||
name = "tile_modifier_with_coords_cc_proto",
|
||||
visibility = ["//visibility:public"],
|
||||
@@ -294,16 +175,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "victory_condition_swift_proto",
|
||||
protos = [":victory_condition_proto"],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0:__subpackages__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
deps = [],
|
||||
)
|
||||
|
||||
cc_proto_library(
|
||||
name = "victory_condition_cc_proto",
|
||||
visibility = ["//visibility:public"],
|
||||
@@ -316,16 +187,6 @@ proto_library(
|
||||
visibility = ["//src/main/protobuf/net/eagle0/shardok:__subpackages__"],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "weather_swift_proto",
|
||||
protos = [":weather_proto"],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0:__subpackages__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
deps = [":hex_map_direction_swift_proto"],
|
||||
)
|
||||
|
||||
cc_proto_library(
|
||||
name = "weather_cc_proto",
|
||||
visibility = ["//visibility:public"],
|
||||
|
||||
@@ -1,26 +1,8 @@
|
||||
load("@build_bazel_rules_swift//proto:proto.bzl", "swift_proto_library")
|
||||
load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
|
||||
load("@rules_cc//cc:defs.bzl", "cc_proto_library")
|
||||
load("@rules_proto//proto:defs.bzl", "proto_library")
|
||||
load("@rules_scala//scala_proto:scala_proto.bzl", "scala_proto_library")
|
||||
|
||||
swift_proto_library(
|
||||
name = "action_result_swift_proto",
|
||||
protos = [":action_result_proto"],
|
||||
visibility = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:__pkg__",
|
||||
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
":odds_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/shardok/common:action_type_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/shardok/common:coords_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/shardok/common:game_status_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/shardok/common:tile_modifier_with_coords_swift_proto",
|
||||
"//src/main/protobuf/net/eagle0/shardok/common:weather_swift_proto",
|
||||
],
|
||||
)
|
||||
|
||||
scala_proto_library(
|
||||
name = "action_result_scala_proto",
|
||||
visibility = ["//visibility:public"],
|
||||
@@ -67,12 +49,6 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_proto_library(
|
||||
name = "odds_swift_proto",
|
||||
protos = [":odds_proto"],
|
||||
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
|
||||
)
|
||||
|
||||
cc_proto_library(
|
||||
name = "odds_cc_proto",
|
||||
visibility = ["//visibility:public"],
|
||||
|
||||
@@ -154,7 +154,6 @@ scala_library(
|
||||
":ai_client_utils",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:battalion_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:command_selection",
|
||||
|
||||
@@ -11,6 +11,7 @@ scala_library(
|
||||
"//src/test/scala/net/eagle0/eagle/service/controller:__pkg__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/command/selected",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/shardok_interface:battle_update",
|
||||
],
|
||||
@@ -21,6 +22,7 @@ scala_library(
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/views:action_result_view_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/command/selected",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/shardok_interface:battle_resolution",
|
||||
"//src/main/scala/net/eagle0/eagle/shardok_interface:battle_update",
|
||||
@@ -75,6 +77,8 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/command/available:one_province_available_commands_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/command:command_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/date",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/faction",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero",
|
||||
@@ -170,6 +174,7 @@ scala_library(
|
||||
name = "round_phase_advancer",
|
||||
srcs = ["RoundPhaseAdvancer.scala"],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:round_phase_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/scala/net/eagle0/common:functional_random",
|
||||
|
||||
@@ -2,8 +2,8 @@ package net.eagle0.eagle.library
|
||||
|
||||
import net.eagle0.eagle.{FactionId, GameId, ProvinceId}
|
||||
import net.eagle0.eagle.api.command.AvailableCommands
|
||||
import net.eagle0.eagle.api.selected_command.SelectedCommand
|
||||
import net.eagle0.eagle.internal.action_result.ActionResult
|
||||
import net.eagle0.eagle.model.state.command.selected.SelectedCommand
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.shardok_interface.{BattleResolution, BattleUpdate}
|
||||
import net.eagle0.eagle.views.action_result_view.ActionResultView
|
||||
|
||||
@@ -5,7 +5,6 @@ import scala.annotation.tailrec
|
||||
import net.eagle0.common.SeededRandom
|
||||
import net.eagle0.eagle.{FactionId, GameId, ProvinceId}
|
||||
import net.eagle0.eagle.api.command.AvailableCommands
|
||||
import net.eagle0.eagle.api.selected_command.SelectedCommand
|
||||
import net.eagle0.eagle.internal.action_result.ActionResult
|
||||
import net.eagle0.eagle.library.actions.applier.{ActionResultApplierImpl, ActionResultWithResultingState}
|
||||
import net.eagle0.eagle.library.actions.availability.AvailableCommandsFactory
|
||||
@@ -24,6 +23,8 @@ import net.eagle0.eagle.library.EngineImpl.{appliedResults, appliedResultsScala,
|
||||
import net.eagle0.eagle.model.action_result.ActionResultT
|
||||
import net.eagle0.eagle.model.proto_converters.command.available.OneProvinceAvailableCommandsConverter
|
||||
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
|
||||
import net.eagle0.eagle.model.state.command.available.AvailableCommand as ScalaAvailableCommand
|
||||
import net.eagle0.eagle.model.state.command.selected.SelectedCommand
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.shardok_interface.{BattleResolution, BattleUpdate}
|
||||
import net.eagle0.eagle.views.action_result_view.ActionResultView
|
||||
@@ -286,34 +287,35 @@ case class EngineImpl(
|
||||
selectedProvinceId: ProvinceId,
|
||||
selectedCommand: SelectedCommand
|
||||
): EngineAndResults = {
|
||||
val maybeAvailableCommands = getAvailablePlayerCommands(factionId)
|
||||
// Get Scala commands directly from factory
|
||||
val scalaCommandsByProvince =
|
||||
availableCommandsFactory.availablePlayerCommands(currentState, factionId)
|
||||
|
||||
commandRequire(
|
||||
maybeAvailableCommands.nonEmpty,
|
||||
scalaCommandsByProvince.nonEmpty,
|
||||
s"Attempted to post a command for faction $factionId, but there are none"
|
||||
)
|
||||
|
||||
val availableCommands = maybeAvailableCommands.get
|
||||
|
||||
commandRequire(
|
||||
availableCommands.commandsByProvince.contains(selectedProvinceId),
|
||||
s"Attempted to select province $selectedProvinceId out of ${availableCommands.commandsByProvince.keys}"
|
||||
scalaCommandsByProvince.contains(selectedProvinceId),
|
||||
s"Attempted to select province $selectedProvinceId out of ${scalaCommandsByProvince.keys}"
|
||||
)
|
||||
|
||||
val availableCommandsForProvince =
|
||||
availableCommands.commandsByProvince(selectedProvinceId)
|
||||
val scalaCommandsForProvince = scalaCommandsByProvince(selectedProvinceId)
|
||||
|
||||
val availableCommandOpt = AvailableCommandTypeMap.matchingAvailableCommand(
|
||||
availableCommandsForProvince.commands,
|
||||
// Match using CommandType
|
||||
val scalaAvailableCommandOpt = AvailableCommandTypeMap.matchingAvailableCommand(
|
||||
scalaCommandsForProvince.commands,
|
||||
selectedCommand
|
||||
)
|
||||
commandRequire(
|
||||
availableCommandOpt.isDefined,
|
||||
scalaAvailableCommandOpt.isDefined,
|
||||
s"No matching available command for selected command $selectedCommand"
|
||||
)
|
||||
val availableCommand = availableCommandOpt.get
|
||||
val scalaAvailableCommand = scalaAvailableCommandOpt.get
|
||||
|
||||
val sequencer = RandomStateSequencer(
|
||||
val commandType = selectedCommand.commandType
|
||||
val sequencer = RandomStateSequencer(
|
||||
initialState = this.currentState,
|
||||
actionResultApplier = ActionResultApplierImpl(Some(ScalaRuntimeValidator)),
|
||||
functionalRandom = SeededRandom(this.currentState.randomSeed)
|
||||
@@ -322,10 +324,10 @@ case class EngineImpl(
|
||||
commandFactory.makeTCommand(
|
||||
actingFactionId = factionId,
|
||||
gameState = gs,
|
||||
availableCommand = availableCommand,
|
||||
availableCommand = scalaAvailableCommand,
|
||||
selectedCommand = selectedCommand
|
||||
),
|
||||
selectedCommand = selectedCommand
|
||||
commandType = commandType
|
||||
).withProtolessSequentialResultsAction(gs => HeroBackstoryUpdateActionGenerator(gs))
|
||||
|
||||
// Validate that the first result has an acting faction set (required for player commands)
|
||||
|
||||
@@ -54,12 +54,12 @@ scala_library(
|
||||
deps = [
|
||||
":province_update_helpers",
|
||||
":province_update_helpers2",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library:eagle_internal_exception",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:eagle_require",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province/concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/command:command_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province/concrete",
|
||||
@@ -155,13 +155,13 @@ scala_library(
|
||||
":game_state_hero_extensions",
|
||||
":game_state_misc_extensions",
|
||||
":game_state_province_extensions",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_faction_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_hero_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:notification_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/command:command_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
deps = [
|
||||
@@ -171,7 +171,6 @@ scala_library(
|
||||
":game_state_hero_extensions",
|
||||
":game_state_misc_extensions",
|
||||
":game_state_province_extensions",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:extra_xp_for_stat_bump_over100",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:xp_for_stat_bump",
|
||||
@@ -180,6 +179,7 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_hero_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:notification_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/command:command_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
+3
-3
@@ -1,11 +1,11 @@
|
||||
package net.eagle0.eagle.library.actions.applier
|
||||
|
||||
import net.eagle0.eagle.*
|
||||
import net.eagle0.eagle.api.selected_command.SelectedCommand
|
||||
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
|
||||
import net.eagle0.eagle.library.EagleInternalException
|
||||
import net.eagle0.eagle.model.action_result.changed_province.concrete.ChangedProvinceC
|
||||
import net.eagle0.eagle.model.action_result.changed_province.ChangedProvinceT
|
||||
import net.eagle0.eagle.model.state.command.CommandType
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.province.concrete.ProvinceC
|
||||
import net.eagle0.eagle.model.state.province.ProvinceT
|
||||
@@ -79,10 +79,10 @@ object GameStateProvinceExtensions {
|
||||
|
||||
def applyLastCommand(
|
||||
provinceId: Option[ProvinceId],
|
||||
lastCommand: Option[SelectedCommand]
|
||||
lastCommand: Option[CommandType]
|
||||
): GameState =
|
||||
provinceId
|
||||
.filter(_ => lastCommand.exists(!_.isEmpty))
|
||||
.filter(_ => lastCommand.isDefined)
|
||||
.map { pid =>
|
||||
val province = gameState.provinces(pid) match {
|
||||
case p: ProvinceC => p
|
||||
|
||||
+13
-6
@@ -1,5 +1,6 @@
|
||||
package net.eagle0.eagle.library.actions.availability
|
||||
|
||||
import net.eagle0.common.hostility.Hostility
|
||||
import net.eagle0.common.MoreOption
|
||||
import net.eagle0.eagle.{FactionId, ProvinceId}
|
||||
import net.eagle0.eagle.library.util.{IncomingArmyUtils, ProvinceDistances}
|
||||
@@ -62,11 +63,13 @@ object AvailableAttackDecisionCommandFactory extends ScalaAvailableCommandsFacto
|
||||
.takeRight(heroCount)
|
||||
.sum
|
||||
|
||||
province.rulingFactionId.map { _ =>
|
||||
province.rulingFactionId.map { defenderFid =>
|
||||
ArmyStats(
|
||||
factionId = province.rulingFactionId.get,
|
||||
factionId = defenderFid,
|
||||
heroCount = heroCount,
|
||||
troopCount = troopCount
|
||||
troopCount = troopCount,
|
||||
hostility = FactionUtils.hostilityStatus(attackerFid, defenderFid, gs.factions.values.toVector),
|
||||
originProvinceId = pid
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -76,22 +79,26 @@ object AvailableAttackDecisionCommandFactory extends ScalaAvailableCommandsFacto
|
||||
pid: ProvinceId,
|
||||
gs: GameState
|
||||
): Vector[ArmyStats] =
|
||||
relevantArmies(pid, gs).map(ma => stats(ma.army, gs)) ++ defendingArmyStats(
|
||||
relevantArmies(pid, gs).map(ma => stats(toFid, ma, gs)) ++ defendingArmyStats(
|
||||
toFid,
|
||||
pid,
|
||||
gs
|
||||
)
|
||||
|
||||
private def stats(
|
||||
army: Army,
|
||||
toFid: FactionId,
|
||||
movingArmy: MovingArmy,
|
||||
gs: GameState
|
||||
): ArmyStats = {
|
||||
val army = movingArmy.army
|
||||
val battalions = army.units.flatMap(_.battalionId).map(gs.battalions)
|
||||
val troopCount = battalions.map(_.size).sum
|
||||
ArmyStats(
|
||||
factionId = army.factionId,
|
||||
heroCount = army.units.size,
|
||||
troopCount = troopCount
|
||||
troopCount = troopCount,
|
||||
hostility = FactionUtils.hostilityStatus(toFid, army.factionId, gs.factions.values.toVector),
|
||||
originProvinceId = movingArmy.originProvinceId
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+18
-63
@@ -7,11 +7,10 @@ import net.eagle0.eagle.{FactionId, ProvinceId}
|
||||
import net.eagle0.eagle.library.util.faction_utils.FactionUtils
|
||||
import net.eagle0.eagle.library.util.province.ProvinceUtils
|
||||
import net.eagle0.eagle.library.util.IncomingArmyUtils
|
||||
import net.eagle0.eagle.model.proto_converters.command.selected.SelectedCommandConverter
|
||||
import net.eagle0.eagle.model.state.command.available.AvailableCommand
|
||||
import net.eagle0.eagle.model.state.command.available.AvailableCommand.*
|
||||
import net.eagle0.eagle.model.state.command.available.OneProvinceAvailableCommands
|
||||
import net.eagle0.eagle.model.state.command.selected.SelectedCommand
|
||||
import net.eagle0.eagle.model.state.command.CommandType
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.province.ProvinceT
|
||||
import net.eagle0.eagle.model.state.RoundPhase
|
||||
@@ -19,66 +18,25 @@ import net.eagle0.eagle.model.state.RoundPhase.*
|
||||
|
||||
object AvailableCommandsFactory {
|
||||
|
||||
/** Command types that should not suggest a follow-up of the same type. */
|
||||
private val noFollowCommands: Set[CommandType] = Set(
|
||||
CommandType.March,
|
||||
CommandType.Diplomacy,
|
||||
CommandType.Rest,
|
||||
CommandType.Trade,
|
||||
CommandType.ArmTroops,
|
||||
CommandType.OrganizeTroops,
|
||||
CommandType.ExileVassal,
|
||||
CommandType.SwearBrotherhood
|
||||
)
|
||||
|
||||
/** Returns true if after executing `last`, the UI should suggest `next` as a follow-up. */
|
||||
def shouldFollow(
|
||||
last: SelectedCommand,
|
||||
last: CommandType,
|
||||
next: AvailableCommand
|
||||
): Boolean = (last, next) match {
|
||||
case (_: SelectedCommand.MarchSelected, _) => false
|
||||
case (_: SelectedCommand.DiplomacySelected, _) => false
|
||||
case (SelectedCommand.RestSelected, _) => false
|
||||
case (_: SelectedCommand.TradeSelected, _) => false
|
||||
case (_: SelectedCommand.ArmTroopsSelected, _) => false
|
||||
case (_: SelectedCommand.OrganizeTroopsSelected, _) => false
|
||||
case (_: SelectedCommand.ExileVassalSelected, _) => false
|
||||
case (_: SelectedCommand.SwearBrotherhoodSelected, _) => false
|
||||
case (last, next) => commandTypesMatch(last, next)
|
||||
}
|
||||
|
||||
/** Compares ordinal values to check if the selected and available commands are the same type. */
|
||||
private def commandTypesMatch(selected: SelectedCommand, available: AvailableCommand): Boolean =
|
||||
(selected, available) match {
|
||||
case (_: SelectedCommand.AlmsSelected, _: AlmsAvailable) => true
|
||||
case (_: SelectedCommand.ApprehendOutlawSelected, _: ApprehendOutlawAvailable) => true
|
||||
case (_: SelectedCommand.ArmTroopsSelected, _: ArmTroopsAvailable) => true
|
||||
case (_: SelectedCommand.AttackDecisionSelected, _: AttackDecisionAvailable) => true
|
||||
case (_: SelectedCommand.ControlWeatherSelected, _: ControlWeatherAvailable) => true
|
||||
case (_: SelectedCommand.DeclineQuestSelected, _: DeclineQuestAvailable) => true
|
||||
case (_: SelectedCommand.DefendSelected, _: DefendAvailable) => true
|
||||
case (_: SelectedCommand.DiplomacySelected, _: DiplomacyAvailable) => true
|
||||
case (_: SelectedCommand.DivineSelected, _: DivineAvailable) => true
|
||||
case (_: SelectedCommand.ExileVassalSelected, _: ExileVassalAvailable) => true
|
||||
case (SelectedCommand.FeastSelected, _: FeastAvailable) => true
|
||||
case (_: SelectedCommand.FreeForAllDecisionSelected, _: FreeForAllDecisionAvailable) => true
|
||||
case (_: SelectedCommand.HandleCapturedHeroSelected, _: HandleCapturedHeroAvailable) => true
|
||||
case (_: SelectedCommand.HandleRiotCrackDownSelected, _: HandleRiotCrackDownAvailable) => true
|
||||
case (SelectedCommand.HandleRiotDoNothingSelected, _: HandleRiotDoNothingAvailable) => true
|
||||
case (_: SelectedCommand.HandleRiotGiveSelected, _: HandleRiotGiveAvailable) => true
|
||||
case (_: SelectedCommand.HeroGiftSelected, _: HeroGiftAvailable) => true
|
||||
case (_: SelectedCommand.ImproveSelected, _: ImproveAvailable) => true
|
||||
case (_: SelectedCommand.IssueOrdersSelected, _: IssueOrdersAvailable) => true
|
||||
case (_: SelectedCommand.ManagePrisonersSelected, _: ManagePrisonersAvailable) => true
|
||||
case (_: SelectedCommand.MarchSelected, _: MarchAvailable) => true
|
||||
case (_: SelectedCommand.OrganizeTroopsSelected, _: OrganizeTroopsAvailable) => true
|
||||
case (_: SelectedCommand.PleaseRecruitMeSelected, _: PleaseRecruitMeAvailable) => true
|
||||
case (_: SelectedCommand.ReconSelected, _: ReconAvailable) => true
|
||||
case (_: SelectedCommand.RecruitHeroesSelected, _: RecruitHeroesAvailable) => true
|
||||
case (_: SelectedCommand.ResolveAllianceOfferSelected, _: ResolveAllianceOfferAvailable) => true
|
||||
case (_: SelectedCommand.ResolveBreakAllianceSelected, _: ResolveBreakAllianceAvailable) => true
|
||||
case (_: SelectedCommand.ResolveInvitationSelected, _: ResolveInvitationAvailable) => true
|
||||
case (_: SelectedCommand.ResolveRansomOfferSelected, _: ResolveRansomOfferAvailable) => true
|
||||
case (_: SelectedCommand.ResolveTruceOfferSelected, _: ResolveTruceOfferAvailable) => true
|
||||
case (_: SelectedCommand.ResolveTributeSelected, _: ResolveTributeAvailable) => true
|
||||
case (SelectedCommand.RestSelected, _: RestAvailable) => true
|
||||
case (SelectedCommand.ReturnSelected, _: ReturnAvailable) => true
|
||||
case (_: SelectedCommand.SendSuppliesSelected, _: SendSuppliesAvailable) => true
|
||||
case (_: SelectedCommand.StartEpidemicSelected, _: StartEpidemicAvailable) => true
|
||||
case (_: SelectedCommand.SuppressBeastsSelected, _: SuppressBeastsAvailable) => true
|
||||
case (_: SelectedCommand.SwearBrotherhoodSelected, _: SwearBrotherhoodAvailable) => true
|
||||
case (_: SelectedCommand.TradeSelected, _: TradeAvailable) => true
|
||||
case (_: SelectedCommand.TrainSelected, _: TrainAvailable) => true
|
||||
case _ => false
|
||||
}
|
||||
): Boolean =
|
||||
if noFollowCommands.contains(last) then false
|
||||
else last == next.commandType
|
||||
|
||||
def suggestedProvinceId(
|
||||
fid: FactionId,
|
||||
@@ -210,10 +168,7 @@ class AvailableCommandsFactory(
|
||||
): Int =
|
||||
commands.zipWithIndex.find {
|
||||
case (ac, _) =>
|
||||
province.lastCommand.forall { lastProto =>
|
||||
val last = SelectedCommandConverter.fromProto(lastProto)
|
||||
AvailableCommandsFactory.shouldFollow(last, ac)
|
||||
}
|
||||
province.lastCommand.forall(last => AvailableCommandsFactory.shouldFollow(last, ac))
|
||||
}
|
||||
.map(_._2)
|
||||
.getOrElse(0)
|
||||
|
||||
+8
-3
@@ -1,6 +1,8 @@
|
||||
package net.eagle0.eagle.library.actions.availability
|
||||
|
||||
import net.eagle0.common.hostility.Hostility
|
||||
import net.eagle0.eagle.{FactionId, ProvinceId}
|
||||
import net.eagle0.eagle.library.util.faction_utils.FactionUtils
|
||||
import net.eagle0.eagle.library.util.IncomingArmyUtils
|
||||
import net.eagle0.eagle.model.state.{Army, HostileArmyGroupStatus, MovingArmy}
|
||||
import net.eagle0.eagle.model.state.command.available.AvailableCommand
|
||||
@@ -53,19 +55,22 @@ object AvailableFreeForAllDecisionCommandFactory extends ScalaAvailableCommandsF
|
||||
pid: ProvinceId,
|
||||
gs: GameState
|
||||
): Vector[ArmyStats] =
|
||||
relevantArmies(pid, gs).map(ma => stats(toFid, ma.army, gs))
|
||||
relevantArmies(pid, gs).map(ma => stats(toFid, ma, gs))
|
||||
|
||||
private def stats(
|
||||
toFid: FactionId,
|
||||
army: Army,
|
||||
movingArmy: MovingArmy,
|
||||
gs: GameState
|
||||
): ArmyStats = {
|
||||
val army = movingArmy.army
|
||||
val battalions = army.units.flatMap(_.battalionId).map(gs.battalions)
|
||||
val troopCount = battalions.map(_.size).sum
|
||||
ArmyStats(
|
||||
factionId = army.factionId,
|
||||
heroCount = army.units.size,
|
||||
troopCount = troopCount
|
||||
troopCount = troopCount,
|
||||
hostility = FactionUtils.hostilityStatus(toFid, army.factionId, gs.factions.values.toVector),
|
||||
originProvinceId = movingArmy.originProvinceId
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -71,10 +71,9 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:incoming_army_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/command/selected:selected_command_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/command:command_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/command/selected",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
],
|
||||
@@ -316,6 +315,7 @@ scala_library(
|
||||
":scala_available_commands_factory",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:incoming_army_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:army",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
|
||||
|
||||
@@ -444,6 +444,7 @@ scala_library(
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/scala/net/eagle0/common:functional_random",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_applier",
|
||||
@@ -1306,6 +1307,7 @@ scala_library(
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:province_order_type_scala_proto",
|
||||
"//src/main/scala/net/eagle0/common:functional_random",
|
||||
"//src/main/scala/net/eagle0/common:more_option",
|
||||
@@ -1344,6 +1346,7 @@ scala_library(
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/scala/net/eagle0/common:functional_random",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_applier",
|
||||
|
||||
+3
-2
@@ -52,11 +52,12 @@ case class EndHandleRiotsPhaseAction(
|
||||
)
|
||||
.continue {
|
||||
case (Some(cs), nextFr) =>
|
||||
val scs = cs.toScala(opac.commands)
|
||||
val cmd = commandFactory.makeTCommand(
|
||||
actingFactionId = province.rulingFactionId.get,
|
||||
gameState = gs,
|
||||
availableCommand = cs.available,
|
||||
selectedCommand = cs.selected
|
||||
availableCommand = scs.available,
|
||||
selectedCommand = scs.selected
|
||||
)
|
||||
cmd match {
|
||||
case TCommand.Simple(action) =>
|
||||
|
||||
+3
-2
@@ -50,11 +50,12 @@ case class PerformVassalCommandsPhaseAction(
|
||||
sequencer.withRandomActionResults { (gs, fr) =>
|
||||
chooseCommand(gs, province.id, fr).continue {
|
||||
case (Some(cs), nextFr) =>
|
||||
val scs = cs.toScala(opac.commands)
|
||||
val cmd = commandFactory.makeTCommand(
|
||||
actingFactionId = cs.actingFactionId,
|
||||
gameState = gs,
|
||||
availableCommand = cs.available,
|
||||
selectedCommand = cs.selected
|
||||
availableCommand = scs.available,
|
||||
selectedCommand = scs.selected
|
||||
)
|
||||
cmd match {
|
||||
case TCommand.Simple(action) =>
|
||||
|
||||
+3
-2
@@ -50,11 +50,12 @@ case class PerformVassalDefenseDecisionsAction(
|
||||
}
|
||||
chooseCommand(gs, province.id, protoCommands, fr).continue {
|
||||
case (Some(cs), nextFr) =>
|
||||
val scs = cs.toScala(opac.commands)
|
||||
val cmd = commandFactory.makeTCommand(
|
||||
actingFactionId = cs.actingFactionId,
|
||||
gameState = gs,
|
||||
availableCommand = cs.available,
|
||||
selectedCommand = cs.selected
|
||||
availableCommand = scs.available,
|
||||
selectedCommand = scs.selected
|
||||
)
|
||||
cmd match {
|
||||
case TCommand.Simple(action) =>
|
||||
|
||||
+6
-7
@@ -1,17 +1,16 @@
|
||||
package net.eagle0.eagle.library.actions
|
||||
package impl.command
|
||||
|
||||
import net.eagle0.eagle.api.available_command.*
|
||||
import net.eagle0.eagle.api.selected_command.*
|
||||
import net.eagle0.eagle.model.state.command.available.AvailableCommand
|
||||
import net.eagle0.eagle.model.state.command.selected.SelectedCommand
|
||||
|
||||
object AvailableCommandTypeMap {
|
||||
def matchingAvailableCommand(
|
||||
acs: Iterable[AvailableCommand],
|
||||
sc: SelectedCommand
|
||||
availableCommands: Iterable[AvailableCommand],
|
||||
selectedCommand: SelectedCommand
|
||||
): Option[AvailableCommand] =
|
||||
acs.find(ac => matches(ac, sc))
|
||||
availableCommands.find(_.commandType == selectedCommand.commandType)
|
||||
|
||||
// This is pretty hacky but avoids a long match that has to be kept up to date.
|
||||
def matches(ac: AvailableCommand, sc: SelectedCommand): Boolean =
|
||||
ac.asMessage.sealedValue.number == sc.asMessage.sealedValue.number
|
||||
ac.commandType == sc.commandType
|
||||
}
|
||||
|
||||
@@ -9,12 +9,14 @@ scala_library(
|
||||
],
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:t_command",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/command/selected",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:t_command",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/command/selected",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
@@ -71,10 +73,6 @@ scala_library(
|
||||
":trade_command",
|
||||
":train_command",
|
||||
":travel_command",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api/command/util:control_weather_type_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api/command/util:province_orders_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library:eagle_client_exception",
|
||||
"//src/main/scala/net/eagle0/eagle/library:eagle_internal_exception",
|
||||
@@ -90,25 +88,14 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:action_result_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_battalion_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_hero_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:battalion_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:battalion_type_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:battalion_type_id_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:combat_unit_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:improvement_type_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:proto_conversion_exception",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:tribute_amount_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:unaffiliated_hero_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/date",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/diplomacy_offer",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/diplomacy_offer/status",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/diplomacy_option",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/faction",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/hero",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/province:order_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:combat_unit",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:improvement_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:tribute_amount",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/command/common",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/command/selected",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/date",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/diplomacy_offer",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/diplomacy_option",
|
||||
@@ -223,8 +210,8 @@ scala_library(
|
||||
"//src/test/scala/net/eagle0/eagle:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/command/selected",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
+307
-429
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,8 @@
|
||||
package net.eagle0.eagle.library.actions.impl.command
|
||||
|
||||
import net.eagle0.eagle.api.available_command.AvailableCommand
|
||||
import net.eagle0.eagle.api.selected_command.SelectedCommand
|
||||
import net.eagle0.eagle.library.actions.impl.common.TCommand
|
||||
import net.eagle0.eagle.model.state.command.available.AvailableCommand
|
||||
import net.eagle0.eagle.model.state.command.selected.SelectedCommand
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.FactionId
|
||||
|
||||
|
||||
+4
-4
@@ -1,7 +1,6 @@
|
||||
package net.eagle0.eagle.library.actions.random_state_sequencer
|
||||
|
||||
import net.eagle0.common.{FunctionalRandom, RandomState}
|
||||
import net.eagle0.eagle.api.selected_command.SelectedCommand
|
||||
import net.eagle0.eagle.library.actions.applier.{ActionResultApplier, ActionResultWithResultingState}
|
||||
import net.eagle0.eagle.library.actions.impl.common.{
|
||||
ProtolessRandomSimpleAction,
|
||||
@@ -10,6 +9,7 @@ import net.eagle0.eagle.library.actions.impl.common.{
|
||||
TCommand
|
||||
}
|
||||
import net.eagle0.eagle.model.action_result.ActionResultT
|
||||
import net.eagle0.eagle.model.state.command.CommandType
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
|
||||
/**
|
||||
@@ -87,7 +87,7 @@ trait RandomStateSequencer {
|
||||
*/
|
||||
def withTCommandAndLastCommand(
|
||||
commandGen: GameState => TCommand,
|
||||
selectedCommand: SelectedCommand
|
||||
commandType: CommandType
|
||||
): RandomStateSequencer
|
||||
}
|
||||
|
||||
@@ -189,10 +189,10 @@ private case class RandomStateSequencerImpl(
|
||||
|
||||
override def withTCommandAndLastCommand(
|
||||
commandGen: GameState => TCommand,
|
||||
selectedCommand: SelectedCommand
|
||||
commandType: CommandType
|
||||
): RandomStateSequencer = {
|
||||
def wrapWithLastCommand(result: ActionResultT): ActionResultT =
|
||||
result.withLastCommandTypeForActingProvince(selectedCommand)
|
||||
result.withLastCommandTypeForActingProvince(commandType)
|
||||
|
||||
commandGen(lastState) match {
|
||||
case TCommand.Simple(action) =>
|
||||
|
||||
@@ -127,6 +127,9 @@ scala_library(
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/command/selected:selected_command_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/command/selected",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -1,16 +1,51 @@
|
||||
package net.eagle0.eagle.library.util
|
||||
|
||||
import net.eagle0.eagle.{FactionId, ProvinceId}
|
||||
import net.eagle0.eagle.api.available_command.AvailableCommand
|
||||
import net.eagle0.eagle.api.selected_command.SelectedCommand
|
||||
import net.eagle0.eagle.api.available_command.AvailableCommand as ProtoAvailableCommand
|
||||
import net.eagle0.eagle.api.selected_command.SelectedCommand as ProtoSelectedCommand
|
||||
import net.eagle0.eagle.model.proto_converters.command.selected.SelectedCommandConverter
|
||||
import net.eagle0.eagle.model.state.command.available.AvailableCommand
|
||||
import net.eagle0.eagle.model.state.command.selected.SelectedCommand
|
||||
|
||||
// Proto-based command selection (legacy, for gradual migration)
|
||||
case class CommandSelection(
|
||||
actingFactionId: FactionId,
|
||||
actingProvinceId: ProvinceId,
|
||||
available: ProtoAvailableCommand,
|
||||
selected: ProtoSelectedCommand,
|
||||
reason: String
|
||||
) {
|
||||
def withReason(reason: String): CommandSelection =
|
||||
this.copy(reason = reason)
|
||||
|
||||
/** Convert to ScalaCommandSelection by finding the matching Scala AvailableCommand */
|
||||
def toScala(scalaAvailableCommands: Iterable[AvailableCommand]): ScalaCommandSelection = {
|
||||
val scalaSelectedCommand = SelectedCommandConverter.fromProto(selected)
|
||||
val scalaAvailableCommand = scalaAvailableCommands
|
||||
.find(_.commandType == scalaSelectedCommand.commandType)
|
||||
.getOrElse(
|
||||
throw new IllegalStateException(
|
||||
s"No matching Scala available command for selected command: ${scalaSelectedCommand.commandType}"
|
||||
)
|
||||
)
|
||||
ScalaCommandSelection(
|
||||
actingFactionId = actingFactionId,
|
||||
actingProvinceId = actingProvinceId,
|
||||
available = scalaAvailableCommand,
|
||||
selected = scalaSelectedCommand,
|
||||
reason = reason
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Scala-based command selection
|
||||
case class ScalaCommandSelection(
|
||||
actingFactionId: FactionId,
|
||||
actingProvinceId: ProvinceId,
|
||||
available: AvailableCommand,
|
||||
selected: SelectedCommand,
|
||||
reason: String
|
||||
) {
|
||||
def withReason(reason: String): CommandSelection =
|
||||
def withReason(reason: String): ScalaCommandSelection =
|
||||
this.copy(reason = reason)
|
||||
}
|
||||
|
||||
@@ -188,6 +188,7 @@ scala_library(
|
||||
":ransom_offer_helpers",
|
||||
"//src/main/protobuf/net/eagle0/common:hostility_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api/command/util:armed_battalion_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api/command/util:battalion_with_food_cost_scala_proto",
|
||||
"//src/main/scala/net/eagle0/common:functional_random",
|
||||
@@ -260,7 +261,6 @@ scala_library(
|
||||
":province_gold_surplus_calculator",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/scala/net/eagle0/common:functional_random",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:max_combat_unit_count_per_side",
|
||||
@@ -395,7 +395,6 @@ scala_library(
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:battalion_type_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:command_selection",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/battalion_type_finder",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user