mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-29 02:48:40 +00:00
Compare commits
95
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5cb23464e5 | ||
|
|
3e1919a715 | ||
|
|
699bcc684a | ||
|
|
42fd9c5853 | ||
|
|
01aa3dbace | ||
|
|
a7595e81a2 | ||
|
|
6a61634f37 | ||
|
|
cfa552270a | ||
|
|
09d560208e | ||
|
|
2188834ddf | ||
|
|
eafe5d73ee | ||
|
|
f16257591b | ||
|
|
d259282213 | ||
|
|
de5e6cc65e | ||
|
|
7da2fb7913 | ||
|
|
a485772e66 | ||
|
|
e0212fa7a2 | ||
|
|
6e76ff0fbc | ||
|
|
a37e0af028 | ||
|
|
d6704413d3 | ||
|
|
be59f9775f | ||
|
|
bebe050352 | ||
|
|
77d6fc571b | ||
|
|
65e0ad93cc | ||
|
|
275cb0c563 | ||
|
|
d64f35b18a | ||
|
|
ac0c03af9b | ||
|
|
6b0d58a9e7 | ||
|
|
fb56cf0029 | ||
|
|
70a5a517af | ||
|
|
6de07a70dc | ||
|
|
8e64af7233 | ||
|
|
ca91d3193e | ||
|
|
1de12597d7 | ||
|
|
36c09cae77 | ||
|
|
83d159823a | ||
|
|
5ce88f04d2 | ||
|
|
1bfcb343d9 | ||
|
|
88b628b73f | ||
|
|
9004e6887a | ||
|
|
c62b3d3166 | ||
|
|
6d54312aec | ||
|
|
c886ddc769 | ||
|
|
f6a7545533 | ||
|
|
8d3bc914ed | ||
|
|
844d7d8e50 | ||
|
|
cae2d47af0 | ||
|
|
cc5c6422bd | ||
|
|
c71bf10c0e | ||
|
|
fd94d2ac8b | ||
|
|
46f3c52174 | ||
|
|
8e952c3611 | ||
|
|
a61be38ea6 | ||
|
|
4120196f11 | ||
|
|
191ee501f4 | ||
|
|
9a26173ceb | ||
|
|
7cf2072dad | ||
|
|
c6c7430dce | ||
|
|
c4fc30d0bd | ||
|
|
b6dd273c12 | ||
|
|
56df06d21b | ||
|
|
a7a64fea11 | ||
|
|
b48a44d910 | ||
|
|
4bec466f54 | ||
|
|
84be65cfc3 | ||
|
|
3834471840 | ||
|
|
fdeb2ccf20 | ||
|
|
f10a749ffe | ||
|
|
f636b2b0df | ||
|
|
cb0111a00f | ||
|
|
508bfabece | ||
|
|
de532a3448 | ||
|
|
29c3d5f2c1 | ||
|
|
7b118f24b6 | ||
|
|
2ddb627c58 | ||
|
|
154fac85a5 | ||
|
|
5e61f43183 | ||
|
|
a79209891d | ||
|
|
cbcad13d23 | ||
|
|
9e7eaa6367 | ||
|
|
3fc8589847 | ||
|
|
abec33585d | ||
|
|
e82c174814 | ||
|
|
38732c9255 | ||
|
|
7cd56e5980 | ||
|
|
454d4e2fc7 | ||
|
|
7ad5ebdb56 | ||
|
|
1e3ae0d82c | ||
|
|
92efdbae8b | ||
|
|
378d3f6828 | ||
|
|
ca0b3930dc | ||
|
|
99fef8312b | ||
|
|
cdb8631e91 | ||
|
|
7c0bbd8b9a | ||
|
|
7c9e434430 |
@@ -11,6 +11,8 @@ on:
|
||||
- 'src/main/protobuf/net/eagle0/eagle/internal/auth_internal.proto'
|
||||
- 'ci/BUILD.bazel'
|
||||
- '.github/workflows/auth_build.yml'
|
||||
- 'nginx/nginx.conf'
|
||||
- 'docker-compose.prod.yml'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
push_images:
|
||||
@@ -104,6 +106,14 @@ jobs:
|
||||
DISCORD_CLIENT_SECRET: ${{ secrets.DISCORD_CLIENT_SECRET }}
|
||||
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
|
||||
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
|
||||
GH_OAUTH_CLIENT_ID: ${{ secrets.GH_OAUTH_CLIENT_ID }}
|
||||
GH_OAUTH_CLIENT_SECRET: ${{ secrets.GH_OAUTH_CLIENT_SECRET }}
|
||||
APPLE_SIGNIN_CLIENT_ID: ${{ secrets.APPLE_SIGNIN_CLIENT_ID }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
APPLE_SIGNIN_KEY_ID: ${{ secrets.APPLE_SIGNIN_KEY_ID }}
|
||||
APPLE_SIGNIN_PRIVATE_KEY: ${{ secrets.APPLE_SIGNIN_PRIVATE_KEY }}
|
||||
TWITCH_CLIENT_ID: ${{ secrets.TWITCH_CLIENT_ID }}
|
||||
TWITCH_CLIENT_SECRET: ${{ secrets.TWITCH_CLIENT_SECRET }}
|
||||
JWT_PRIVATE_KEY: ${{ secrets.JWT_PRIVATE_KEY }}
|
||||
FASTMAIL_API_TOKEN: ${{ secrets.FASTMAIL_API_TOKEN }}
|
||||
FASTMAIL_FROM_EMAIL: ${{ secrets.FASTMAIL_FROM_EMAIL }}
|
||||
@@ -112,15 +122,15 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Copy update-env script to server
|
||||
- name: Copy config files to server
|
||||
uses: appleboy/scp-action@v0.1.7
|
||||
with:
|
||||
host: ${{ secrets.DO_DROPLET_IP }}
|
||||
username: deploy
|
||||
key: ${{ secrets.DO_SSH_KEY }}
|
||||
source: "deploy/update-env.sh,deploy/env.template"
|
||||
target: /opt/eagle0/
|
||||
strip_components: 1
|
||||
source: "nginx/nginx.conf,docker-compose.prod.yml"
|
||||
target: "/opt/eagle0"
|
||||
strip_components: 0
|
||||
|
||||
- name: Deploy auth service to production
|
||||
uses: appleboy/ssh-action@v1.0.3
|
||||
@@ -129,51 +139,80 @@ jobs:
|
||||
username: deploy
|
||||
key: ${{ secrets.DO_SSH_KEY }}
|
||||
script_stop: true
|
||||
envs: AUTH_IMAGE,DISCORD_CLIENT_ID,DISCORD_CLIENT_SECRET,GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET,JWT_PRIVATE_KEY,FASTMAIL_API_TOKEN,FASTMAIL_FROM_EMAIL,FASTMAIL_FROM_NAME
|
||||
envs: AUTH_IMAGE,DISCORD_CLIENT_ID,DISCORD_CLIENT_SECRET,GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET,GH_OAUTH_CLIENT_ID,GH_OAUTH_CLIENT_SECRET,APPLE_SIGNIN_CLIENT_ID,APPLE_TEAM_ID,APPLE_SIGNIN_KEY_ID,APPLE_SIGNIN_PRIVATE_KEY,TWITCH_CLIENT_ID,TWITCH_CLIENT_SECRET,JWT_PRIVATE_KEY,FASTMAIL_API_TOKEN,FASTMAIL_FROM_EMAIL,FASTMAIL_FROM_NAME
|
||||
script: |
|
||||
set -x
|
||||
cd /opt/eagle0
|
||||
|
||||
# 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}"
|
||||
# Export env vars for docker compose (appleboy/ssh-action sets them but doesn't export)
|
||||
export AUTH_IMAGE="${AUTH_IMAGE}"
|
||||
export DISCORD_CLIENT_ID="${DISCORD_CLIENT_ID}"
|
||||
export DISCORD_CLIENT_SECRET="${DISCORD_CLIENT_SECRET}"
|
||||
export GOOGLE_CLIENT_ID="${GOOGLE_CLIENT_ID}"
|
||||
export GOOGLE_CLIENT_SECRET="${GOOGLE_CLIENT_SECRET}"
|
||||
export GH_OAUTH_CLIENT_ID="${GH_OAUTH_CLIENT_ID}"
|
||||
export GH_OAUTH_CLIENT_SECRET="${GH_OAUTH_CLIENT_SECRET}"
|
||||
export APPLE_SIGNIN_CLIENT_ID="${APPLE_SIGNIN_CLIENT_ID}"
|
||||
export APPLE_TEAM_ID="${APPLE_TEAM_ID}"
|
||||
export APPLE_SIGNIN_KEY_ID="${APPLE_SIGNIN_KEY_ID}"
|
||||
export APPLE_SIGNIN_PRIVATE_KEY="${APPLE_SIGNIN_PRIVATE_KEY}"
|
||||
export TWITCH_CLIENT_ID="${TWITCH_CLIENT_ID}"
|
||||
export TWITCH_CLIENT_SECRET="${TWITCH_CLIENT_SECRET}"
|
||||
export JWT_PRIVATE_KEY="${JWT_PRIVATE_KEY}"
|
||||
export FASTMAIL_API_TOKEN="${FASTMAIL_API_TOKEN}"
|
||||
export FASTMAIL_FROM_EMAIL="${FASTMAIL_FROM_EMAIL}"
|
||||
export 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
|
||||
|
||||
echo "Deploying auth service: $AUTH_IMAGE"
|
||||
|
||||
# Use crane to pull image
|
||||
echo "Installing crane..."
|
||||
curl -sL https://github.com/google/go-containerregistry/releases/download/v0.20.2/go-containerregistry_Linux_x86_64.tar.gz | tar xzf - crane
|
||||
chmod +x crane
|
||||
# Pull the image directly (docker is already logged in)
|
||||
echo "Pulling Auth image..."
|
||||
docker pull "${AUTH_IMAGE}" || { echo "ERROR: Failed to pull auth image"; exit 1; }
|
||||
|
||||
echo "Pulling Auth image with crane..."
|
||||
./crane pull "${AUTH_IMAGE}" auth.tar || { echo "ERROR: Failed to pull auth image"; exit 1; }
|
||||
echo "Loading Auth image into Docker..."
|
||||
docker load -i auth.tar
|
||||
rm auth.tar
|
||||
rm ./crane
|
||||
# Tag as :latest locally so any fallback uses correct image
|
||||
docker tag "${AUTH_IMAGE}" registry.digitalocean.com/eagle0/auth-server:latest
|
||||
|
||||
# Only recreate the auth container (not eagle, shardok, etc.)
|
||||
docker compose -f docker-compose.prod.yml up -d --no-deps --force-recreate auth
|
||||
# Debug: check environment and .env file
|
||||
echo "DEBUG: AUTH_IMAGE=$AUTH_IMAGE"
|
||||
env | grep AUTH || echo "AUTH_IMAGE not in env output"
|
||||
if [ -f .env ]; then
|
||||
echo "DEBUG: .env file contents related to AUTH:"
|
||||
grep AUTH .env || echo "No AUTH in .env"
|
||||
fi
|
||||
|
||||
# Recreate auth container - pass AUTH_IMAGE explicitly on command line
|
||||
AUTH_IMAGE="${AUTH_IMAGE}" docker compose -f docker-compose.prod.yml up -d --no-deps --force-recreate auth
|
||||
|
||||
# Wait for health check
|
||||
sleep 5
|
||||
|
||||
# Verify container is using the correct image
|
||||
# Note: docker-compose may use :latest tag (which we tagged to the correct image)
|
||||
echo "=== Verifying auth container image ==="
|
||||
RUNNING_IMAGE=$(docker inspect auth-server --format '{{.Config.Image}}')
|
||||
RUNNING_DIGEST=$(docker inspect auth-server --format '{{.Image}}')
|
||||
EXPECTED_DIGEST=$(docker inspect "${AUTH_IMAGE}" --format '{{.Id}}')
|
||||
echo "Expected image: ${AUTH_IMAGE}"
|
||||
echo "Running image: ${RUNNING_IMAGE}"
|
||||
echo "Expected digest: ${EXPECTED_DIGEST}"
|
||||
echo "Running digest: ${RUNNING_DIGEST}"
|
||||
|
||||
if [ "$RUNNING_DIGEST" != "$EXPECTED_DIGEST" ]; then
|
||||
echo "ERROR: Container is running wrong image!"
|
||||
exit 1
|
||||
fi
|
||||
echo "Image digests match - correct image is running"
|
||||
|
||||
# Show container status
|
||||
docker compose -f docker-compose.prod.yml ps auth
|
||||
|
||||
# Verify container is using correct image
|
||||
echo "=== Verifying auth container image ==="
|
||||
docker compose -f docker-compose.prod.yml images auth
|
||||
# Restart nginx with updated config (copied via SCP step)
|
||||
echo "=== Restarting nginx ==="
|
||||
docker compose -f docker-compose.prod.yml up -d --no-deps --force-recreate nginx
|
||||
echo "Nginx restarted"
|
||||
|
||||
# Cleanup old images
|
||||
docker image prune -f
|
||||
|
||||
@@ -5,9 +5,15 @@ on:
|
||||
branches: [ "main" ]
|
||||
paths:
|
||||
# Note: C++ changes trigger shardok_arm64_build.yml instead
|
||||
# Note: Auth changes trigger auth_build.yml instead
|
||||
- 'src/main/go/**'
|
||||
- '!src/main/go/net/eagle0/authservice/**'
|
||||
- '!src/main/go/net/eagle0/authcli/**'
|
||||
- 'src/main/scala/**'
|
||||
- 'src/main/protobuf/**'
|
||||
- '!src/main/protobuf/net/eagle0/eagle/api/auth.proto'
|
||||
- '!src/main/protobuf/net/eagle0/eagle/api/admin/**'
|
||||
- '!src/main/protobuf/net/eagle0/eagle/internal/auth_internal.proto'
|
||||
- 'src/main/resources/**'
|
||||
- 'ci/BUILD.bazel'
|
||||
- 'MODULE.bazel'
|
||||
@@ -162,6 +168,12 @@ jobs:
|
||||
DISCORD_CLIENT_SECRET: ${{ secrets.DISCORD_CLIENT_SECRET }}
|
||||
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
|
||||
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
|
||||
GH_OAUTH_CLIENT_ID: ${{ secrets.GH_OAUTH_CLIENT_ID }}
|
||||
GH_OAUTH_CLIENT_SECRET: ${{ secrets.GH_OAUTH_CLIENT_SECRET }}
|
||||
APPLE_SIGNIN_CLIENT_ID: ${{ secrets.APPLE_SIGNIN_CLIENT_ID }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
APPLE_SIGNIN_KEY_ID: ${{ secrets.APPLE_SIGNIN_KEY_ID }}
|
||||
APPLE_SIGNIN_PRIVATE_KEY: ${{ secrets.APPLE_SIGNIN_PRIVATE_KEY }}
|
||||
SHARDOK_ADDRESS: ${{ secrets.SHARDOK_ADDRESS }}
|
||||
SHARDOK_AUTH_TOKEN: ${{ secrets.SHARDOK_AUTH_TOKEN }}
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
@@ -192,7 +204,7 @@ jobs:
|
||||
# Create directory structure on remote
|
||||
ssh -i ~/.ssh/deploy_key deploy@"$DO_DROPLET_IP" bash -s << 'SETUP_DIRS'
|
||||
set -e
|
||||
mkdir -p /opt/eagle0/scripts/bin /opt/eagle0/nginx /opt/eagle0/deploy
|
||||
mkdir -p /opt/eagle0/scripts/bin /opt/eagle0/nginx
|
||||
rm -f /opt/eagle0/scripts/bin/warmup
|
||||
SETUP_DIRS
|
||||
|
||||
@@ -201,7 +213,6 @@ jobs:
|
||||
scp -i ~/.ssh/deploy_key nginx/nginx.conf deploy@"$DO_DROPLET_IP":/opt/eagle0/nginx/
|
||||
scp -i ~/.ssh/deploy_key scripts/deploy-blue-green.sh scripts/warmup-eagle.sh deploy@"$DO_DROPLET_IP":/opt/eagle0/scripts/
|
||||
scp -i ~/.ssh/deploy_key scripts/bin/warmup deploy@"$DO_DROPLET_IP":/opt/eagle0/scripts/bin/
|
||||
scp -i ~/.ssh/deploy_key deploy/env.template deploy/update-env.sh deploy@"$DO_DROPLET_IP":/opt/eagle0/deploy/
|
||||
|
||||
- name: Deploy to production droplet
|
||||
run: |
|
||||
@@ -209,57 +220,89 @@ jobs:
|
||||
set -ex
|
||||
cd /opt/eagle0
|
||||
|
||||
# Environment variables passed via heredoc
|
||||
EAGLE_IMAGE="${EAGLE_IMAGE}"
|
||||
ADMIN_IMAGE="${ADMIN_IMAGE}"
|
||||
JFR_SIDECAR_IMAGE="${JFR_SIDECAR_IMAGE}"
|
||||
OPENAI_API_KEY="${OPENAI_API_KEY}"
|
||||
GPT_MODEL_NAME="${GPT_MODEL_NAME}"
|
||||
EAGLE_ENABLE_S3="${EAGLE_ENABLE_S3}"
|
||||
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_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}"
|
||||
# =================================================================
|
||||
# CRITICAL: Validate environment variables before proceeding
|
||||
# This catches GitHub Actions secret store hiccups early
|
||||
# =================================================================
|
||||
validate_env() {
|
||||
local var_name="\$1"
|
||||
local var_value="\$2"
|
||||
local default_value="\${3:-}"
|
||||
|
||||
if [ -z "\${var_value}" ]; then
|
||||
echo "ERROR: \${var_name} is empty. GitHub Actions secrets may have failed to load."
|
||||
echo "Please retry the workflow."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check if we got a default value instead of the real secret
|
||||
if [ -n "\${default_value}" ] && [ "\${var_value}" = "\${default_value}" ]; then
|
||||
echo "ERROR: \${var_name} has default value '\${default_value}' instead of the actual secret."
|
||||
echo "This indicates GitHub Actions secrets failed to load. Please retry the workflow."
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
echo "Validating critical environment variables..."
|
||||
|
||||
# These are the raw values from GitHub Actions (before export)
|
||||
# We check them before exporting to catch issues early
|
||||
VALIDATION_FAILED=0
|
||||
|
||||
validate_env "SHARDOK_ADDRESS" "${SHARDOK_ADDRESS}" "" || VALIDATION_FAILED=1
|
||||
validate_env "EAGLE_IMAGE" "${EAGLE_IMAGE}" "" || VALIDATION_FAILED=1
|
||||
validate_env "JWT_PRIVATE_KEY" "${JWT_PRIVATE_KEY}" "" || VALIDATION_FAILED=1
|
||||
validate_env "DO_REGISTRY_TOKEN" "${DO_REGISTRY_TOKEN}" "" || VALIDATION_FAILED=1
|
||||
|
||||
if [ "\${VALIDATION_FAILED}" -eq 1 ]; then
|
||||
echo ""
|
||||
echo "========================================="
|
||||
echo "DEPLOYMENT ABORTED: Missing critical secrets"
|
||||
echo "This is likely a transient GitHub Actions issue."
|
||||
echo "Please retry the workflow."
|
||||
echo "========================================="
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "All critical environment variables validated successfully."
|
||||
|
||||
# =================================================================
|
||||
# Export environment variables for docker compose
|
||||
# These are passed via heredoc and exported so child processes (docker compose) can access them
|
||||
export EAGLE_IMAGE="${EAGLE_IMAGE}"
|
||||
export ADMIN_IMAGE="${ADMIN_IMAGE}"
|
||||
export JFR_SIDECAR_IMAGE="${JFR_SIDECAR_IMAGE}"
|
||||
export OPENAI_API_KEY="${OPENAI_API_KEY}"
|
||||
export GPT_MODEL_NAME="${GPT_MODEL_NAME:-gpt-4o}"
|
||||
export EAGLE_ENABLE_S3="${EAGLE_ENABLE_S3:-false}"
|
||||
export DO_SPACES_ACCESS_KEY="${DO_SPACES_ACCESS_KEY}"
|
||||
export DO_SPACES_SECRET_KEY="${DO_SPACES_SECRET_KEY}"
|
||||
export JWT_PRIVATE_KEY="${JWT_PRIVATE_KEY}"
|
||||
export DISCORD_CLIENT_ID="${DISCORD_CLIENT_ID}"
|
||||
export DISCORD_CLIENT_SECRET="${DISCORD_CLIENT_SECRET}"
|
||||
export GOOGLE_CLIENT_ID="${GOOGLE_CLIENT_ID}"
|
||||
export GOOGLE_CLIENT_SECRET="${GOOGLE_CLIENT_SECRET}"
|
||||
export GH_OAUTH_CLIENT_ID="${GH_OAUTH_CLIENT_ID}"
|
||||
export GH_OAUTH_CLIENT_SECRET="${GH_OAUTH_CLIENT_SECRET}"
|
||||
export APPLE_SIGNIN_CLIENT_ID="${APPLE_SIGNIN_CLIENT_ID}"
|
||||
export APPLE_TEAM_ID="${APPLE_TEAM_ID}"
|
||||
export APPLE_SIGNIN_KEY_ID="${APPLE_SIGNIN_KEY_ID}"
|
||||
export APPLE_SIGNIN_PRIVATE_KEY="${APPLE_SIGNIN_PRIVATE_KEY}"
|
||||
export SHARDOK_ADDRESS="${SHARDOK_ADDRESS}"
|
||||
export SHARDOK_AUTH_TOKEN="${SHARDOK_AUTH_TOKEN}"
|
||||
export SENTRY_DSN="${SENTRY_DSN}"
|
||||
export FASTMAIL_API_TOKEN="${FASTMAIL_API_TOKEN}"
|
||||
export FASTMAIL_FROM_EMAIL="${FASTMAIL_FROM_EMAIL}"
|
||||
export FASTMAIL_FROM_NAME="${FASTMAIL_FROM_NAME}"
|
||||
export DO_REGISTRY_TOKEN="${DO_REGISTRY_TOKEN}"
|
||||
|
||||
# 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."
|
||||
fi
|
||||
|
||||
# Update env vars
|
||||
chmod +x deploy/update-env.sh
|
||||
cd deploy && ./update-env.sh \
|
||||
"EAGLE_IMAGE=\${EAGLE_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
|
||||
|
||||
@@ -287,20 +330,43 @@ jobs:
|
||||
|
||||
echo "All images pulled successfully"
|
||||
|
||||
# =================================================================
|
||||
# Verify Shardok connectivity before proceeding with deployment
|
||||
# This catches network/firewall issues early
|
||||
# =================================================================
|
||||
echo "Verifying Shardok connectivity..."
|
||||
SHARDOK_HOST=\$(echo "\${SHARDOK_ADDRESS}" | cut -d: -f1)
|
||||
SHARDOK_PORT=\$(echo "\${SHARDOK_ADDRESS}" | cut -d: -f2)
|
||||
|
||||
# Try to connect to Shardok (timeout after 10 seconds)
|
||||
if nc -z -w 10 "\${SHARDOK_HOST}" "\${SHARDOK_PORT}" 2>/dev/null; then
|
||||
echo "Shardok connectivity verified: \${SHARDOK_ADDRESS} is reachable"
|
||||
else
|
||||
echo ""
|
||||
echo "========================================="
|
||||
echo "ERROR: Cannot reach Shardok at \${SHARDOK_ADDRESS}"
|
||||
echo "This may indicate:"
|
||||
echo " - Shardok server is not running on Hetzner"
|
||||
echo " - Network/firewall issues between DigitalOcean and Hetzner"
|
||||
echo " - Incorrect SHARDOK_ADDRESS configuration"
|
||||
echo ""
|
||||
echo "DEPLOYMENT ABORTED: Shardok must be reachable for battles to work."
|
||||
echo "========================================="
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Stop local shardok container if running (now runs on Hetzner)
|
||||
docker stop shardok-server 2>/dev/null || true
|
||||
docker rm shardok-server 2>/dev/null || true
|
||||
|
||||
# Deploy Eagle with blue-green (handles eagle, nginx, admin, jfr-sidecar)
|
||||
# Note: Shardok runs on Hetzner, deployed separately via shardok_arm64_build.yml
|
||||
# Note: Auth is deployed separately via auth_build.yml - do NOT touch auth here
|
||||
chmod +x /opt/eagle0/scripts/*.sh
|
||||
[ -f "/opt/eagle0/scripts/bin/warmup" ] && chmod +x /opt/eagle0/scripts/bin/warmup
|
||||
GIT_SHA=\$(echo "\${EAGLE_IMAGE}" | sed 's/.*://')
|
||||
/opt/eagle0/scripts/deploy-blue-green.sh "\${GIT_SHA}"
|
||||
|
||||
# Ensure auth is running
|
||||
docker compose -f docker-compose.prod.yml up -d auth
|
||||
|
||||
# Verify
|
||||
sleep 10
|
||||
docker compose -f docker-compose.prod.yml ps
|
||||
|
||||
@@ -61,19 +61,24 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: true
|
||||
clean: false
|
||||
clean: true # Remove untracked files like old SparklePlugin.bundle
|
||||
fetch-depth: 0 # For version numbering from git history
|
||||
|
||||
- name: Pull LFS files
|
||||
run: git lfs pull
|
||||
|
||||
- name: Restore Library/
|
||||
env:
|
||||
UNITY_CACHE_PLATFORM: mac
|
||||
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/
|
||||
if: success()
|
||||
env:
|
||||
UNITY_CACHE_PLATFORM: mac
|
||||
run: ./ci/github_actions/persist_library.sh
|
||||
|
||||
- name: Inject Sparkle Framework
|
||||
@@ -256,8 +261,9 @@ jobs:
|
||||
echo "$SPARKLE_EDDSA_PRIVATE_KEY" > "$SPARKLE_PRIVATE_KEY_PATH"
|
||||
chmod 600 "$SPARKLE_PRIVATE_KEY_PATH"
|
||||
|
||||
VERSION=$(git describe --tags --always)
|
||||
# Use commit count for automatic incrementing versions (e.g., 1.0.9548)
|
||||
BUILD_NUMBER=$(git rev-list --count HEAD)
|
||||
VERSION="1.0.${BUILD_NUMBER}"
|
||||
|
||||
bazel run //src/main/go/net/eagle0/build/mac_build_handler:mac_build_handler -- \
|
||||
"/tmp/eagle0/eagle0MAC/eagle0.app" \
|
||||
|
||||
@@ -46,26 +46,26 @@ jobs:
|
||||
for REPO in $REPOS; do
|
||||
echo "=== Processing repository: ${REPO} ==="
|
||||
|
||||
# Get all manifests with their tags and dates
|
||||
# Filter out header row and empty lines
|
||||
MANIFESTS=$(doctl registry repository list-manifests "${REPO}" --format Digest,UpdatedAt,Tags --no-header 2>/dev/null | grep -v '^Digest' | grep -v '^$' || echo "")
|
||||
# Get all manifests with their tags and dates using JSON output for reliable parsing
|
||||
MANIFESTS_JSON=$(doctl registry repository list-manifests "${REPO}" --output json 2>/dev/null || echo "[]")
|
||||
|
||||
if [ -z "$MANIFESTS" ]; then
|
||||
if [ "$MANIFESTS_JSON" = "[]" ] || [ -z "$MANIFESTS_JSON" ]; then
|
||||
echo " No manifests found"
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "$MANIFESTS" | while read -r DIGEST UPDATED_AT TAGS; do
|
||||
# Parse JSON and process each manifest
|
||||
echo "$MANIFESTS_JSON" | jq -r '.[] | "\(.digest) \(.updated_at) \(.tags // [] | join(","))"' | while read -r DIGEST UPDATED_AT TAGS; do
|
||||
# Skip if no digest or if it doesn't look like a valid digest (sha256:...)
|
||||
if [ -z "$DIGEST" ] || ! echo "$DIGEST" | grep -q '^sha256:'; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Parse the date
|
||||
# Parse the date (ISO 8601 format from JSON)
|
||||
MANIFEST_DATE=$(date -d "$UPDATED_AT" +%s 2>/dev/null || echo "0")
|
||||
|
||||
# Skip protected tags (latest, arm64-latest)
|
||||
if echo "$TAGS" | grep -qE '(^|,)(latest|arm64-latest)(,|$)'; then
|
||||
if echo ",$TAGS," | grep -qE ',(latest|arm64-latest),'; then
|
||||
echo " KEEP: ${DIGEST:0:20}... (protected tag: $TAGS)"
|
||||
continue
|
||||
fi
|
||||
|
||||
@@ -49,14 +49,19 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: true
|
||||
clean: false
|
||||
clean: true # Remove untracked files from previous builds
|
||||
- name: Pull lfs files
|
||||
run: git lfs pull
|
||||
- name: Restore Library/
|
||||
env:
|
||||
UNITY_CACHE_PLATFORM: windows
|
||||
run: ./ci/github_actions/restore_library.sh
|
||||
- name: Build Windows unity
|
||||
run: ./ci/github_actions/build_unity.sh "/tmp/eagle0/eagle0WIN"
|
||||
- name: Persist Library/
|
||||
if: success()
|
||||
env:
|
||||
UNITY_CACHE_PLATFORM: windows
|
||||
run: ./ci/github_actions/persist_library.sh
|
||||
- name: Deploy Windows unity
|
||||
if: success() && github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
|
||||
-213
@@ -1,213 +0,0 @@
|
||||
# Deproto Migration Plan
|
||||
|
||||
This document tracks the migration from protobuf types to native Scala models inside the Eagle game engine.
|
||||
|
||||
## Architectural Decisions
|
||||
|
||||
1. **Keep proto for persistence**: Yes - protobuf is used for persisting game state
|
||||
2. **Keep proto for Shardok communication**: Yes - protobuf is used for Eagle-Shardok gRPC communication
|
||||
3. **Use Scala views inside the library**: Yes - use native Scala types like `ProvinceView`, `FactionView`, `HeroView`, etc. within the library code
|
||||
|
||||
## Recent Completed Work
|
||||
|
||||
### GameState Round-Trip Elimination (PRs #4913, #4914, #4915)
|
||||
|
||||
Eliminated wasteful Scala→proto→Scala conversions in the hot path:
|
||||
|
||||
1. **LLM Pipeline** (#4913): `LlmRequestWithGameState` now uses Scala `GameState` instead of proto. All ~38 prompt generators updated to use Scala model types (`FactionT`, `HeroT`, `ProvinceT`).
|
||||
|
||||
2. **ActionWithResultingState Caching** (#4914): Added `precomputedScalaState: Option[GameState]` to cache Scala state when available, avoiding `fromProto()` conversion in `stateAfter()`.
|
||||
|
||||
3. **PostResults Simplification** (#4915): Changed `PostResults.gameState` from proto to `Option[GameState]` (Scala), eliminating `toProto()` calls when creating PostResults.
|
||||
|
||||
## Migration Pattern
|
||||
|
||||
The codebase follows a **Legacy* pattern** for separating proto-dependent and protoless code:
|
||||
|
||||
- **Protoless utilities**: `FactionUtils`, `HeroUtils`, `ProvinceUtils`, `ProvinceDistances`, etc.
|
||||
- **Proto-dependent utilities**: `LegacyFactionUtils`, `LegacyHeroUtils`, `LegacyProvinceUtils`, `LegacyProvinceDistances`, etc.
|
||||
|
||||
When migrating a file:
|
||||
1. Create a `Legacy*` version containing the proto-dependent methods
|
||||
2. Keep the original file name for protoless methods
|
||||
3. Update callers to use the appropriate version based on their context
|
||||
|
||||
## Migration Status
|
||||
|
||||
### Fully Protoless (no proto imports)
|
||||
|
||||
**Utilities:**
|
||||
- [x] `FactionUtils` - has protoless `ownedNeighbors` method
|
||||
- [x] `ProvinceDistances` - split into protoless + `LegacyProvinceDistances`
|
||||
- [x] `SwornBrotherChooser` - fully protoless (removed `bestChoiceProto`)
|
||||
|
||||
**Command Selectors (all use native GameState):**
|
||||
- [x] `AllianceOfferCommandSelector`
|
||||
- [x] `AlmsCommandSelector`
|
||||
- [x] `AttackCommandChooser`
|
||||
- [x] `ExpandCommandSelector`
|
||||
- [x] `HeroGiftCommandSelector`
|
||||
- [x] `ImproveCommandSelector`
|
||||
- [x] `MarchTowardProvinceCommandChooser` - in AI folder, uses native GameState (callers convert)
|
||||
- [x] `OrganizeCommandSelector`
|
||||
- [x] `RansomOfferHelpers`
|
||||
- [x] `SeekMoreLeadersCommandChooser` - in AI folder, uses native GameState
|
||||
- [x] `TruceOfferCommandSelector`
|
||||
- [x] `TrustForDiplomacy`
|
||||
|
||||
**Quest Command Selectors (all protoless):**
|
||||
- [x] `AllianceQuestCommandChooser`
|
||||
- [x] `AlmsAcrossRealmQuestCommandChooser`
|
||||
- [x] `AlmsToProvinceQuestCommandChooser`
|
||||
- [x] `DismissSpecificVassalCommandChooser`
|
||||
- [x] `GiveToHeroesAcrossRealmQuestCommandChooser`
|
||||
- [x] `GiveToHeroesInProvinceQuestCommandChooser`
|
||||
- [x] `ImproveQuestCommandChooser`
|
||||
- [x] `QuestCommandChooser`
|
||||
- [x] `TruceCountQuestCommandChooser`
|
||||
- [x] `TruceWithFactionQuestCommandChooser`
|
||||
|
||||
### Fully Protoless
|
||||
|
||||
- [x] `AIClientUtils` - has protoless overloads (`takenHeroIdsForMarchTowardFocus`, `mostPowerfulHeroes`)
|
||||
- [x] `AttackCommandChooser` - uses Scala `GameState` and `BattalionViewC` (Scala view type)
|
||||
- [x] `BattalionPower` - has `estimatedPower(BattalionViewC)` for recon data with optional stats
|
||||
- [x] `CommandChoiceHelpers` - fully protoless, uses Scala `GameState` throughout
|
||||
- [x] `ProvinceGoldSurplusCalculator` - fully protoless (callers use converters)
|
||||
- [x] `HeroSelector` - fully protoless (removed dead `minimallyFatiguedHeroesProto`)
|
||||
|
||||
### AI Layer ✅ COMPLETE
|
||||
|
||||
All AI and command chooser code is now fully protoless:
|
||||
- [x] `AttackDecisionCommandChooser` - uses Scala GameState
|
||||
- [x] `CommandChooser` - trait uses Scala GameState
|
||||
- [x] `FulfillQuestsCommandSelector` - uses Scala GameState
|
||||
- [x] `MidGameAIClient` - uses Scala GameState internally
|
||||
|
||||
### Still Using Proto GameState (Boundary Code)
|
||||
|
||||
These files use proto GameState because they're at system boundaries:
|
||||
|
||||
**View Filters (client projection):**
|
||||
- `view_filters/GameStateViewFilter` - has Scala overload, uses Scala sub-filters
|
||||
- `view_filters/ProvinceViewFilter` - has Scala overloads for some methods
|
||||
- `view_filters/FactionViewFilter` - has Scala overload
|
||||
- `view_filters/HeroViewFilter` - has Scala overload
|
||||
- `view_filters/BattalionNameFilter` - has Scala overload
|
||||
- `view_filters/BattleFilter` - has Scala overload
|
||||
|
||||
**Legacy Utilities (to be deprecated):**
|
||||
- `LegacyProvinceDistances`, `LegacyFactionUtils`, `LegacyHeroUtils`, etc.
|
||||
- Used by code that still needs proto GameState
|
||||
|
||||
**Persistence/Action System:**
|
||||
- `ActionResultTApplier`, `ActionResultProtoApplier` - apply results to proto state
|
||||
- `ActionWithResultingState` - caches both proto and Scala state
|
||||
|
||||
**Shardok Interface (gRPC boundary):**
|
||||
- `ShardokInterfaceGrpcClient`, `ShardokInterfaceProxy` - must use proto for C++ communication
|
||||
|
||||
## Next Steps
|
||||
|
||||
### Phase 1-3: AI Layer ✅ COMPLETE
|
||||
|
||||
The entire AI decision-making layer is now protoless.
|
||||
|
||||
### Phase 4: View Filters ✅ COMPLETE
|
||||
|
||||
The view_filters package migration is complete:
|
||||
|
||||
**Completed:**
|
||||
- [x] `GameStateViewFilter` - added Scala GameState overload (uses Scala sub-filters)
|
||||
- [x] `ProvinceViewFilter` - already has Scala overloads for server-side views
|
||||
- [x] `HumanPlayerClientConnectionState` - updated to pass Scala GameState directly
|
||||
- [x] `HeroViewFilter` - added Scala overload
|
||||
- [x] `FactionViewFilter` - added Scala overload
|
||||
- [x] `Visibility` - added Scala overloads
|
||||
|
||||
**Still Using Proto:**
|
||||
- [x] `BattalionNameFilter` - has Scala overload
|
||||
- [x] `BattleFilter` - has Scala overload
|
||||
- [ ] `ActionResultFilter` - uses proto internally (boundary code)
|
||||
|
||||
**Strategy:**
|
||||
1. Add Scala GameState overloads to view filter methods
|
||||
2. Update callers to pass Scala GameState where available
|
||||
3. Eventually deprecate proto versions
|
||||
|
||||
### Phase 5: Legacy Utility Cleanup (IN PROGRESS)
|
||||
|
||||
Remove Legacy* utilities by migrating remaining callers:
|
||||
1. Identify callers of each Legacy* util
|
||||
2. Update callers to use protoless versions
|
||||
3. Delete Legacy* files when no longer needed
|
||||
|
||||
**Deleted (no production callers):**
|
||||
- [x] `LegacyProvinceDistances` - deleted (no callers)
|
||||
- [x] `LegacyBattalionSuitability` - deleted (no callers)
|
||||
- [x] `LegacyFoodConsumptionUtils` - deleted (no callers)
|
||||
- [x] `LegacyHandleRiotUtils` - deleted (no callers)
|
||||
|
||||
**Refactored to Thin Wrappers (delegating to protoless versions):**
|
||||
- [x] `LegacyRansomValidity` - already a thin wrapper delegating to `RansomValidity`
|
||||
- [x] `LegacyRecruitmentOdds` - refactored to delegate to `RecruitmentOdds`
|
||||
|
||||
**Parallel Implementations (proto mirrors protoless):**
|
||||
- [x] `FactionUtils` / `LegacyFactionUtils` - both have matching APIs; LegacyFactionUtils used by boundary code (24+ callers)
|
||||
- [x] `HeroUtils` / `LegacyHeroUtils` - both have matching APIs; LegacyHeroUtils used by boundary code (10 callers)
|
||||
- [x] `ProvinceUtils` / `LegacyProvinceUtils` - both have matching APIs; LegacyProvinceUtils used by boundary code (20 callers: availability factories, view filters)
|
||||
|
||||
**Parallel Implementations (awaiting migration of callers):**
|
||||
- [x] `BattalionUtils` / `LegacyBattalionUtils` - both have matching core methods; LegacyBattalionUtils used by boundary code (4 callers)
|
||||
- [x] `BattalionViewFilter` / `LegacyBattalionViewFilter` - protoless version exists; Legacy used by view filters, action appliers (3 callers)
|
||||
- [x] `BattalionTypeFinder` / `LegacyBattalionTypeFinder` - protoless version exists; Legacy used by validators (1 caller: RuntimeValidator)
|
||||
|
||||
### Recent Caller Migration
|
||||
|
||||
**CheckForFulfilledQuestsAction** - migrated to use protoless `BattalionTypeFinder`:
|
||||
- Changed `battalionTypes` parameter from proto `Vector[BattalionType]` to Scala `Vector[BattalionType]`
|
||||
- Updated callers (EngineImpl, EndVassalCommandsPhaseAction) to pass Scala types directly
|
||||
- Eliminated wasteful `BattalionTypeConverter.toProto()` conversions
|
||||
|
||||
**ExpandedUnaffiliatedHeroUtils** - added Scala overload:
|
||||
- New overload takes Scala `GameState` and `UnaffiliatedHeroT` directly
|
||||
- Added `UnaffiliatedHeroConverter.unaffiliatedHeroTypeToProto()` helper for efficient enum conversion
|
||||
- Proto overload retained for backward compatibility
|
||||
|
||||
**AvailablePleaseRecruitMeCommandFactory** - eliminated wasteful proto conversions:
|
||||
- Now uses `ExpandedUnaffiliatedHeroUtils.expandedUnaffiliatedHero(ScalaGameState, UnaffiliatedHeroT)` directly
|
||||
- Removed `GameStateConverter.toProto()` and `UnaffiliatedHeroConverter.toProto()` calls
|
||||
- Factory is now fully protoless internally (still returns proto types for API boundary)
|
||||
|
||||
**ProvinceViewFilter** - added Scala overload with faction filtering:
|
||||
- New `filteredProvinceView(province: ProvinceT, gs: ScalaGameState, factionId: FactionId)` overload
|
||||
- Uses protoless `FactionUtils.hasAlliance`, `Visibility.hasFullVisibility`, and `ProvinceUtils.incomingOthers`
|
||||
- Handles reconned provinces directly from Scala `FactionT.reconnedProvinces` (already Scala type)
|
||||
- Added helper methods: `fullProvinceInfoScala`, `maybeIncomingAttackersScala`, `unaffiliatedHeroInfoScala`
|
||||
- Events still converted to proto at the end (ProvinceView.knownEvents uses proto events)
|
||||
|
||||
**GameStateViewFilter** - eliminated GameStateConverter.toProto() call:
|
||||
- Scala overload now fully protoless internally
|
||||
- Uses the new ProvinceViewFilter Scala overload with faction filtering
|
||||
- Converts `battalionTypes` and `chronicleEntries` to proto only at output boundary
|
||||
|
||||
## Key Files
|
||||
|
||||
### Protoless Model Types
|
||||
|
||||
- `src/main/scala/net/eagle0/eagle/model/state/game_state/GameState.scala` - native Scala GameState
|
||||
- `src/main/scala/net/eagle0/eagle/model/state/province/ProvinceView.scala` - province view type
|
||||
- `src/main/scala/net/eagle0/eagle/model/state/faction/FactionView.scala` - faction view type
|
||||
- `src/main/scala/net/eagle0/eagle/model/state/hero/HeroView.scala` - hero view type
|
||||
|
||||
### Proto Converters
|
||||
|
||||
- `src/main/scala/net/eagle0/eagle/model/proto_converters/game_state/` - converts between proto and Scala types
|
||||
|
||||
## Notes
|
||||
|
||||
- The AI client code (`src/main/scala/net/eagle0/eagle/ai/`) is now fully protoless
|
||||
- Proto GameState is still needed at boundaries: persistence, gRPC to Shardok
|
||||
- `PerformUnaffiliatedHeroesAction` and the LLM pipeline use protoless `GameState`
|
||||
- `GameStateViewFilter` Scala overload is now fully protoless internally (converts to proto only at output)
|
||||
- `ProvinceViewFilter` has Scala overloads for all three modes: no filtering, faction filtering, and withdrawn-from view
|
||||
@@ -130,6 +130,13 @@ bazel_dep(name = "apple_support", version = "1.21.1", repo_name = "build_bazel_a
|
||||
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")
|
||||
|
||||
# Register Apple CC toolchain for Objective-C compilation
|
||||
apple_cc_configure = use_extension(
|
||||
"@build_bazel_apple_support//crosstool:setup.bzl",
|
||||
"apple_cc_configure_extension",
|
||||
)
|
||||
use_repo(apple_cc_configure, "local_config_apple_cc")
|
||||
|
||||
#
|
||||
# Protocol Buffers & RPC
|
||||
#
|
||||
@@ -293,6 +300,17 @@ http_archive(
|
||||
],
|
||||
)
|
||||
|
||||
# Sparkle framework for macOS auto-updates
|
||||
SPARKLE_VERSION = "2.6.4"
|
||||
|
||||
http_archive(
|
||||
name = "sparkle",
|
||||
build_file = "@//external:BUILD.sparkle",
|
||||
sha256 = "50612a06038abc931f16011d7903b8326a362c1074dabccb718404ce8e585f0b",
|
||||
strip_prefix = "",
|
||||
url = "https://github.com/sparkle-project/Sparkle/releases/download/%s/Sparkle-%s.tar.xz" % (SPARKLE_VERSION, SPARKLE_VERSION),
|
||||
)
|
||||
|
||||
# Busybox static binary for Docker health checks (provides nc, wget, etc.)
|
||||
# Primary: DigitalOcean Spaces (public, reliable)
|
||||
# Fallback: busybox.net (can be unreliable/slow)
|
||||
|
||||
Generated
+1
-1
@@ -396,7 +396,7 @@
|
||||
"@@apple_support~//crosstool:setup.bzl%apple_cc_configure_extension": {
|
||||
"general": {
|
||||
"bzlTransitiveDigest": "Z3yAd66IJL0GAZUTSeMOjoHiE1SZPPwiIs/XQui5BvE=",
|
||||
"usagesDigest": "tl3VVeQX3Hzh7FhM2gjnkCwEJpRMlY5S6a850WY/xvc=",
|
||||
"usagesDigest": "DqQsfZN5lA8z+nLEEY+EpKGzQ8M73mDm/A8lofDSyus=",
|
||||
"recordedFileInputs": {},
|
||||
"recordedDirentsInputs": {},
|
||||
"envVariables": {},
|
||||
|
||||
@@ -9,9 +9,6 @@ COMMIT=$(/usr/bin/git rev-parse --short HEAD)
|
||||
/bin/echo "build protos"
|
||||
./scripts/build_protos.sh
|
||||
|
||||
/bin/echo "build plugins"
|
||||
./scripts/build_windows_plugin.sh
|
||||
|
||||
git log -3
|
||||
|
||||
/bin/echo "build Windows"
|
||||
|
||||
@@ -7,8 +7,8 @@ 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
|
||||
/bin/echo "build Sparkle plugin"
|
||||
./scripts/build_sparkle_plugin.sh
|
||||
|
||||
git log -3
|
||||
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Persist Unity Library/ cache to persistent storage
|
||||
#
|
||||
# Environment variables:
|
||||
# UNITY_CACHE_PLATFORM - Platform identifier (e.g., "mac", "windows")
|
||||
# Defaults to "mac" if not set
|
||||
#
|
||||
# Note: Library/Bee/ is excluded because it contains DAG files with hardcoded
|
||||
# file paths that become stale when project files change. This prevents
|
||||
# "Data at the root level is invalid" XML errors from stale references.
|
||||
|
||||
set -uxo pipefail
|
||||
|
||||
/bin/echo "persist Library/"
|
||||
PLATFORM="${UNITY_CACHE_PLATFORM:-mac}"
|
||||
CACHE_DIR="/tmp/eagle0/Library-${PLATFORM}"
|
||||
|
||||
/bin/echo "persist Library/ to $CACHE_DIR (excluding Bee/)"
|
||||
|
||||
# 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/
|
||||
/usr/bin/rsync -rtlDvq --exclude='Bee/' src/main/csharp/net/eagle0/clients/unity/eagle0/Library/ "$CACHE_DIR/"
|
||||
rsync_exit=$?
|
||||
|
||||
if [ $rsync_exit -eq 0 ]; then
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Restore Unity Library/ cache from persistent storage
|
||||
#
|
||||
# Environment variables:
|
||||
# UNITY_CACHE_PLATFORM - Platform identifier (e.g., "mac", "windows")
|
||||
# Defaults to "mac" if not set
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
/bin/echo "restore Library/"
|
||||
/bin/mkdir -p /tmp/eagle0/Library
|
||||
/usr/bin/rsync -rtlDvq /tmp/eagle0/Library/ src/main/csharp/net/eagle0/clients/unity/eagle0/Library/
|
||||
PLATFORM="${UNITY_CACHE_PLATFORM:-mac}"
|
||||
CACHE_DIR="/tmp/eagle0/Library-${PLATFORM}"
|
||||
|
||||
/bin/echo "restore Library/ from $CACHE_DIR"
|
||||
/bin/mkdir -p "$CACHE_DIR"
|
||||
/usr/bin/rsync -rtlDvq "$CACHE_DIR/" src/main/csharp/net/eagle0/clients/unity/eagle0/Library/
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
# Environment template for production deployment
|
||||
# This file defines all env vars used by docker-compose.prod.yml
|
||||
# Workflows should update their specific vars without overwriting others
|
||||
|
||||
# Container images (managed by respective build workflows)
|
||||
# Note: Shardok runs on Hetzner, deployed via shardok_arm64_build.yml
|
||||
EAGLE_IMAGE=registry.digitalocean.com/eagle0/eagle-server:latest
|
||||
ADMIN_IMAGE=registry.digitalocean.com/eagle0/admin-server:latest
|
||||
JFR_SIDECAR_IMAGE=registry.digitalocean.com/eagle0/jfr-sidecar:latest
|
||||
AUTH_IMAGE=registry.digitalocean.com/eagle0/auth-server:latest
|
||||
|
||||
# OpenAI / LLM
|
||||
OPENAI_API_KEY=
|
||||
GPT_MODEL_NAME=gpt-4o
|
||||
|
||||
# DigitalOcean Spaces (S3-compatible storage)
|
||||
EAGLE_ENABLE_S3=false
|
||||
DO_SPACES_ACCESS_KEY=
|
||||
DO_SPACES_SECRET_KEY=
|
||||
|
||||
# JWT authentication
|
||||
JWT_PRIVATE_KEY=
|
||||
|
||||
# OAuth providers
|
||||
DISCORD_CLIENT_ID=
|
||||
DISCORD_CLIENT_SECRET=
|
||||
GOOGLE_CLIENT_ID=
|
||||
GOOGLE_CLIENT_SECRET=
|
||||
|
||||
# Shardok connection (Hetzner ARM64 server)
|
||||
SHARDOK_ADDRESS=
|
||||
SHARDOK_AUTH_TOKEN=
|
||||
|
||||
# Monitoring
|
||||
SENTRY_DSN=
|
||||
|
||||
# Email (Fastmail JMAP)
|
||||
FASTMAIL_API_TOKEN=
|
||||
FASTMAIL_FROM_EMAIL=
|
||||
FASTMAIL_FROM_NAME=
|
||||
@@ -1,59 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Update .env file without losing other variables
|
||||
# Usage: ./update-env.sh KEY1=value1 KEY2=value2 ...
|
||||
#
|
||||
# This script:
|
||||
# 1. Creates .env from template if it doesn't exist
|
||||
# 2. Updates only the specified KEY=value pairs
|
||||
# 3. Preserves all other existing values
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
ENV_FILE="${ENV_FILE:-/opt/eagle0/.env}"
|
||||
TEMPLATE_FILE="${TEMPLATE_FILE:-$SCRIPT_DIR/env.template}"
|
||||
|
||||
# Create .env from template if it doesn't exist
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
if [ -f "$TEMPLATE_FILE" ]; then
|
||||
echo "Creating .env from template..."
|
||||
grep -v '^#' "$TEMPLATE_FILE" | grep -v '^$' > "$ENV_FILE"
|
||||
else
|
||||
echo "Creating empty .env..."
|
||||
touch "$ENV_FILE"
|
||||
fi
|
||||
chmod 600 "$ENV_FILE"
|
||||
fi
|
||||
|
||||
# Process each KEY=VALUE argument
|
||||
for arg in "$@"; do
|
||||
# Skip empty args
|
||||
[ -z "$arg" ] && continue
|
||||
|
||||
# Parse KEY=VALUE
|
||||
KEY="${arg%%=*}"
|
||||
VALUE="${arg#*=}"
|
||||
|
||||
# Skip if no key
|
||||
[ -z "$KEY" ] && continue
|
||||
|
||||
# Skip setting empty values (keeps existing value)
|
||||
if [ -z "$VALUE" ]; then
|
||||
echo "Skipping $KEY (empty value)"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Remove existing line for this key and add new one
|
||||
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"
|
||||
+18
-10
@@ -21,7 +21,7 @@ services:
|
||||
- "--gpt-model-name"
|
||||
- "${GPT_MODEL_NAME:-gpt-5.1}"
|
||||
- "--shardok-interface-remote-address"
|
||||
- "${SHARDOK_ADDRESS:-shardok:40042}"
|
||||
- "${SHARDOK_ADDRESS}"
|
||||
- "--auth-service-url"
|
||||
- "auth:40033"
|
||||
ports:
|
||||
@@ -32,9 +32,8 @@ services:
|
||||
DO_SPACES_ENDPOINT: "${DO_SPACES_ENDPOINT:-https://sfo3.digitaloceanspaces.com}"
|
||||
DO_SPACES_ACCESS_KEY: "${DO_SPACES_ACCESS_KEY:-}"
|
||||
DO_SPACES_SECRET_KEY: "${DO_SPACES_SECRET_KEY:-}"
|
||||
JWT_PRIVATE_KEY: "${JWT_PRIVATE_KEY:-}"
|
||||
DISCORD_CLIENT_ID: "${DISCORD_CLIENT_ID:-}"
|
||||
DISCORD_CLIENT_SECRET: "${DISCORD_CLIENT_SECRET:-}"
|
||||
# JWT public key for token validation (auth service handles signing)
|
||||
# Reads from /etc/eagle0/keys/public.pem via shared volume
|
||||
# Auth token for Shardok on Hetzner (required)
|
||||
SHARDOK_AUTH_TOKEN: "${SHARDOK_AUTH_TOKEN:-}"
|
||||
# Use persistent volume for save data (users, games, etc.)
|
||||
@@ -47,7 +46,7 @@ services:
|
||||
- ./archived:/app/archived # Archived completed games
|
||||
- ./jfr:/app/jfr # JFR recordings - dump with: docker exec eagle-blue jcmd 1 JFR.dump filename=/app/jfr/profile.jfr
|
||||
- jvm-tmp:/tmp # Shared with jfr-sidecar for JVM attach socket files
|
||||
- jwt-keys:/etc/eagle0/keys # Shared JWT keys with auth service
|
||||
- jwt-keys:/etc/eagle0/keys:ro # JWT public key from auth service (read-only)
|
||||
depends_on:
|
||||
- auth
|
||||
restart: unless-stopped
|
||||
@@ -71,7 +70,7 @@ services:
|
||||
- "--gpt-model-name"
|
||||
- "${GPT_MODEL_NAME:-gpt-5.1}"
|
||||
- "--shardok-interface-remote-address"
|
||||
- "${SHARDOK_ADDRESS:-shardok:40042}"
|
||||
- "${SHARDOK_ADDRESS}"
|
||||
- "--auth-service-url"
|
||||
- "auth:40033"
|
||||
ports:
|
||||
@@ -82,9 +81,8 @@ services:
|
||||
DO_SPACES_ENDPOINT: "${DO_SPACES_ENDPOINT:-https://sfo3.digitaloceanspaces.com}"
|
||||
DO_SPACES_ACCESS_KEY: "${DO_SPACES_ACCESS_KEY:-}"
|
||||
DO_SPACES_SECRET_KEY: "${DO_SPACES_SECRET_KEY:-}"
|
||||
JWT_PRIVATE_KEY: "${JWT_PRIVATE_KEY:-}"
|
||||
DISCORD_CLIENT_ID: "${DISCORD_CLIENT_ID:-}"
|
||||
DISCORD_CLIENT_SECRET: "${DISCORD_CLIENT_SECRET:-}"
|
||||
# JWT public key for token validation (auth service handles signing)
|
||||
# Reads from /etc/eagle0/keys/public.pem via shared volume
|
||||
SHARDOK_AUTH_TOKEN: "${SHARDOK_AUTH_TOKEN:-}"
|
||||
EAGLE_SAVE_DIR: "/app/saves"
|
||||
EAGLE_ARCHIVE_DIR: "/app/archived"
|
||||
@@ -95,7 +93,7 @@ services:
|
||||
- ./archived:/app/archived # Same archive directory as blue
|
||||
- ./jfr:/app/jfr # JFR recordings (same as blue)
|
||||
- jvm-tmp:/tmp # Shared with jfr-sidecar-green for JVM attach socket files
|
||||
- jwt-keys:/etc/eagle0/keys # Shared JWT keys with auth service
|
||||
- jwt-keys:/etc/eagle0/keys:ro # JWT public key from auth service (read-only)
|
||||
depends_on:
|
||||
- auth
|
||||
restart: "no" # Don't auto-restart during deployment
|
||||
@@ -133,6 +131,16 @@ services:
|
||||
DISCORD_CLIENT_SECRET: "${DISCORD_CLIENT_SECRET:-}"
|
||||
GOOGLE_CLIENT_ID: "${GOOGLE_CLIENT_ID:-}"
|
||||
GOOGLE_CLIENT_SECRET: "${GOOGLE_CLIENT_SECRET:-}"
|
||||
GH_OAUTH_CLIENT_ID: "${GH_OAUTH_CLIENT_ID:-}"
|
||||
GH_OAUTH_CLIENT_SECRET: "${GH_OAUTH_CLIENT_SECRET:-}"
|
||||
# Apple Sign-In credentials
|
||||
APPLE_SIGNIN_CLIENT_ID: "${APPLE_SIGNIN_CLIENT_ID:-}"
|
||||
APPLE_TEAM_ID: "${APPLE_TEAM_ID:-}"
|
||||
APPLE_SIGNIN_KEY_ID: "${APPLE_SIGNIN_KEY_ID:-}"
|
||||
APPLE_SIGNIN_PRIVATE_KEY: "${APPLE_SIGNIN_PRIVATE_KEY:-}"
|
||||
# Twitch OAuth credentials
|
||||
TWITCH_CLIENT_ID: "${TWITCH_CLIENT_ID:-}"
|
||||
TWITCH_CLIENT_SECRET: "${TWITCH_CLIENT_SECRET:-}"
|
||||
# Server base URL for OAuth callbacks
|
||||
SERVER_BASE_URL: "${SERVER_BASE_URL:-https://prod.eagle0.net}"
|
||||
# JWT keys - PEM files in volume, bootstrapped from JWK on first run
|
||||
|
||||
+218
-302
@@ -33,7 +33,25 @@
|
||||
|
||||
---
|
||||
|
||||
## Current State
|
||||
## Current State (January 2026)
|
||||
|
||||
### Summary
|
||||
|
||||
| Area | Proto Imports | Status |
|
||||
|------|---------------|--------|
|
||||
| AI (`/ai/`) | **0** | ✅ **Complete** |
|
||||
| Actions (`/library/actions/impl/action/`) | **0** | ✅ **Complete** |
|
||||
| Commands (`/library/actions/impl/command/`) | **0** | ✅ **Complete** |
|
||||
| Availability (`/library/actions/availability/`) | **0** | ✅ **Complete** |
|
||||
| Command Choice Helpers (`/library/util/command_choice_helpers/`) | **0** | ✅ **Complete** |
|
||||
| View Filters (`/library/util/view_filters/`) | **0** | ✅ **Complete** (except boundary code) |
|
||||
| Other Utilities (`/library/util/`) | **0** | ✅ **Complete** |
|
||||
| NewGameCreation (`/service/new_game_creation/`) | **0** | ✅ **Complete** (except input GameParameters proto) |
|
||||
| LLM Prompt Generators (`/library/actions/llm_prompt_generators/`) | **56** | ⏳ Remaining work (34 files) |
|
||||
| LLM Request Generators (`/library/actions/llm_request_generators/`) | **2** | ⏳ Remaining work (1 file) |
|
||||
| Root Library (`/library/`) | **7** | Boundary code (4 files) |
|
||||
|
||||
**Total: ~65 proto imports remaining** (down from 149)
|
||||
|
||||
### Completed Phases
|
||||
|
||||
@@ -43,346 +61,244 @@
|
||||
| Phase 2: EngineImpl | **Complete** | Holds Scala `GameState` internally |
|
||||
| Phase 3: GameHistory | **Complete** | `stateAfter` returns Scala GameState |
|
||||
| Phase 4: ActionResultT | **Complete** | All 59 actions return `ActionResultT` |
|
||||
| Phase 5: Action Base Classes | **Complete** | All `RandomSequentialResultsAction` and `DeterministicSingleResultAction` converted to T-type base classes |
|
||||
| Phase 5b: Base Class Cleanup | **Complete** | `RandomSequentialResultsAction` and `DeterministicSingleResultAction` deleted |
|
||||
| Phase 5c: RoundPhaseAdvancer Actions | **Complete** | All actions called by RoundPhaseAdvancer accept Scala GameState |
|
||||
| Phase 5d: RoundPhaseAdvancer Itself | **Complete** | RoundPhaseAdvancer.checkForPhaseAdvancement takes Scala GameState |
|
||||
| Phase 5: Action Base Classes | **Complete** | All actions converted to T-type base classes |
|
||||
| Phase 5b-d: RoundPhaseAdvancer | **Complete** | Uses Scala GameState throughout |
|
||||
| Phase 6: Core Engine | **Complete** | ActionResultApplier, RandomStateSequencer protoless |
|
||||
| Phase 6b: CommandFactory | **Complete** | Uses Scala SelectedCommand and AvailableCommand |
|
||||
| Phase 6c: AI Clients | **Complete** | AIClient, command choosers all protoless |
|
||||
| Phase 6d: CommandSelection | **Complete** | Renamed ScalaCommandSelection → CommandSelection |
|
||||
|
||||
### Phase 5c/5d Progress (Complete)
|
||||
### Recent Completions (PRs #5326-#5390)
|
||||
|
||||
`RoundPhaseAdvancer.checkForPhaseAdvancement` now accepts Scala `GameState` and `ActionResultApplier` directly (PR #4677).
|
||||
|
||||
| Action | PR | Status |
|
||||
|--------|-----|--------|
|
||||
| `PrisonerExchangeAction` | #4670 | ✅ Merged |
|
||||
| `PerformForcedTurnBackAction` | #4671 | ✅ Merged |
|
||||
| `PerformHeroDeparturesAction` | #4672 | ✅ Merged |
|
||||
| `RequestFreeForAllBattlesAction` | #4673 | ✅ Merged |
|
||||
| `EndPlayerCommandsPhaseAction` | #4674 | ✅ Merged |
|
||||
| `EndDiplomacyResolutionPhaseAction` | #4675 | ✅ Merged |
|
||||
| `RoundPhaseAdvancer` itself | #4677 | ✅ Merged |
|
||||
|
||||
### EngineImpl Progress
|
||||
|
||||
| Change | PR | Status |
|
||||
|--------|-----|--------|
|
||||
| `recursiveTransform` deleted | #4677 | ✅ Merged |
|
||||
| `recursiveTransformT` uses `RandomStateTSequencer` | #4677 | ✅ Merged |
|
||||
|
||||
### Current Architecture
|
||||
|
||||
**ActionResultT Production (100% Complete):**
|
||||
- All actions produce `ActionResultT`
|
||||
- Conversion to `ActionResultProto` happens via `ActionResultProtoConverter.toProto()`
|
||||
- No direct `ActionResultProto` construction outside the converter
|
||||
|
||||
**ActionResultProto Consumption (Next Target):**
|
||||
- `ActionResultProtoApplierImpl` - applies proto results to proto GameState
|
||||
- `RoundPhaseAdvancer` - calls converter, passes protos to applier
|
||||
- `InMemoryHistory` / `PersistedHistory` - stores proto results
|
||||
- Service layer (`GameController`, `GamesManager`, etc.) - uses proto for client communication
|
||||
1. **CommandFactory protoless** - Now accepts/returns Scala `SelectedCommand` types
|
||||
2. **AI clients protoless** - `AIClient`, `MidGameAIClient`, all command choosers use Scala types
|
||||
3. **CommandSelection cleanup** - Deleted legacy proto-based `CommandSelection`, renamed `ScalaCommandSelection` → `CommandSelection`
|
||||
4. **AvailableCommandsFactory protoless** - Returns Scala `OneProvinceAvailableCommands`
|
||||
5. **All CommandChoiceHelpers protoless** - All selector/chooser files converted
|
||||
6. **ChronicleEventGenerator protoless** (PR #5332) - Removed last proto enum import from Actions layer
|
||||
7. **ProvinceUtils protoless** (PR #5333) - Converted to use Scala `BattalionType`
|
||||
8. **FactionViewFilter protoless** (PR #5334) - Returns Scala `FactionView`, converts to proto at edge
|
||||
9. **HeroViewFilter protoless** (PR #5336) - Returns Scala `HeroView`, converts to proto at edge
|
||||
10. **ProvinceViewFilter protoless** (PR #5341) - `ProvinceView.knownEvents` uses Scala `ProvinceEvent`, converts at edge
|
||||
11. **StatWithConditionUtils & ProvinceEventUtils protoless** (PR #5342) - Deleted unused proto overloads
|
||||
12. **IncomingArmyUtils protoless** (PR #5348) - Deleted unused `stats()` method
|
||||
13. **RansomOfferHelpers deleted** (PR #5351) - Deleted unused dead code file
|
||||
14. **MapGenerator protoless** (PR #5373) - Returns Scala types instead of proto
|
||||
15. **Remove proto ActionResult from Engine layer** (PR #5378) - Engine uses Scala `ActionResultT`
|
||||
16. **NewGameCreation protoless** (PR #5381) - Uses Scala types throughout, no proto conversion
|
||||
17. **PersistedHistory Scala API** (PR #5390) - New `apply` overload accepts `Vector[ActionResultT]` directly
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Migrate to ActionResultT Consumers
|
||||
## Remaining Work
|
||||
|
||||
### Objective
|
||||
### Phase 7: View Filters ✅ Complete
|
||||
|
||||
Eliminate internal consumption of `ActionResultProto`. Everything inside the engine should work with `ActionResultT`.
|
||||
| File | Status | Migration Path |
|
||||
|------|--------|----------------|
|
||||
| `FactionViewFilter.scala` | ✅ **Complete** | Returns Scala `FactionView` (PR #5334) |
|
||||
| `HeroViewFilter.scala` | ✅ **Complete** | Returns Scala `HeroView` (PR #5336) |
|
||||
| `ProvinceViewFilter.scala` | ✅ **Complete** | Returns Scala `ProvinceView` with Scala events (PR #5341) |
|
||||
| `GameStateViewFilter.scala` | ✅ **Complete** | Returns Scala `GameStateView`, converted at boundary |
|
||||
| `GameStateViewDiffer.scala` | ✅ **Complete** | Uses Scala diff types internally, converted at boundary |
|
||||
| `BattleFilter.scala` | Keep | Uses `ShardokBattleView` (boundary code for battles) |
|
||||
|
||||
### Current Flow (Proto-Heavy)
|
||||
```
|
||||
Action.execute()
|
||||
→ ActionResultT
|
||||
→ ActionResultProtoConverter.toProto()
|
||||
→ ActionResultProto
|
||||
→ ActionResultProtoApplierImpl.applyActionResults()
|
||||
→ GameStateProto
|
||||
→ GameStateConverter.fromProto()
|
||||
→ GameStateC
|
||||
```
|
||||
**Pattern established**: Create Scala view type → Create converter → Update filter to return Scala type → Convert at edge.
|
||||
|
||||
### Target Flow (T-Types Throughout)
|
||||
```
|
||||
Action.execute()
|
||||
→ ActionResultT
|
||||
→ ActionResultApplier.applyActionResults()
|
||||
→ GameStateC
|
||||
### Phase 8: LLM Prompt Generators (~56 proto imports in 34 files)
|
||||
|
||||
(Proto conversion only at boundaries)
|
||||
```
|
||||
The LLM prompt generators still use proto types for hero/faction/province data:
|
||||
|
||||
### Key Files to Convert
|
||||
| File Category | Files | Proto Usage |
|
||||
|---------------|-------|-------------|
|
||||
| Diplomacy prompts | 12 | `Hero`, `Faction`, `Province` protos |
|
||||
| Quest prompts | 4 | `Hero`, `Faction` protos |
|
||||
| Chronicle prompts | 3 | `Hero`, `Faction`, `Province` protos |
|
||||
| Other prompts | 15 | Various proto types |
|
||||
|
||||
**Tier 1 - Core Applier:** ✅ **Complete**
|
||||
```
|
||||
src/main/scala/net/eagle0/eagle/library/actions/applier/ActionResultApplierImpl.scala
|
||||
```
|
||||
`ActionResultApplier` applies `ActionResultT` directly to Scala `GameState`. The legacy `ActionResultTApplierImpl` wraps it and converts to/from proto for callers that still need proto types.
|
||||
**Migration strategy**: These files generate text for LLM prompts. They can be migrated to accept Scala types (`HeroT`, `FactionT`, `ProvinceT`) with converters at the call sites if needed.
|
||||
|
||||
**Tier 2 - RoundPhaseAdvancer:** ✅ **Complete**
|
||||
```
|
||||
src/main/scala/net/eagle0/eagle/library/RoundPhaseAdvancer.scala
|
||||
```
|
||||
Now accepts Scala `GameState` and `ActionResultApplier`. Only converts to proto lazily for `AvailableCommandsFactory` calls.
|
||||
**Priority**: Medium - These don't block other migrations and are isolated.
|
||||
|
||||
**Tier 3 - Sequencers:**
|
||||
```
|
||||
src/main/scala/net/eagle0/eagle/library/actions/impl/common/RandomStateTSequencer.scala
|
||||
src/main/scala/net/eagle0/eagle/library/actions/impl/common/RandomStateProtoSequencer.scala
|
||||
```
|
||||
Modify `RandomStateTSequencer` to thread Scala `GameState` throughout (currently converts to proto internally). Then evaluate whether `RandomStateProtoSequencer` is still needed at all.
|
||||
### Phase 9: Remaining Utilities ✅ Complete
|
||||
|
||||
**Current State**: `RandomStateTSequencer` accepts Scala `GameState` via its `apply()` method but internally converts to proto. All callback methods (`withRandomActionResult`, `withActionResults`, etc.) pass `GameStateProto` to callers, forcing actions that use the sequencer to work with proto types internally.
|
||||
| File | Proto Usage | Migration Path |
|
||||
|------|-------------|----------------|
|
||||
| `ProvinceEventUtils.scala` | ✅ **0** | Deleted unused proto overloads (PR #5342) |
|
||||
| `StatWithConditionUtils.scala` | ✅ **0** | Deleted unused proto overloads (PR #5342) |
|
||||
| `MapGenerator.scala` | ✅ **0** | Returns Scala types (PR #5373) |
|
||||
| `IncomingArmyUtils.scala` | ✅ **0** | Deleted unused `stats()` method (PR #5348) |
|
||||
| `GameStateViewDiffer.scala` | ✅ **0** | Uses Scala diff types, converts at boundary |
|
||||
|
||||
**Target State**: Create a fully protoless sequencer where:
|
||||
1. `lastState` returns Scala `GameState` (not `lastStateProto`)
|
||||
2. All callback methods pass Scala `GameState` to callers
|
||||
3. Actions using the sequencer can be fully protoless
|
||||
|
||||
**Migration Path**:
|
||||
1. Add `lastState: GameState` method alongside `lastStateProto` (non-breaking)
|
||||
2. Add parallel callback methods that pass Scala GameState (e.g., `withScalaActionResult`)
|
||||
3. Migrate actions one by one to use the new Scala-based callbacks
|
||||
4. Once all actions migrated, deprecate/remove proto-based callbacks
|
||||
5. Remove `lastStateProto` once no longer used
|
||||
|
||||
**RandomStateSequencer Migration Progress** (PR #4679 introduced protoless `RandomStateSequencer`):
|
||||
|
||||
| Action | Status |
|
||||
|--------|--------|
|
||||
| `TruceTurnBackPhaseAction` | ✅ Migrated (PR #4680) |
|
||||
| `EndHandleRiotsPhaseAction` | ✅ Migrated (PR #4684) |
|
||||
| `PerformVassalCommandsPhaseAction` | ✅ Migrated |
|
||||
| `PerformVassalDefenseDecisionsAction` | ✅ Migrated |
|
||||
| `EndVassalCommandsPhaseAction` | ✅ Migrated |
|
||||
| `PerformReconResolutionAction` | ✅ Migrated |
|
||||
| `NewRoundAction` | ✅ Migrated (PR #4698) |
|
||||
| `EndBattleAftermathPhaseAction` | ✅ Migrated (PR #4699) |
|
||||
| `EndDiplomacyResolutionPhaseAction` | ✅ Migrated |
|
||||
| `PerformUnaffiliatedHeroesAction` | ✅ Migrated |
|
||||
| `EngineImpl.recursiveTransformT` | ✅ Migrated (PR #4704) |
|
||||
| `ProtolessSequentialResultsActionWrapper` | ✅ Migrated (PR #4705) |
|
||||
| `LegacyRandomStateTSequencer` | ✅ **Deleted** (PR #4705) |
|
||||
|
||||
**TCommandFactory Extraction** (PR #4684):
|
||||
|
||||
To enable lightweight mocking of command creation in tests, `TCommandFactory` trait was extracted from `CommandFactory`. This allows tests to mock just the `makeTCommand` method without pulling in all 40+ command dependencies that `CommandFactory` requires.
|
||||
|
||||
- `TCommandFactory` - lightweight trait with just `makeTCommand`
|
||||
- `CommandFactory extends TCommandFactory` - maintains backward compatibility
|
||||
- Actions accepting command factories now use `TCommandFactory` type for better testability
|
||||
|
||||
**Tier 4 - History APIs:**
|
||||
```
|
||||
src/main/scala/net/eagle0/eagle/service/InMemoryHistory.scala
|
||||
src/main/scala/net/eagle0/eagle/service/PersistedHistory.scala
|
||||
```
|
||||
Change APIs to vend Scala `GameState` and `ActionResultT` instead of proto versions. `PersistedHistory` converts to proto internally for disk persistence; `InMemoryHistory` doesn't need proto at all.
|
||||
|
||||
### ActionResultProto Consumer Inventory
|
||||
|
||||
| File | Usage | Status |
|
||||
|------|-------|--------|
|
||||
| `ActionResultApplierImpl.scala` | Applies ActionResultT to Scala GameState | ✅ **Complete** |
|
||||
| `ActionResultTApplierImpl.scala` | Legacy wrapper - converts to/from proto | Keep until all callers migrated |
|
||||
| `RoundPhaseAdvancer.scala` | Uses Scala GameState | ✅ **Complete** |
|
||||
| `RandomStateSequencer.scala` | Threads Scala GameState | ✅ **Complete** |
|
||||
| `VigorXPApplier.scala` | Has both proto and Scala methods | Scala method exists, delete proto method when unused |
|
||||
| `PerformForcedTurnBackAction.scala` | Fully protoless | ✅ **Complete** |
|
||||
| `ResolveBattleAction.scala` | ✅ Fully protoless | Complete (PR #5048) |
|
||||
| `InMemoryHistory.scala` | Stores proto results | Pending - vend Scala types |
|
||||
| `PersistedHistory.scala` | Stores proto results | Pending - vend Scala types, convert for disk |
|
||||
| `GameController.scala` | Uses proto for client communication | Keep proto (gRPC boundary) |
|
||||
|
||||
### Remaining Proto Usage in Actions
|
||||
|
||||
**Progress: 52 of 52 action files (100%) are fully protoless.** ✅
|
||||
|
||||
All action files have been migrated to use Scala types:
|
||||
|
||||
| Action | Status | Notes |
|
||||
|--------|--------|-------|
|
||||
| `ResolveBattleAction` | ✅ Complete | PR #5048 - uses Scala GameState and ActionResultApplier |
|
||||
| `PerformVassalCommandsPhaseAction` | ✅ Complete | Uses Scala types via CommandChoiceHelpers |
|
||||
| `EndHandleRiotsPhaseAction` | ✅ Complete | Uses Scala types via CommandChoiceHelpers |
|
||||
| `PerformVassalDefenseDecisionsAction` | ✅ Complete | Uses Scala types via CommandChoiceHelpers |
|
||||
| `EndVassalCommandsPhaseAction` | ✅ Complete | Uses Scala types via CommandChoiceHelpers |
|
||||
|
||||
**Note:** `NewRoundAction` is now fully protoless after converting `ChronicleEventGenerator` to return Scala `ChronicleEvent` types directly.
|
||||
|
||||
**Deleted Dead Code:**
|
||||
- `UnaffiliatedHeroMovedAction` - Was never called; `PerformUnaffiliatedHeroesAction.heroMovedResult` constructs `ActionResultC` directly
|
||||
- `HeroBackstoryUpdateActionGenerator.fromGameState` - Dead method that converted proto to Scala; only `apply(GameState)` is used
|
||||
|
||||
**Note**: `PerformReconResolutionAction` and `EndBattleAftermathPhaseAction` are now fully protoless after:
|
||||
1. Migrating `FactionT.reconnedProvinces` and `ChangedFactionC.updatedReconnedProvinces` to use Scala `ProvinceView`
|
||||
2. Adding Scala overload of `ProvinceViewFilter.withdrawnFromProvinceView`
|
||||
|
||||
### Estimated Effort (Remaining)
|
||||
|
||||
| Component | Lines | Complexity | Blocks |
|
||||
|-----------|-------|------------|--------|
|
||||
| History API updates | ~100 | Low | - |
|
||||
| **Total Remaining** | **~100** | | |
|
||||
### Phase 10: History APIs ✅ Complete
|
||||
|
||||
**Completed:**
|
||||
- ✅ `ChronicleEventGenerator` converted to return Scala `ChronicleEvent` types directly
|
||||
- ✅ `CommandChoiceHelpers` migrated to Scala types
|
||||
- ✅ `ResolveBattleAction` refactored to use Scala GameState and ActionResultApplier (PR #5048)
|
||||
- `ActionWithResultingState` now has `scalaActionResult` property (lazy converted from proto)
|
||||
- `ActionResultFilter.includeForPlayer` uses Scala types (`ActionResultT`, `NotificationT`, `ActionResultType`)
|
||||
- `GameHistory.withNewResultsScala` preserves both Scala `GameState` and `ActionResultT` to avoid re-conversion
|
||||
- `PersistedHistory.apply` now accepts `Vector[ActionResultT]` directly (PR #5390)
|
||||
- `NewGameCreation` is fully protoless (PR #5381)
|
||||
|
||||
### Enum Type Migrations
|
||||
|
||||
Proto enums are being converted to Scala sealed traits with converters at boundaries:
|
||||
|
||||
| Enum | Scala Type | Status | Notes |
|
||||
|------|------------|--------|-------|
|
||||
| `DiplomacyOfferStatus` | `Status` sealed trait | ✅ **Complete** | PR #5093 - `EligibleDiplomacyStatuses` uses Scala types internally |
|
||||
| `RoundPhase` | `RoundPhase` sealed trait | Partial | Some usages converted |
|
||||
| `BattalionType` | `BattalionType` sealed trait | Partial | Some usages converted |
|
||||
|
||||
**DiplomacyOfferStatus Migration (PR #5093):**
|
||||
- `EligibleDiplomacyStatuses.scala` now returns `Vector[Status]` instead of `Vector[DiplomacyOfferStatus]`
|
||||
- Call sites in `AvailableResolve*CommandFactory` files convert to proto via `StatusConverter.toProto` at the boundary
|
||||
- This pattern should be applied to other proto enums
|
||||
|
||||
### CommandChoiceHelpers Migration Status
|
||||
|
||||
Several command selectors have already been converted to use Scala types:
|
||||
|
||||
| File | Status | Notes |
|
||||
|------|--------|-------|
|
||||
| `AttackCommandChooser.scala` | ✅ **Protoless** | Uses Scala `GameState`, `HeroT`, `ProvinceT` |
|
||||
| `AlmsCommandSelector.scala` | ✅ **Protoless** | Uses Scala `GameState`, `HeroT`, `ProvinceT` |
|
||||
| `FoodConsumptionUtils.scala` | ✅ **Protoless** | Uses Scala `GameState`, `ProvinceT`, `RoundPhase` |
|
||||
| `MarchSuppliesHelpers.scala` | ✅ **Protoless** | Uses `BattalionT` |
|
||||
| `CombatUnitSelector.scala` | ✅ **Protoless** | Uses `HeroT`, `BattalionT`, `BattalionType` |
|
||||
| `ExpandCommandSelector.scala` | ✅ **Protoless** | Uses Scala `GameState`, `ProvinceT`, `FactionT` |
|
||||
| `ImproveCommandSelector.scala` | ✅ **Protoless** | Uses Scala `GameState`, `ProvinceT`, `HeroT` |
|
||||
| `OrganizeCommandSelector.scala` | ✅ **Protoless** | Uses Scala `GameState`, `BattalionT`, `BattalionType` |
|
||||
| `RansomOfferHelpers.scala` | ✅ **Protoless** | Uses Scala `GameState`, `FactionT` |
|
||||
| `CommandChoiceHelpers.scala` | ✅ **Protoless** | Uses Scala `GameState` throughout |
|
||||
| `ProvinceGoldSurplusCalculator.scala` | ✅ **Protoless** | Uses Scala types |
|
||||
|
||||
**All CommandChoiceHelpers selectors have been migrated to Scala types.** ✅
|
||||
|
||||
### Progress Summary
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Action files fully protoless | 52 / 52 (100%) ✅ |
|
||||
| Proto usages in remaining actions | 0 |
|
||||
| Next target | See "Next Candidates" section below |
|
||||
|
||||
### Next Candidates
|
||||
|
||||
Priority candidates for further deproto work:
|
||||
|
||||
1. **More Enum Migrations** - Apply the `DiplomacyOfferStatus` pattern to other proto enums:
|
||||
- Files importing `net.eagle0.eagle.common.round_phase.RoundPhase` (proto) could use Scala `RoundPhase`
|
||||
- Files importing `net.eagle0.eagle.common.battalion_type.BattalionType` (proto) could use Scala `BattalionType`
|
||||
|
||||
2. **AvailableCommandsFactory Files** - Many still use proto `GameState` internally:
|
||||
- These files build proto `AvailableCommand` messages but could use Scala types for internal logic
|
||||
- Convert to accept Scala `GameState`, only convert fields to proto when building the response
|
||||
|
||||
3. **History APIs** - `InMemoryHistory` and `PersistedHistory`:
|
||||
- Change to vend Scala `GameState` and `ActionResultT` instead of proto versions
|
||||
- `PersistedHistory` converts to proto internally for disk persistence
|
||||
|
||||
### Validation
|
||||
- [x] `ActionResultApplier` created and tested
|
||||
- [x] `RandomStateSequencer` threads Scala GameState throughout
|
||||
- [x] `RoundPhaseAdvancer` uses T-types internally
|
||||
- [x] `ProvinceViewFilter` has Scala overload for server-side use (PR #4752)
|
||||
- [x] `FactionT.reconnedProvinces` and `ChangedFactionC.updatedReconnedProvinces` use Scala `ProvinceView`
|
||||
- [x] `ProvinceViewFilter.withdrawnFromProvinceView` has Scala overload
|
||||
- [x] `CommandChoiceHelpers` uses Scala types ✅
|
||||
- [x] All action files (52/52) are fully protoless ✅
|
||||
- [ ] `ProvinceViewFilter` faction-filtered views use Scala types
|
||||
- [ ] History APIs vend Scala types
|
||||
- [ ] No `ActionResultProtoConverter.toProto()` calls except at persistence/gRPC boundaries
|
||||
- [ ] All tests pass
|
||||
**Note:** History internals still use proto for persistence (expected - proto is good for disk serialization).
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: Clean Up Legacy Utilities
|
||||
## Architecture Summary
|
||||
|
||||
### Objective
|
||||
Remove remaining direct proto imports from utility classes.
|
||||
### Fully Protoless Areas ✅
|
||||
|
||||
### Files to Modify
|
||||
- **AI layer** (`/ai/`) - All AI clients and command choosers
|
||||
- **Actions** (`/library/actions/impl/action/`) - All 52 action files
|
||||
- **Commands** (`/library/actions/impl/command/`) - CommandFactory and all commands
|
||||
- **Availability** (`/library/actions/availability/`) - AvailableCommandsFactory and all factories
|
||||
- **Command helpers** (`/library/util/command_choice_helpers/`) - All selectors and choosers
|
||||
- **Core types** - `CommandSelection`, `SelectedCommand`, `AvailableCommand`, `OneProvinceAvailableCommands`
|
||||
- **View types** - `FactionView`, `HeroView`, `ProvinceView` (return Scala, convert at edge)
|
||||
|
||||
| File | Status |
|
||||
|------|--------|
|
||||
| `CommandChoiceHelpers.scala` | Accepts proto `GameState`; blocks full deproto of `PerformVassalCommandsPhaseAction` and `PerformVassalDefenseDecisionsAction` |
|
||||
| `LegacyProvinceUtils.scala` | Replace with `ProvinceUtils.scala` - `hasImminentRiot` added (PR #4683) |
|
||||
| `LegacyFactionUtils.scala` | Replace proto imports with `FactionT` |
|
||||
| `LegacyUnaffiliatedHeroUtils.scala` | Replace proto imports with Scala models |
|
||||
| `BattalionTypeLoader.scala` | Keep proto for file loading, convert immediately after |
|
||||
| `BeastUtils.scala` | **Complete** - now uses Scala `BeastInfo` only |
|
||||
### Proto at Boundaries (Expected) ✅
|
||||
|
||||
### View Filters (Partially Complete)
|
||||
|
||||
The view filter utilities now have Scala overloads for server-side use:
|
||||
|
||||
| File | Status | Notes |
|
||||
|------|--------|-------|
|
||||
| `ProvinceViewFilter.scala` | **Partial** | `filteredProvinceView(ProvinceT, ScalaGameState)` added (PR #4752) |
|
||||
| `ArmyFilter.scala` | **Partial** | `filterArmy(ScalaArmy, Map[BattalionId, BattalionT], Option[FactionId])` added |
|
||||
| `BattalionViewFilter.scala` | **Complete** | Uses Scala `BattalionT` throughout |
|
||||
| `GameStateViewFilter.scala` | Pending | Uses proto types throughout |
|
||||
| `GameStateViewDiffer.scala` | Pending | Works with view protos |
|
||||
|
||||
**Unblocked Actions** (PR #4752):
|
||||
- `EndBattleAftermathPhaseAction` - can now use `filteredProvinceView(province, scalaGameState)`
|
||||
- `PerformReconResolutionAction` - can now use Scala overload
|
||||
- `GameStateFactionExtensions` - can now use `updatedReconnedProvinces` with Scala types
|
||||
|
||||
**Remaining Work**:
|
||||
- Faction-filtered `filteredProvinceView(Province, GameState, FactionId)` still uses proto types
|
||||
- `withdrawnFromProvinceView` still uses proto types
|
||||
- These are needed for client-facing views with visibility restrictions
|
||||
|
||||
---
|
||||
|
||||
## Phase 8: Verify Boundaries
|
||||
|
||||
### Objective
|
||||
Confirm protos are used correctly at boundaries — and ONLY there.
|
||||
|
||||
### Expected Proto Usage (Keep)
|
||||
- `EagleServiceImpl.scala` - gRPC boundary
|
||||
- `InMemoryHistory.scala` / `PersistedHistory.scala` - Persistence boundary
|
||||
- `GameController.scala` - Client communication
|
||||
- `GameStateViewFilter.scala` - Converts Scala views to proto for client
|
||||
- `*Converter.scala` - Explicit conversion utilities
|
||||
- `*Loader.scala` - File loading utilities
|
||||
- `PersistedHistory.scala` - Disk persistence
|
||||
|
||||
### Expected No Proto Usage (Verify)
|
||||
- `/library/actions/impl/` - Pure Scala models
|
||||
- `/library/util/` - Pure Scala models (except loaders)
|
||||
- `/model/state/` - Pure Scala models
|
||||
### Remaining Proto Usage ⏳
|
||||
|
||||
- LLM prompt generators (56 imports in 34 files) - Medium priority
|
||||
- LLM request generators (2 imports in 1 file) - Medium priority
|
||||
- BattleFilter (1 import) - Keep (boundary)
|
||||
- Root library boundary code (7 imports in 4 files) - Keep (boundary)
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
## Estimated Remaining Effort
|
||||
|
||||
1. **Persistence Format**: Currently game state is persisted as proto. Should we keep proto for persistence (good for schema evolution) or switch to a different format?
|
||||
|
||||
2. **Shardok Integration**: `ResolveBattleAction` communicates with Shardok. Should the Shardok interface use protos (external service) or Scala models?
|
||||
|
||||
3. **View Generation**: `GameStateViewDiffer` works with view protos for client updates. Views need Scala models (`ProvinceViewT`, etc.) to allow actions like `EndBattleAftermathPhaseAction` to be fully protoless. The Scala views would be converted to proto only at the gRPC boundary when sending updates to clients.
|
||||
| Component | Files | Imports | Priority |
|
||||
|-----------|-------|---------|----------|
|
||||
| View Filters | 0 | 0 | ✅ Complete |
|
||||
| Utility files | 0 | 0 | ✅ Complete |
|
||||
| History APIs | 0 | 0 | ✅ Complete |
|
||||
| LLM Prompt Generators | 34 | 56 | Medium |
|
||||
| LLM Request Generators | 1 | 2 | Medium |
|
||||
| Root Library (boundary) | 4 | 7 | Keep |
|
||||
| **Total** | **~35** | **~65** | |
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
### Code Quality
|
||||
- [ ] Zero proto imports in `/library/actions/` (except boundaries)
|
||||
- [ ] Zero proto imports in `/library/` utilities (except loaders)
|
||||
- [ ] `GameStateT` used throughout engine internals
|
||||
- [ ] Proto usage limited to: `EagleServiceImpl`, loaders, converters, persistence
|
||||
- [x] Zero proto imports in `/ai/`
|
||||
- [x] Zero proto imports in `/library/actions/impl/command/`
|
||||
- [x] Zero proto imports in `/library/actions/availability/`
|
||||
- [x] Zero proto imports in `/library/util/command_choice_helpers/`
|
||||
- [x] Zero proto imports in `/library/actions/impl/action/`
|
||||
- [x] Zero proto imports in `/library/util/view_filters/` (except BattleFilter boundary code)
|
||||
- [x] Zero proto imports in `/library/util/` (except view filters)
|
||||
- [x] Zero proto imports in `/service/new_game_creation/` (except input GameParameters)
|
||||
- [ ] Zero proto imports in `/library/actions/llm_prompt_generators/`
|
||||
- [ ] Zero proto imports in `/library/actions/llm_request_generators/`
|
||||
|
||||
### Architecture
|
||||
- [ ] Clear separation: Scala models (internal) vs Proto (boundaries)
|
||||
- [ ] Converters as the only bridge between domains
|
||||
- [x] Clear separation: Scala models (internal) vs Proto (boundaries)
|
||||
- [x] Converters as the only bridge between domains
|
||||
- [x] CommandFactory accepts/returns Scala types
|
||||
- [x] AI layer fully protoless
|
||||
- [x] FactionViewFilter returns Scala types
|
||||
- [x] HeroViewFilter returns Scala types
|
||||
- [x] ProvinceViewFilter returns Scala types
|
||||
- [x] All view filters return Scala types
|
||||
- [x] NewGameCreation uses Scala types
|
||||
- [x] PersistedHistory accepts Scala types for new games
|
||||
- [ ] LLM layer uses Scala types
|
||||
- [ ] No "proto creep" into business logic
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **LLM Prompt Generators**: Should these accept Scala types directly, or is it acceptable to have proto usage here since they're generating text (not core game logic)?
|
||||
|
||||
2. ~~**GameStateViewDiffer**: This works with view protos for client updates. Should it remain proto-based since it's generating client-facing data?~~ **Resolved**: GameStateViewDiffer now uses Scala types internally (`GameStateViewDiff`, `ProvinceViewDiff`, etc.) and converts to proto at the boundary in `ActionResultFilter`.
|
||||
|
||||
3. **History Serialization**: Keep proto for persistence (good for schema evolution) or consider alternatives?
|
||||
|
||||
---
|
||||
|
||||
## Proto Import Inventory (Detailed)
|
||||
|
||||
**Current: ~65 proto imports across ~35 files** (as of 2026-01-17)
|
||||
|
||||
### Summary by Directory
|
||||
|
||||
| Directory | Files | Imports | Status |
|
||||
|-----------|-------|---------|--------|
|
||||
| `ai/` | 0 | 0 | ✅ Clean |
|
||||
| `actions/availability/` | 0 | 0 | ✅ Clean |
|
||||
| `actions/impl/action/` | 0 | 0 | ✅ Clean |
|
||||
| `actions/impl/command/` | 0 | 0 | ✅ Clean |
|
||||
| `actions/llm_prompt_generators/` | 34 | 56 | LLM request types |
|
||||
| `actions/llm_request_generators/` | 1 | 2 | LLM request types |
|
||||
| `util/command_choice_helpers/` | 0 | 0 | ✅ Clean |
|
||||
| `util/view_filters/` | 0 | 0 | ✅ Clean (except boundary) |
|
||||
| `util/` (other) | 0 | 0 | ✅ Clean |
|
||||
| Root (`library/`) | 4 | 7 | Boundary code |
|
||||
|
||||
### util/view_filters/ (0 imports, except boundary)
|
||||
|
||||
| File | Import | Notes |
|
||||
|------|--------|-------|
|
||||
| `GameStateViewFilter.scala` | ✅ **0** | Returns Scala `GameStateView`, converts at edge |
|
||||
| `GameStateViewDiffer.scala` | ✅ **0** | Uses Scala diff types, converts at edge |
|
||||
| `BattleFilter.scala` | 1 | Battle view boundary (keep proto) |
|
||||
|
||||
### util/ other files ✅ Complete
|
||||
|
||||
| File | Imports | Notes |
|
||||
|------|---------|-------|
|
||||
| `MapGenerator.scala` | ✅ **0** | Returns Scala types (PR #5373) |
|
||||
|
||||
### Root library/ (7 imports, 4 files)
|
||||
|
||||
| File | Imports | Notes |
|
||||
|------|---------|-------|
|
||||
| `ActionResultFilter.scala` | 3 | Action result filtering (boundary, uses Scala types internally) |
|
||||
| `Engine.scala` | 1 | Trait interface (returns `ActionResultView` proto for client) |
|
||||
| `EngineImpl.scala` | 1 | Returns proto for persistence (boundary) |
|
||||
| `ActionWithResultingState.scala` | 2 | Stores both proto and Scala (boundary for persistence) |
|
||||
|
||||
### actions/llm_prompt_generators/ (56 imports, 34 files)
|
||||
|
||||
These files generate LLM prompts and primarily use `internal.generated_text_request.*` types plus some common enums like `DiplomacyOfferStatus` and `BattalionTypeId`.
|
||||
|
||||
**Migration strategy**: Can be migrated to Scala types when convenient, but low priority as they're isolated from core game logic.
|
||||
|
||||
### actions/llm_request_generators/ (2 imports, 1 file)
|
||||
|
||||
| File | Imports | Notes |
|
||||
|------|---------|-------|
|
||||
| `DiplomacyResolutionLlmRequestGenerator.scala` | 2 | Uses `DiplomacyOfferStatus` enum and `GeneratedTextRequest` |
|
||||
|
||||
**Migration strategy**: Create Scala `DiplomacyOfferStatus` enum and `GeneratedTextRequest` types.
|
||||
|
||||
---
|
||||
|
||||
## Migration Pattern
|
||||
|
||||
The codebase follows a **Legacy* pattern** for separating proto-dependent and protoless code:
|
||||
|
||||
- **Protoless utilities**: `FactionUtils`, `HeroUtils`, `ProvinceUtils`, `ProvinceDistances`, etc.
|
||||
- **Proto-dependent utilities**: `LegacyFactionUtils`, `LegacyHeroUtils`, `LegacyProvinceUtils`, etc.
|
||||
|
||||
When migrating a file:
|
||||
1. Create a `Legacy*` version containing the proto-dependent methods
|
||||
2. Keep the original file name for protoless methods
|
||||
3. Update callers to use the appropriate version based on their context
|
||||
4. Delete `Legacy*` files when no longer needed
|
||||
|
||||
### Deleted Legacy Files (no production callers)
|
||||
- `LegacyProvinceDistances`
|
||||
- `LegacyBattalionSuitability`
|
||||
- `LegacyFoodConsumptionUtils`
|
||||
- `LegacyHandleRiotUtils`
|
||||
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
load("@build_bazel_rules_apple//apple:apple.bzl", "apple_dynamic_framework_import")
|
||||
|
||||
# Import pre-built Sparkle framework
|
||||
apple_dynamic_framework_import(
|
||||
name = "Sparkle",
|
||||
framework_imports = glob(["Sparkle.framework/**"]),
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
@@ -112,6 +112,20 @@ http {
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
# Apple OAuth callback (Apple uses POST with form_post response mode)
|
||||
location /oauth/apple/callback {
|
||||
proxy_pass http://auth:8080;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
# Steam OAuth callback (Steam uses OpenID 2.0)
|
||||
location /oauth/steam/callback {
|
||||
proxy_pass http://auth:8080;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
# Invitation landing page (proxied to Go auth service)
|
||||
location /invite/ {
|
||||
proxy_pass http://auth:8080;
|
||||
|
||||
@@ -8,3 +8,6 @@ ZIP_LOCATION=$(bazel cquery --config=mactools --output=files @net_eagle0_unity_g
|
||||
/usr/bin/unzip -o $ZIP_LOCATION -d src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Plugins/
|
||||
|
||||
/usr/bin/plutil -convert xml1 src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Plugins/DarwinGodiceBundle.bundle/Contents/Info.plist
|
||||
|
||||
/bin/echo "building sparkle plugin"
|
||||
./scripts/build_sparkle_plugin.sh
|
||||
|
||||
Executable
+30
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Build the SparklePlugin native library for Unity using Bazel
|
||||
#
|
||||
# Usage: build_sparkle_plugin.sh [output_dir]
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
OUTPUT_DIR="${1:-$PROJECT_ROOT/src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Plugins/macOS}"
|
||||
|
||||
echo "=== Building SparklePlugin with Bazel ==="
|
||||
|
||||
bazel build --config=mactools //src/main/objc/net/eagle0/sparkle:SparklePlugin
|
||||
|
||||
# Get the zip path from bazel
|
||||
ZIP_PATH=$(bazel cquery --config=mactools --output=files //src/main/objc/net/eagle0/sparkle:SparklePlugin 2>/dev/null)
|
||||
|
||||
echo "=== Extracting SparklePlugin.bundle ==="
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
rm -rf "$OUTPUT_DIR/SparklePlugin.bundle"
|
||||
unzip -o "$ZIP_PATH" -d "$OUTPUT_DIR/"
|
||||
|
||||
# Convert Info.plist from binary to XML format (Unity requires XML)
|
||||
/usr/bin/plutil -convert xml1 "$OUTPUT_DIR/SparklePlugin.bundle/Contents/Info.plist"
|
||||
|
||||
echo "=== SparklePlugin built successfully ==="
|
||||
ls -la "$OUTPUT_DIR/SparklePlugin.bundle/"
|
||||
@@ -39,32 +39,51 @@ find "$APP_PATH" -name "*.bundle" -print0 | while IFS= read -r -d '' item; do
|
||||
--sign "$SIGNING_IDENTITY" "$item"
|
||||
done
|
||||
|
||||
# Sign XPC services (inside Sparkle framework)
|
||||
# Sign XPC services (but skip ones inside Sparkle.framework - they're already signed)
|
||||
find "$APP_PATH" -name "*.xpc" -print0 | while IFS= read -r -d '' item; do
|
||||
if [[ "$item" == *"Sparkle.framework"* ]]; then
|
||||
echo "Skipping Sparkle XPC service (pre-signed): $item"
|
||||
continue
|
||||
fi
|
||||
echo "Signing XPC service: $item"
|
||||
codesign --force --verify --verbose --timestamp --options runtime \
|
||||
--sign "$SIGNING_IDENTITY" "$item"
|
||||
done
|
||||
|
||||
# Sign nested apps (like Sparkle's Updater.app)
|
||||
# Sign nested apps (but skip ones inside Sparkle.framework - they're already signed)
|
||||
find "$APP_PATH" -path "*/Frameworks/*.app" -print0 | while IFS= read -r -d '' item; do
|
||||
if [[ "$item" == *"Sparkle.framework"* ]]; then
|
||||
echo "Skipping Sparkle nested app (pre-signed): $item"
|
||||
continue
|
||||
fi
|
||||
echo "Signing nested app: $item"
|
||||
codesign --force --verify --verbose --timestamp --options runtime \
|
||||
--sign "$SIGNING_IDENTITY" "$item"
|
||||
done
|
||||
|
||||
# Sign standalone executables inside frameworks (like Autoupdate)
|
||||
# Sign standalone executables inside frameworks (but skip Sparkle.framework internals)
|
||||
find "$APP_PATH" -path "*/Frameworks/*/Versions/*/Autoupdate" -type f -print0 | while IFS= read -r -d '' item; do
|
||||
if [[ "$item" == *"Sparkle.framework"* ]]; then
|
||||
echo "Skipping Sparkle executable (pre-signed): $item"
|
||||
continue
|
||||
fi
|
||||
echo "Signing executable: $item"
|
||||
codesign --force --verify --verbose --timestamp --options runtime \
|
||||
--sign "$SIGNING_IDENTITY" "$item"
|
||||
done
|
||||
|
||||
# Sign all frameworks (after their contents are signed)
|
||||
# Use --deep for Sparkle.framework to handle its XPC services
|
||||
find "$APP_PATH" -name "*.framework" -print0 | while IFS= read -r -d '' item; do
|
||||
echo "Signing framework: $item"
|
||||
codesign --force --verify --verbose --timestamp --options runtime \
|
||||
--sign "$SIGNING_IDENTITY" "$item"
|
||||
if [[ "$item" == *"Sparkle.framework" ]]; then
|
||||
echo "Signing Sparkle framework with --deep: $item"
|
||||
codesign --deep --force --verify --verbose --timestamp --options runtime \
|
||||
--sign "$SIGNING_IDENTITY" "$item"
|
||||
else
|
||||
echo "Signing framework: $item"
|
||||
codesign --force --verify --verbose --timestamp --options runtime \
|
||||
--sign "$SIGNING_IDENTITY" "$item"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "=== Signing main app bundle ==="
|
||||
|
||||
+46
-19
@@ -27,33 +27,59 @@ if [ -z "${SPARKLE_EDDSA_PUBLIC_KEY:-}" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Download Sparkle if not cached
|
||||
# Always use a fresh Sparkle download to avoid cache corruption issues
|
||||
SPARKLE_DIR="$SPARKLE_CACHE_DIR/Sparkle-$SPARKLE_VERSION"
|
||||
if [ ! -d "$SPARKLE_DIR/Sparkle.framework" ]; then
|
||||
echo "=== Downloading Sparkle $SPARKLE_VERSION ==="
|
||||
mkdir -p "$SPARKLE_CACHE_DIR"
|
||||
SPARKLE_URL="https://github.com/sparkle-project/Sparkle/releases/download/${SPARKLE_VERSION}/Sparkle-${SPARKLE_VERSION}.tar.xz"
|
||||
curl -L "$SPARKLE_URL" | tar -xJ -C "$SPARKLE_CACHE_DIR"
|
||||
mv "$SPARKLE_CACHE_DIR/Sparkle-$SPARKLE_VERSION" "$SPARKLE_DIR" 2>/dev/null || true
|
||||
# If the extracted directory doesn't match version pattern, it may just be "Sparkle"
|
||||
if [ ! -d "$SPARKLE_DIR" ]; then
|
||||
mkdir -p "$SPARKLE_DIR"
|
||||
mv "$SPARKLE_CACHE_DIR/Sparkle.framework" "$SPARKLE_DIR/" 2>/dev/null || true
|
||||
mv "$SPARKLE_CACHE_DIR/bin" "$SPARKLE_DIR/" 2>/dev/null || true
|
||||
fi
|
||||
echo "=== Clearing Sparkle cache and downloading fresh copy ==="
|
||||
rm -rf "$SPARKLE_DIR"
|
||||
mkdir -p "$SPARKLE_DIR"
|
||||
|
||||
SPARKLE_URL="https://github.com/sparkle-project/Sparkle/releases/download/${SPARKLE_VERSION}/Sparkle-${SPARKLE_VERSION}.tar.xz"
|
||||
echo "Downloading from: $SPARKLE_URL"
|
||||
curl -L "$SPARKLE_URL" -o /tmp/sparkle.tar.xz
|
||||
tar -xJf /tmp/sparkle.tar.xz -C "$SPARKLE_DIR"
|
||||
rm /tmp/sparkle.tar.xz
|
||||
|
||||
# Show what was extracted
|
||||
echo "=== Extracted contents ==="
|
||||
ls -la "$SPARKLE_DIR/"
|
||||
|
||||
# The tarball extracts files directly, not into a subdirectory
|
||||
# Verify the framework has proper symlink structure
|
||||
echo "=== Verifying Sparkle.framework structure ==="
|
||||
ls -la "$SPARKLE_DIR/Sparkle.framework/"
|
||||
|
||||
if [ ! -L "$SPARKLE_DIR/Sparkle.framework/Sparkle" ]; then
|
||||
echo "ERROR: Sparkle.framework/Sparkle is not a symlink"
|
||||
file "$SPARKLE_DIR/Sparkle.framework/Sparkle"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -L "$SPARKLE_DIR/Sparkle.framework/Versions/Current" ]; then
|
||||
echo "ERROR: Sparkle.framework/Versions/Current is not a symlink"
|
||||
ls -la "$SPARKLE_DIR/Sparkle.framework/Versions/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Sparkle framework structure verified OK"
|
||||
|
||||
echo "=== Injecting Sparkle framework ==="
|
||||
FRAMEWORKS_DIR="$APP_PATH/Contents/Frameworks"
|
||||
mkdir -p "$FRAMEWORKS_DIR"
|
||||
|
||||
# Copy Sparkle framework
|
||||
cp -R "$SPARKLE_DIR/Sparkle.framework" "$FRAMEWORKS_DIR/"
|
||||
# Remove any existing Sparkle.framework in the app
|
||||
rm -rf "$FRAMEWORKS_DIR/Sparkle.framework"
|
||||
|
||||
# Also copy the XPC services if present
|
||||
if [ -d "$SPARKLE_DIR/Sparkle.framework/Versions/B/XPCServices" ]; then
|
||||
echo "Sparkle XPC services present"
|
||||
# Copy Sparkle framework (use ditto to preserve symlinks and bundle structure)
|
||||
ditto "$SPARKLE_DIR/Sparkle.framework" "$FRAMEWORKS_DIR/Sparkle.framework"
|
||||
|
||||
# Verify the copied framework still has proper structure
|
||||
echo "=== Verifying copied Sparkle.framework structure ==="
|
||||
ls -la "$FRAMEWORKS_DIR/Sparkle.framework/"
|
||||
if [ ! -L "$FRAMEWORKS_DIR/Sparkle.framework/Sparkle" ]; then
|
||||
echo "ERROR: Copied framework lost symlink structure"
|
||||
exit 1
|
||||
fi
|
||||
echo "Copied framework structure OK"
|
||||
|
||||
echo "=== Updating Info.plist ==="
|
||||
PLIST_PATH="$APP_PATH/Contents/Info.plist"
|
||||
@@ -69,8 +95,9 @@ PLIST_PATH="$APP_PATH/Contents/Info.plist"
|
||||
/usr/libexec/PlistBuddy -c "Add :SUEnableAutomaticChecks bool true" "$PLIST_PATH"
|
||||
|
||||
# Set bundle version from git for Sparkle version comparison
|
||||
VERSION=$(git describe --tags --always 2>/dev/null || echo "1.0.0")
|
||||
# Use commit count for automatic incrementing versions (e.g., 1.0.9548)
|
||||
BUILD_NUMBER=$(git rev-list --count HEAD 2>/dev/null || echo "1")
|
||||
VERSION="1.0.${BUILD_NUMBER}"
|
||||
|
||||
echo "Setting version: $VERSION (build $BUILD_NUMBER)"
|
||||
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $VERSION" "$PLIST_PATH" 2>/dev/null || \
|
||||
|
||||
@@ -87,7 +87,6 @@
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Demo/LaunchURL.cs" />
|
||||
<Compile Include="Assets/Shardok/HexCoordinates.cs" />
|
||||
<Compile Include="Assets/Eagle/Table Rows/OrganizeTroopsTableRow.cs" />
|
||||
<Compile Include="Assets/Bluetooth/PickerRowController.cs" />
|
||||
<Compile Include="Assets/TouchHandler.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/TravelCommandSelector.cs" />
|
||||
<Compile Include="Assets/Eagle/ConnectionStatusUI.cs" />
|
||||
@@ -98,7 +97,6 @@
|
||||
<Compile Include="Assets/HoveringTooltipTextProvider.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/util/KeyModifiedAmount.cs" />
|
||||
<Compile Include="Assets/HoveringTooltip.cs" />
|
||||
<Compile Include="Assets/Bluetooth/DiceInterface.cs" />
|
||||
<Compile Include="Assets/ButtonColors.cs" />
|
||||
<Compile Include="Assets/Eagle/MapController.cs" />
|
||||
<Compile Include="Assets/common/DisclosureTriangle.cs" />
|
||||
@@ -151,10 +149,8 @@
|
||||
<Compile Include="Assets/Eagle/GeneratedTextUpdater.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/OutlawApprehendedDetailsNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Shardok/HexMetrics.cs" />
|
||||
<Compile Include="Assets/Bluetooth/OneDiceRollController.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Switch/SwitchManager.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/ProvinceHeldDetailsNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Bluetooth/DieInfo.cs" />
|
||||
<Compile Include="Assets/common/ResourceFetcher.cs" />
|
||||
<Compile Include="Assets/Tutorial/Content/TutorialStep.cs" />
|
||||
<Compile Include="Assets/Auth/AuthClient.cs" />
|
||||
@@ -162,12 +158,11 @@
|
||||
<Compile Include="Assets/Eagle/Notifications/RiotSuppressedNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Modal Window/ModalWindowManagerEditor.cs" />
|
||||
<Compile Include="Assets/common/GUIUtils/AutoScrollingText.cs" />
|
||||
<Compile Include="Assets/Bluetooth/NativeDiceInterfaceImports.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/ProvinceConqueredNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Eagle/Table Rows/ExtraTroopsRowController.cs" />
|
||||
<Compile Include="Assets/Auth/JwtAuthInterceptor.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandPanelController.cs" />
|
||||
<Compile Include="Assets/Tutorial/Content/TutorialContentDefinitions.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandPanelController.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/ShatteredArmyDetailsNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Shardok/TurnHistoryPanelController.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/HeroDepartureDetailsNotificationGenerator.cs" />
|
||||
@@ -212,7 +207,6 @@
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/ExileVassalCommandSelector.cs" />
|
||||
<Compile Include="Assets/Tutorial/TutorialState.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Button/ButtonManagerBasicWithIconEditor.cs" />
|
||||
<Compile Include="Assets/Bluetooth/DiceVectors.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/UI Manager/UIManagerProgressBarLoop.cs" />
|
||||
<Compile Include="Assets/Eagle/MovingArmiesTableController.cs" />
|
||||
<Compile Include="Assets/ConnectionHandler/ConnectionHandler.cs" />
|
||||
@@ -233,9 +227,11 @@
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/DiplomacyCommandSelector.cs" />
|
||||
<Compile Include="Assets/Tutorial/Triggers/TutorialTriggerRegistry.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/ResolveBreakAllianceCommandSelector.cs" />
|
||||
<Compile Include="Assets/common/WindowFocusManager.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/ResolveAllianceCommandSelector.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/WithdrewForTruceDetailsNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/MarchCommandSelector.cs" />
|
||||
<Compile Include="Assets/Tutorial/TutorialTargetRegistry.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/TrainCommandSelector.cs" />
|
||||
<Compile Include="Assets/Eagle/CustomFileLogger.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/InvitationAcceptedDetailsNotificationGenerator.cs" />
|
||||
@@ -269,7 +265,6 @@
|
||||
<Compile Include="Assets/Shardok/AnimationTestController.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/TradeCommandSelector.cs" />
|
||||
<Compile Include="Assets/Eagle/Table Rows/DominionTableRowController.cs" />
|
||||
<Compile Include="Assets/Bluetooth/RollPanelController.cs" />
|
||||
<Compile Include="Assets/Shardok/ChargeAnimator.cs" />
|
||||
<Compile Include="Assets/Eagle/ClientTextProvider.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/UI Manager/UIManager.cs" />
|
||||
@@ -304,6 +299,7 @@
|
||||
<Compile Include="Assets/Shardok/ControlAnimator.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Horizontal Selector/HorizontalSelector.cs" />
|
||||
<Compile Include="Assets/Eagle/Table Rows/OrganizeTroopsResultRow.cs" />
|
||||
<Compile Include="Assets/Eagle/SparkleUpdater.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Slider/SliderManagerEditor.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/SwearBrotherhoodDetailsNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Eagle/PersistentClientConnection.cs" />
|
||||
@@ -322,10 +318,10 @@
|
||||
<Compile Include="Assets/Shardok/ShardokGameModel.cs" />
|
||||
<Compile Include="Assets/common/GUIUtils/GeneralClickDetector.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/UI Manager/UIManagerContextMenu.cs" />
|
||||
<Compile Include="Assets/Eagle/SparkleInitializer.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/TruceAmbassadorImprisonedDetailsNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Button/ButtonManagerBasicWithIcon.cs" />
|
||||
<Compile Include="Assets/Tutorial/UI/TutorialHintIndicator.cs" />
|
||||
<Compile Include="Assets/Bluetooth/UnityDieColors.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/OrganizeTroopsCommandSelector.cs" />
|
||||
<Compile Include="Assets/Shardok/ExtinguishAnimator.cs" />
|
||||
<Compile Include="Assets/Eagle/PanelPositions.cs" />
|
||||
@@ -338,7 +334,6 @@
|
||||
<Compile Include="Assets/EagleConnection.cs" />
|
||||
<Compile Include="Assets/GUI Pro Kit Fantasy RPG/Scripts/CtrPanel.cs" />
|
||||
<Compile Include="Assets/UI/Scripts/SceneLoadTester.cs" />
|
||||
<Compile Include="Assets/Bluetooth/RollFetcher.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/HandleRiotGiveCommandSelector.cs" />
|
||||
<Compile Include="Assets/Eagle/ProvinceUtils.cs" />
|
||||
<Compile Include="Assets/common/GUIUtils/ProvinceStatUtils.cs" />
|
||||
@@ -372,16 +367,15 @@
|
||||
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/SuppressBeastsFailedDetailsNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Terrain Hexes/Example Scene/BasicHexArranger.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/ManagePrisonersCommandSelector.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/SendSuppliesCommandSelector.cs" />
|
||||
<Compile Include="Assets/ConnectionHandler/StoredAccountButton.cs" />
|
||||
<Compile Include="Assets/Tutorial/UI/TutorialUIManager.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/SendSuppliesCommandSelector.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Window/WindowManager.cs" />
|
||||
<Compile Include="Assets/Auth/InvitationCodeManager.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/CapturedHeroExiledDetailsNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/SuppressBeastsSucceededNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Shardok/ReservesTableController.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/CommandSelector.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/RestCommandSelector.cs" />
|
||||
<Compile Include="Assets/Bluetooth/DiceConfigurationPanelController.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/UI Manager/UIManagerDropdown.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/UI Manager/UIManagerButton.cs" />
|
||||
<Compile Include="Assets/common/GUIUtils/GUIUtils.cs" />
|
||||
|
||||
@@ -57,20 +57,10 @@ namespace Auth {
|
||||
/// Returns both the URL and the state token for polling.
|
||||
/// Routed to Go auth service.
|
||||
/// </summary>
|
||||
/// <param name="provider">Discord or Google</param>
|
||||
/// <param name="invitationCode">Optional invitation code for new account
|
||||
/// registration</param> <returns>Tuple of (URL to open in browser, state token for
|
||||
/// polling)</returns>
|
||||
public async Task<(string authUrl, string state)> GetOAuthUrlAsync(
|
||||
OAuthProvider provider,
|
||||
string invitationCode = null) {
|
||||
/// <param name="provider">OAuth provider (Discord, Google, GitHub, Apple)</param>
|
||||
/// <returns>Tuple of (URL to open in browser, state token for polling)</returns>
|
||||
public async Task<(string authUrl, string state)> GetOAuthUrlAsync(OAuthProvider provider) {
|
||||
var request = new GetOAuthUrlRequest { Provider = provider };
|
||||
|
||||
if (!string.IsNullOrEmpty(invitationCode)) {
|
||||
request.InvitationCode = invitationCode;
|
||||
Debug.Log("[AuthClient] Including invitation code in OAuth request");
|
||||
}
|
||||
|
||||
var response = await _authServiceClient.GetOAuthUrlAsync(request);
|
||||
|
||||
Debug.Log($"[AuthClient] Got OAuth URL for {provider}, state={response.State}");
|
||||
@@ -103,10 +93,6 @@ namespace Auth {
|
||||
case OAuthStatus.Expired:
|
||||
throw new Exception("OAuth session expired. Please try again.");
|
||||
|
||||
case OAuthStatus.InvitationRequired:
|
||||
Debug.Log("[AuthClient] Server requires invitation code for new account");
|
||||
return response; // Return so OAuthManager can handle this
|
||||
|
||||
case OAuthStatus.Pending:
|
||||
// Check timeout
|
||||
if ((DateTime.UtcNow - startTime).TotalMilliseconds > PollTimeoutMs) {
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Auth {
|
||||
/// <summary>
|
||||
/// Manages invitation codes for new account registration.
|
||||
/// Reads codes from installer-provided file or allows manual entry.
|
||||
/// </summary>
|
||||
public static class InvitationCodeManager {
|
||||
private const string InvitationFileName = "invitation.json";
|
||||
private const string PlayerPrefsKey = "InvitationCode";
|
||||
|
||||
// Cache the code to avoid repeated file reads
|
||||
private static string _cachedCode;
|
||||
private static bool _cacheInitialized;
|
||||
|
||||
/// <summary>
|
||||
/// Get the invitation code, if available.
|
||||
/// Checks: 1) Cached value, 2) PlayerPrefs, 3) File from installer
|
||||
/// </summary>
|
||||
public static string GetInvitationCode() {
|
||||
if (_cacheInitialized) { return _cachedCode; }
|
||||
|
||||
// Check PlayerPrefs first (manual entry takes priority)
|
||||
string prefsCode = PlayerPrefs.GetString(PlayerPrefsKey, null);
|
||||
if (!string.IsNullOrEmpty(prefsCode)) {
|
||||
_cachedCode = prefsCode;
|
||||
_cacheInitialized = true;
|
||||
Debug.Log("[InvitationCodeManager] Using invitation code from PlayerPrefs");
|
||||
return _cachedCode;
|
||||
}
|
||||
|
||||
// Try to read from installer file
|
||||
string fileCode = ReadFromFile();
|
||||
if (!string.IsNullOrEmpty(fileCode)) {
|
||||
_cachedCode = fileCode;
|
||||
_cacheInitialized = true;
|
||||
Debug.Log("[InvitationCodeManager] Using invitation code from installer file");
|
||||
return _cachedCode;
|
||||
}
|
||||
|
||||
_cacheInitialized = true;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set invitation code manually (e.g., from UI input).
|
||||
/// </summary>
|
||||
public static void SetInvitationCode(string code) {
|
||||
if (string.IsNullOrEmpty(code)) {
|
||||
PlayerPrefs.DeleteKey(PlayerPrefsKey);
|
||||
_cachedCode = null;
|
||||
} else {
|
||||
PlayerPrefs.SetString(PlayerPrefsKey, code);
|
||||
_cachedCode = code;
|
||||
}
|
||||
_cacheInitialized = true;
|
||||
PlayerPrefs.Save();
|
||||
Debug.Log(
|
||||
$"[InvitationCodeManager] Invitation code {(string.IsNullOrEmpty(code) ? "cleared" : "set")}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear the invitation code after successful account creation.
|
||||
/// </summary>
|
||||
public static void ClearInvitationCode() {
|
||||
PlayerPrefs.DeleteKey(PlayerPrefsKey);
|
||||
_cachedCode = null;
|
||||
_cacheInitialized = true;
|
||||
PlayerPrefs.Save();
|
||||
|
||||
// Also delete the installer file if it exists
|
||||
DeleteInstallerFile();
|
||||
|
||||
Debug.Log("[InvitationCodeManager] Invitation code cleared");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if an invitation code is available.
|
||||
/// </summary>
|
||||
public static bool HasInvitationCode => !string.IsNullOrEmpty(GetInvitationCode());
|
||||
|
||||
private static string ReadFromFile() {
|
||||
try {
|
||||
string installDir = GetInstallDirectory();
|
||||
if (string.IsNullOrEmpty(installDir)) { return null; }
|
||||
|
||||
string filePath = Path.Combine(installDir, InvitationFileName);
|
||||
if (!File.Exists(filePath)) { return null; }
|
||||
|
||||
string json = File.ReadAllText(filePath);
|
||||
// Simple JSON parsing for {"invitation_code":"XXXX"}
|
||||
var parsed = JsonUtility.FromJson<InvitationFile>(json);
|
||||
return parsed?.invitation_code;
|
||||
} catch (Exception ex) {
|
||||
Debug.LogWarning(
|
||||
$"[InvitationCodeManager] Failed to read invitation file: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void DeleteInstallerFile() {
|
||||
try {
|
||||
string installDir = GetInstallDirectory();
|
||||
if (string.IsNullOrEmpty(installDir)) { return; }
|
||||
|
||||
string filePath = Path.Combine(installDir, InvitationFileName);
|
||||
if (File.Exists(filePath)) {
|
||||
File.Delete(filePath);
|
||||
Debug.Log("[InvitationCodeManager] Deleted installer invitation file");
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Debug.LogWarning(
|
||||
$"[InvitationCodeManager] Failed to delete invitation file: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetInstallDirectory() {
|
||||
#if UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN
|
||||
// Windows: %LOCALAPPDATA%\eagle0
|
||||
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 not yet supported
|
||||
return null;
|
||||
#endif
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
private class InvitationFile {
|
||||
public string invitation_code;
|
||||
}
|
||||
}
|
||||
}
|
||||
-2
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a2a19f11d1f82412e8e2811b366113ac
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using common;
|
||||
using Net.Eagle0.Eagle.Api.Auth;
|
||||
using UnityEngine;
|
||||
|
||||
@@ -25,7 +26,6 @@ namespace Auth {
|
||||
public event Action<string> OnLoginFailed;
|
||||
public event Action OnLogout;
|
||||
public event Action<bool> OnNewUserNeedsDisplayName; // true if new user
|
||||
public event Action OnInvitationRequired; // new user needs invitation code
|
||||
|
||||
public bool IsAuthenticated => TokenStorage.HasValidToken;
|
||||
public string DisplayName => TokenStorage.DisplayName;
|
||||
@@ -87,11 +87,11 @@ namespace Auth {
|
||||
_currentLoginProvider = provider; // Track for later storage
|
||||
|
||||
try {
|
||||
// Get invitation code if available (for new account registration)
|
||||
string invitationCode = InvitationCodeManager.GetInvitationCode();
|
||||
|
||||
// Get OAuth URL and state from server
|
||||
var (authUrl, state) = await _authClient.GetOAuthUrlAsync(provider, invitationCode);
|
||||
var (authUrl, state) = await _authClient.GetOAuthUrlAsync(provider);
|
||||
|
||||
// Minimize game window so user can see the browser
|
||||
WindowFocusManager.MinimizeForExternalBrowser();
|
||||
|
||||
// Open system browser
|
||||
Debug.Log($"[OAuthManager] Opening browser: {authUrl}");
|
||||
@@ -100,12 +100,8 @@ namespace Auth {
|
||||
// Poll for OAuth completion (server handles the callback)
|
||||
var response = await _authClient.PollForOAuthCompletionAsync(state);
|
||||
|
||||
// Handle invitation required status
|
||||
if (response.Status == OAuthStatus.InvitationRequired) {
|
||||
Debug.Log("[OAuthManager] Server requires invitation code for new account");
|
||||
OnInvitationRequired?.Invoke();
|
||||
throw new Exception("An invitation code is required to create a new account");
|
||||
}
|
||||
// Bring game back to foreground now that auth is complete
|
||||
WindowFocusManager.BringToForeground();
|
||||
|
||||
// Store tokens with provider info
|
||||
var providerName = provider.ToString().ToLowerInvariant();
|
||||
@@ -117,9 +113,7 @@ namespace Auth {
|
||||
response.User.DisplayName ?? "",
|
||||
providerName);
|
||||
|
||||
// Clear invitation code after successful new account creation
|
||||
if (response.IsNewUser) {
|
||||
InvitationCodeManager.ClearInvitationCode();
|
||||
OnNewUserNeedsDisplayName?.Invoke(true);
|
||||
} else {
|
||||
OnLoginSuccess?.Invoke(response.User);
|
||||
@@ -127,6 +121,9 @@ namespace Auth {
|
||||
|
||||
return response;
|
||||
} catch (Exception ex) {
|
||||
// Bring game back to foreground even on failure
|
||||
WindowFocusManager.BringToForeground();
|
||||
|
||||
Debug.LogError($"[OAuthManager] Login failed: {ex.Message}");
|
||||
OnLoginFailed?.Invoke(ex.Message);
|
||||
throw;
|
||||
|
||||
-221
@@ -1,221 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using common;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityGoDiceInterface;
|
||||
|
||||
public class DiceConfigurationPanelController : MonoBehaviour {
|
||||
private DiceInterface _diceInterface;
|
||||
|
||||
public EventBasedTable diceRollGroup;
|
||||
public PickerRowController pickerRowPrefab;
|
||||
public TMP_Text instructionsLabel;
|
||||
|
||||
public delegate void ConfigurationCallback(DieInfo? tensDieInfo, DieInfo? onesDieInfo);
|
||||
|
||||
private ConfigurationCallback _configurationCallback;
|
||||
|
||||
private DieInfo? _tensDieInfo = null;
|
||||
private DieInfo? _onesDieInfo = null;
|
||||
|
||||
private List<DieInfo> _knownDice = new List<DieInfo>();
|
||||
private HashSet<string> _connectingIdentifiers = new HashSet<string>();
|
||||
|
||||
void Awake() {}
|
||||
|
||||
public void GetConfiguration(ConfigurationCallback cb) {
|
||||
if (_diceInterface == null) { _diceInterface = GetComponentInParent<DiceInterface>(); }
|
||||
_diceInterface.deviceFoundCallback = DeviceFoundCallback;
|
||||
_diceInterface.connectionCallback = ConnectionCallback;
|
||||
_diceInterface.connectionFailedCallback = ConnectionFailedCallback;
|
||||
_diceInterface.disconnectionCallback = DisconnectionCallback;
|
||||
_diceInterface.listenerStoppedCallback = ListenerStoppedCallback;
|
||||
_diceInterface.colorCallback = ReceiveColorCallback;
|
||||
|
||||
string currentPath = NewLogLocation();
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(currentPath));
|
||||
_diceInterface.logger = log => {
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
Debug.Log(log);
|
||||
File.AppendAllText(currentPath, log);
|
||||
});
|
||||
};
|
||||
|
||||
_diceInterface.StartListening();
|
||||
|
||||
_configurationCallback = cb;
|
||||
_tensDieInfo = null;
|
||||
_onesDieInfo = null;
|
||||
instructionsLabel.text = "Looking for dice...";
|
||||
gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
string NewLogLocation() {
|
||||
return Path.Combine(
|
||||
Application.persistentDataPath,
|
||||
"eagle0",
|
||||
"Resources",
|
||||
"DiceConfigurationLogs",
|
||||
DateTime.Now.ToString("yyyy-MM-dd-HH-mm-ss") + ".log");
|
||||
}
|
||||
|
||||
public void RowClicked(int rowIndex) {
|
||||
if (_tensDieInfo == null) {
|
||||
_tensDieInfo = _knownDice[rowIndex];
|
||||
_knownDice.RemoveAt(rowIndex);
|
||||
SetUpTable();
|
||||
} else {
|
||||
_diceInterface.StopListening();
|
||||
|
||||
_onesDieInfo = _knownDice[rowIndex];
|
||||
_knownDice.RemoveAt(rowIndex);
|
||||
_knownDice.ForEach(dieInfo => _diceInterface.Disconnect(dieInfo.identifier));
|
||||
|
||||
// _diceInterface.deviceFoundCallback = null;
|
||||
// _diceInterface.connectionCallback = null;
|
||||
// _diceInterface.disconnectionCallback = null;
|
||||
// _diceInterface.listenerStoppedCallback = null;
|
||||
// _diceInterface.rollCallback = null;
|
||||
// _diceInterface.colorCallback = null;
|
||||
|
||||
_configurationCallback(_tensDieInfo, _onesDieInfo);
|
||||
gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEnable() {}
|
||||
|
||||
private void OnDisable() { diceRollGroup.RowCount = 0; }
|
||||
|
||||
void ListenerStoppedCallback() { Debug.Log("Listener stopped!"); }
|
||||
|
||||
void SetUpTable() {
|
||||
diceRollGroup.RowCount = _knownDice.Count;
|
||||
|
||||
if (_knownDice.Count == 0) {
|
||||
instructionsLabel.text = "Looking for dice...";
|
||||
} else if (_tensDieInfo == null) {
|
||||
instructionsLabel.text = "Select a TENS die:";
|
||||
} else {
|
||||
instructionsLabel.text = "Select an ONES die:";
|
||||
}
|
||||
|
||||
for (int i = 0; i < _knownDice.Count; i++) {
|
||||
var row = diceRollGroup.ComponentAt<PickerRowController>(i);
|
||||
|
||||
row.Text = _knownDice[i].deviceName;
|
||||
row.TextColor = UnityDieColors.ColorFromDieColor(_knownDice[i].color);
|
||||
}
|
||||
}
|
||||
|
||||
void ReceiveColorCallback(string identifier, DieColor dieColor) {
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
var existingIndex =
|
||||
_knownDice.FindIndex(dieInfo => dieInfo.identifier.Equals(identifier));
|
||||
if (existingIndex >= 0) {
|
||||
var colorName = dieColor.ToString();
|
||||
_knownDice[existingIndex] = new DieInfo(
|
||||
identifier: _knownDice[existingIndex].identifier,
|
||||
deviceName: colorName,
|
||||
color: dieColor,
|
||||
connected: true);
|
||||
} else {
|
||||
Debug.Log("This shouldn't happen");
|
||||
}
|
||||
|
||||
SetUpTable();
|
||||
});
|
||||
}
|
||||
|
||||
void DeviceFoundCallback(string identifier) {
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
if (_connectingIdentifiers.Contains(identifier)) { return; }
|
||||
|
||||
_connectingIdentifiers.Add(identifier);
|
||||
|
||||
var existingIndex =
|
||||
_knownDice.FindIndex(dieInfo => dieInfo.identifier.Equals(identifier));
|
||||
if (existingIndex >= 0) {
|
||||
_knownDice[existingIndex] =
|
||||
new DieInfo(identifier, "Unknown", DieColor.DieColorBlack);
|
||||
} else {
|
||||
_knownDice.Add(new DieInfo(identifier, "Unknown", DieColor.DieColorBlack));
|
||||
}
|
||||
|
||||
SetUpTable();
|
||||
|
||||
_diceInterface.Connect(identifier);
|
||||
});
|
||||
}
|
||||
|
||||
void ConnectionCallback(string identifier, string deviceName) {
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
_connectingIdentifiers.Remove(identifier);
|
||||
|
||||
if (_tensDieInfo != null && _tensDieInfo.Value.identifier.Equals(identifier)) {
|
||||
return;
|
||||
}
|
||||
if (_onesDieInfo != null && _onesDieInfo.Value.identifier.Equals(identifier)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var shortenedName = deviceName.Split('_')[1];
|
||||
var existingIndex =
|
||||
_knownDice.FindIndex(dieInfo => dieInfo.identifier.Equals(identifier));
|
||||
if (existingIndex >= 0) {
|
||||
_knownDice[existingIndex] =
|
||||
new DieInfo(identifier, shortenedName, DieColor.DieColorBlack);
|
||||
} else {
|
||||
_knownDice.Add(new DieInfo(identifier, shortenedName, DieColor.DieColorBlack));
|
||||
}
|
||||
|
||||
SetUpTable();
|
||||
|
||||
_diceInterface.RequestColor(identifier);
|
||||
});
|
||||
}
|
||||
|
||||
void ConnectionFailedCallback(string identifier) {
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
_connectingIdentifiers.Remove(identifier);
|
||||
|
||||
var existingIndex =
|
||||
_knownDice.FindIndex(dieInfo => dieInfo.identifier.Equals(identifier));
|
||||
if (existingIndex >= 0) {
|
||||
_knownDice.RemoveAt(existingIndex);
|
||||
|
||||
SetUpTable();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void DisconnectionCallback(string identifier) {
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
_connectingIdentifiers.Remove(identifier);
|
||||
|
||||
if (_tensDieInfo is {} tensInfo && tensInfo.identifier.Equals(identifier)) {
|
||||
_tensDieInfo = null;
|
||||
_onesDieInfo = null;
|
||||
} else if (_onesDieInfo is {} onesInfo && onesInfo.identifier.Equals(identifier)) {
|
||||
_onesDieInfo = null;
|
||||
}
|
||||
|
||||
var existingIndex =
|
||||
_knownDice.FindIndex(dieInfo => dieInfo.identifier.Equals(identifier));
|
||||
if (existingIndex >= 0) { _knownDice.RemoveAt(existingIndex); }
|
||||
|
||||
SetUpTable();
|
||||
_diceInterface.Connect(identifier);
|
||||
});
|
||||
}
|
||||
|
||||
public void DismissButtonClicked() {
|
||||
gameObject.SetActive(false);
|
||||
_configurationCallback(null, null);
|
||||
}
|
||||
|
||||
// Update is called once per frame
|
||||
void Update() {}
|
||||
}
|
||||
@@ -1,213 +0,0 @@
|
||||
#if UNITY_IOS || UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX
|
||||
#define USE_SWIFT_INTERFACE
|
||||
#elif UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN || ENABLE_WINMD_SUPPORT
|
||||
#define USE_WINDOWS_INTERFACE
|
||||
#else
|
||||
#endif
|
||||
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityGoDiceInterface;
|
||||
|
||||
public class DiceInterface : MonoBehaviour {
|
||||
public struct Commands {
|
||||
public static readonly byte PulseLed = 16;
|
||||
public static readonly byte GetColor = 23;
|
||||
}
|
||||
|
||||
readonly NativeDiceInterfaceImports _diceInterfaceImports = new();
|
||||
private static DiceInterface _singleton = null;
|
||||
|
||||
private static Dictionary<string, string> _deviceNames = new Dictionary<string, string>();
|
||||
|
||||
public delegate void DeviceFoundCallback(string identifier);
|
||||
public delegate void ConnectionCallback(string identifier, string deviceName);
|
||||
public delegate void ConnectionFailedCallback(string identifier);
|
||||
public delegate void DisconnectionCallback(string identifier);
|
||||
public delegate void ListenerStoppedCallback();
|
||||
public delegate void RollCallback(string identifier, sbyte x, sbyte y, sbyte z);
|
||||
public delegate void ColorCallback(string identifier, DieColor color);
|
||||
public delegate void LoggerCallback(string log);
|
||||
|
||||
public DeviceFoundCallback deviceFoundCallback = null;
|
||||
public ConnectionCallback connectionCallback = null;
|
||||
public ConnectionFailedCallback connectionFailedCallback = null;
|
||||
public DisconnectionCallback disconnectionCallback = null;
|
||||
public ListenerStoppedCallback listenerStoppedCallback = null;
|
||||
public RollCallback rollCallback = null;
|
||||
public ColorCallback colorCallback = null;
|
||||
public LoggerCallback logger = null;
|
||||
|
||||
public void StartListening() { _diceInterfaceImports.StartListening(); }
|
||||
|
||||
public void StopListening() { _diceInterfaceImports.StopListening(); }
|
||||
|
||||
public void Connect(string identifier) { _diceInterfaceImports.Connect(identifier); }
|
||||
public void Disconnect(string identifier) { _diceInterfaceImports.Disconnect(identifier); }
|
||||
|
||||
public void Reset() {
|
||||
_diceInterfaceImports.Reset();
|
||||
_deviceNames.Clear();
|
||||
}
|
||||
|
||||
public void RequestColor(string identifier) {
|
||||
_diceInterfaceImports.Send(identifier, new List<byte> { Commands.GetColor });
|
||||
}
|
||||
|
||||
public void FlashColor(
|
||||
string identifier,
|
||||
byte pulseCount,
|
||||
byte onTime10ms,
|
||||
byte offTime10ms,
|
||||
byte red,
|
||||
byte green,
|
||||
byte blue) {
|
||||
_diceInterfaceImports.Send(
|
||||
identifier,
|
||||
new List<byte> {
|
||||
Commands.PulseLed,
|
||||
pulseCount,
|
||||
onTime10ms,
|
||||
offTime10ms,
|
||||
red,
|
||||
green,
|
||||
blue,
|
||||
0x01,
|
||||
0x00
|
||||
});
|
||||
}
|
||||
|
||||
// Start is called before the first frame update
|
||||
void Start() {
|
||||
_singleton = this;
|
||||
|
||||
_diceInterfaceImports.SetDeviceConnectedCallback(DeviceConnectedDelegateMessageReceived);
|
||||
_diceInterfaceImports.SetDeviceConnectionFailedCallback(
|
||||
DeviceConnectionFailedDelegateMessageReceived);
|
||||
_diceInterfaceImports.SetDeviceDisconnectedCallback(
|
||||
DeviceDisconnectedDelegateMessageReceived);
|
||||
_diceInterfaceImports.SetDeviceFoundCallback(DeviceFoundDelegateMessageReceived);
|
||||
_diceInterfaceImports.SetDataCallback(DataDelegateMessageReceived);
|
||||
_diceInterfaceImports.SetListenerStoppedCallback(ListenerStoppedDelegateMessageReceived);
|
||||
_diceInterfaceImports.SetLoggerCallback(LoggerDelegateMessageReceived);
|
||||
}
|
||||
|
||||
// Update is called once per frame
|
||||
void Update() {}
|
||||
|
||||
private static sbyte[] GetRollVector(List<byte> rawData) {
|
||||
if (rawData.Count < 1) {
|
||||
Debug.Log("rollVector: no data");
|
||||
return null;
|
||||
}
|
||||
byte firstByte = rawData[0];
|
||||
if (firstByte != 83) {
|
||||
Debug.Log("rollVector: first byte is not 83");
|
||||
return null;
|
||||
}
|
||||
if (rawData.Count != 4) {
|
||||
Debug.Log("rollVector: data length is not 4");
|
||||
return null;
|
||||
}
|
||||
return new[] { (sbyte)rawData[1], (sbyte)rawData[2], (sbyte)rawData[3] };
|
||||
}
|
||||
|
||||
private static void DeviceFoundDelegateMessageReceived(string identifier, string deviceName) {
|
||||
if (_singleton != null) {
|
||||
_deviceNames[identifier] = deviceName;
|
||||
if (_singleton.deviceFoundCallback != null) {
|
||||
_singleton.deviceFoundCallback(identifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void DeviceConnectedDelegateMessageReceived(string identifier) {
|
||||
if (_singleton != null && _singleton.connectionCallback != null) {
|
||||
_singleton.connectionCallback(identifier, _deviceNames[identifier]);
|
||||
}
|
||||
}
|
||||
|
||||
private static void DeviceConnectionFailedDelegateMessageReceived(string identifier) {
|
||||
if (_singleton != null && _singleton.connectionFailedCallback != null) {
|
||||
_singleton.connectionFailedCallback(identifier);
|
||||
}
|
||||
}
|
||||
|
||||
private static void DeviceDisconnectedDelegateMessageReceived(string identifier) {
|
||||
if (_singleton != null && _singleton.disconnectionCallback != null) {
|
||||
_singleton.disconnectionCallback(identifier);
|
||||
}
|
||||
}
|
||||
|
||||
private static void DataDelegateMessageReceived(string identifier, List<byte> byteList) {
|
||||
if (_singleton != null) {
|
||||
if (byteList.Count == 0) {
|
||||
if (_singleton.connectionCallback != null) {
|
||||
// I don't think this one should still happen
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
byte firstByte = byteList[0];
|
||||
|
||||
switch (firstByte) {
|
||||
case 82:
|
||||
// Roll started
|
||||
break;
|
||||
case 66:
|
||||
// Battery level
|
||||
break;
|
||||
case (byte)'C':
|
||||
if (byteList[1] == (byte)'o' && byteList[2] == (byte)'l') {
|
||||
byte colorRawValue = byteList[3];
|
||||
|
||||
_singleton.colorCallback(identifier, (DieColor)colorRawValue);
|
||||
}
|
||||
// Color (fetched)
|
||||
break;
|
||||
case 83: {
|
||||
var roll = GetRollVector(byteList);
|
||||
if (roll != null && _singleton.rollCallback != null) {
|
||||
_singleton.rollCallback(identifier, roll[0], roll[1], roll[2]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 70:
|
||||
case 84:
|
||||
case 77: {
|
||||
byteList.RemoveAt(0);
|
||||
var roll = GetRollVector(byteList);
|
||||
if (roll != null && _singleton.rollCallback != null) {
|
||||
_singleton.rollCallback(identifier, roll[0], roll[1], roll[2]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: Debug.Log("Not yet handled"); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ListenerStoppedDelegateMessageReceived() {
|
||||
if (_singleton != null && _singleton.listenerStoppedCallback != null) {
|
||||
_singleton.listenerStoppedCallback();
|
||||
}
|
||||
}
|
||||
|
||||
private static void LoggerDelegateMessageReceived(string log) {
|
||||
if (_singleton != null && _singleton.logger != null) { _singleton.logger(log); }
|
||||
}
|
||||
|
||||
public void OnDestroy() {
|
||||
_diceInterfaceImports.SetDeviceConnectedCallback(null);
|
||||
_diceInterfaceImports.SetDeviceConnectionFailedCallback(null);
|
||||
_diceInterfaceImports.SetDeviceDisconnectedCallback(null);
|
||||
_diceInterfaceImports.SetDeviceFoundCallback(null);
|
||||
_diceInterfaceImports.SetDataCallback(null);
|
||||
_diceInterfaceImports.SetListenerStoppedCallback(null);
|
||||
_diceInterfaceImports.SetLoggerCallback(null);
|
||||
#if UNITY_EDITOR
|
||||
StopListening();
|
||||
Reset();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 75ea5d911cfab4851831d1de4b61f559
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,204 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace UnityGoDiceInterface {
|
||||
public class DiceVectors {
|
||||
public struct Vector3 {
|
||||
public sbyte x;
|
||||
public sbyte y;
|
||||
public sbyte z;
|
||||
|
||||
public Vector3(sbyte x, sbyte y, sbyte z) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
}
|
||||
|
||||
public static int D6Value(sbyte x, sbyte y, sbyte z) {
|
||||
return DieValueForVector(d6, new Vector3(x, y, z));
|
||||
}
|
||||
|
||||
public static int D10Value(sbyte x, sbyte y, sbyte z) {
|
||||
return d10Transform[DieValueForVector(d20, new Vector3(x, y, z))];
|
||||
}
|
||||
|
||||
private static int sq(int x) { return x * x; }
|
||||
|
||||
private static int DieValueForVector(List<Vector3> table, Vector3 vector) {
|
||||
var closestIndex = -1;
|
||||
var closestDistance = int.MaxValue;
|
||||
|
||||
for (int i = 0; i < table.Count; i++) {
|
||||
var entry = table[i];
|
||||
var distance =
|
||||
sq(entry.x - vector.x) + sq(entry.y - vector.y) + sq(entry.z - vector.z);
|
||||
if (distance < closestDistance) {
|
||||
closestDistance = distance;
|
||||
closestIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
return closestIndex + 1;
|
||||
}
|
||||
|
||||
// The private vectors are 0-based, so we need to add 1 to the index
|
||||
private static List<Vector3> d6 = new List<Vector3> {
|
||||
new Vector3(-64, 0, 0),
|
||||
new Vector3(-0, 0, 64),
|
||||
new Vector3(0, 64, 0),
|
||||
new Vector3(0, -64, 0),
|
||||
new Vector3(0, 0, -64),
|
||||
new Vector3(64, 0, 0)
|
||||
};
|
||||
|
||||
private static List<Vector3> d20 = new List<Vector3> {
|
||||
new Vector3(-64, 0, -22), new Vector3(42, -42, 40), new Vector3(0, 22, -64),
|
||||
new Vector3(0, 22, 64), new Vector3(-42, -42, 42), new Vector3(22, 64, 0),
|
||||
new Vector3(-42, -42, -42), new Vector3(64, 0, -22), new Vector3(-22, 64, 0),
|
||||
new Vector3(42, -42, -42), new Vector3(-42, 42, 42), new Vector3(22, -64, 0),
|
||||
new Vector3(-64, 0, 22), new Vector3(42, 42, 42), new Vector3(-22, -64, 0),
|
||||
new Vector3(42, 42, -42), new Vector3(0, -22, -64), new Vector3(0, -22, 64),
|
||||
new Vector3(-42, 42, -42), new Vector3(64, 0, 22),
|
||||
};
|
||||
|
||||
// static d24Vectors = {
|
||||
// new Vector3(20, -60, -20),
|
||||
// new Vector3(20, 0, 60),
|
||||
// new Vector3(-40, -40, 40),
|
||||
// new Vector3(-60, 0, 20),
|
||||
// new Vector3(40, 20, 40),
|
||||
// new Vector3(-20, -60, -20),
|
||||
// new Vector3(20, 60, 20),
|
||||
// new Vector3(-40, 20, -40),
|
||||
// new Vector3(-40, 40, 40),
|
||||
// new Vector3(-20, 0, 60),
|
||||
// new Vector3(-20, -60, 20),
|
||||
// new Vector3(60, 0, 20),
|
||||
// new Vector3(-60, 0, -20),
|
||||
// new Vector3(20, 60, -20),
|
||||
// new Vector3(20, 0, -60),
|
||||
// new Vector3(40, -20, -40),
|
||||
// new Vector3(-20, 60, -20),
|
||||
// new Vector3(-40, -40, -40),
|
||||
// new Vector3(40, -20, 40),
|
||||
// new Vector3(20, -60, 20),
|
||||
// new Vector3(60, 0, -20),
|
||||
// new Vector3(40, 20, -40),
|
||||
// new Vector3(-20, 0, -60),
|
||||
// new Vector3(-20, 60, 20),
|
||||
// }
|
||||
//
|
||||
// Transforms from each shell type to according number on shell
|
||||
// D20 Transforms
|
||||
private static Dictionary<int, int> d10Transform = new Dictionary<int, int> {
|
||||
{ 1, 8 }, { 2, 2 }, { 3, 6 }, { 4, 1 }, { 5, 4 }, { 6, 3 }, { 7, 9 },
|
||||
{ 8, 0 }, { 9, 7 }, { 10, 5 }, { 11, 5 }, { 12, 7 }, { 13, 0 }, { 14, 9 },
|
||||
{ 15, 3 }, { 16, 4 }, { 17, 1 }, { 18, 6 }, { 19, 2 }, { 20, 8 },
|
||||
};
|
||||
//
|
||||
// static d10XTransform = {
|
||||
// 1: 80,
|
||||
// 2: 20,
|
||||
// 3: 60,
|
||||
// 4: 10,
|
||||
// 5: 40,
|
||||
// 6: 30,
|
||||
// 7: 90,
|
||||
// 8: 0,
|
||||
// 9: 70,
|
||||
// 10: 50,
|
||||
// 11: 50,
|
||||
// 12: 70,
|
||||
// 13: 0,
|
||||
// 14: 90,
|
||||
// 15: 30,
|
||||
// 16: 40,
|
||||
// 17: 10,
|
||||
// 18: 60,
|
||||
// 19: 20,
|
||||
// 20: 80,
|
||||
// }
|
||||
//
|
||||
// // D24 Transforms
|
||||
// static d4Transform = {
|
||||
// 1: 3,
|
||||
// 2: 1,
|
||||
// 3: 4,
|
||||
// 4: 1,
|
||||
// 5: 4,
|
||||
// 6: 4,
|
||||
// 7: 1,
|
||||
// 8: 4,
|
||||
// 9: 2,
|
||||
// 10: 3,
|
||||
// 11: 1,
|
||||
// 12: 1,
|
||||
// 13: 1,
|
||||
// 14: 4,
|
||||
// 15: 2,
|
||||
// 16: 3,
|
||||
// 17: 3,
|
||||
// 18: 2,
|
||||
// 19: 2,
|
||||
// 20: 2,
|
||||
// 21: 4,
|
||||
// 22: 1,
|
||||
// 23: 3,
|
||||
// 24: 2,
|
||||
// }
|
||||
//
|
||||
// static d8Transform = {
|
||||
// 1: 3,
|
||||
// 2: 3,
|
||||
// 3: 6,
|
||||
// 4: 1,
|
||||
// 5: 2,
|
||||
// 6: 8,
|
||||
// 7: 1,
|
||||
// 8: 1,
|
||||
// 9: 4,
|
||||
// 10: 7,
|
||||
// 11: 5,
|
||||
// 12: 5,
|
||||
// 13: 4,
|
||||
// 14: 4,
|
||||
// 15: 2,
|
||||
// 16: 5,
|
||||
// 17: 7,
|
||||
// 18: 7,
|
||||
// 19: 8,
|
||||
// 20: 2,
|
||||
// 21: 8,
|
||||
// 22: 3,
|
||||
// 23: 6,
|
||||
// 24: 6,
|
||||
// }
|
||||
//
|
||||
// static d12Transform = {
|
||||
// 1: 1,
|
||||
// 2: 2,
|
||||
// 3: 3,
|
||||
// 4: 4,
|
||||
// 5: 5,
|
||||
// 6: 6,
|
||||
// 7: 7,
|
||||
// 8: 8,
|
||||
// 9: 9,
|
||||
// 10: 10,
|
||||
// 11: 11,
|
||||
// 12: 12,
|
||||
// 13: 1,
|
||||
// 14: 2,
|
||||
// 15: 3,
|
||||
// 16: 4,
|
||||
// 17: 5,
|
||||
// 18: 6,
|
||||
// 19: 7,
|
||||
// 20: 8,
|
||||
// 21: 9,
|
||||
// 22: 10,
|
||||
// 23: 11,
|
||||
// 24: 12,
|
||||
// }
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e9d67d9a2f8145a999d04de91c27073d
|
||||
timeCreated: 1702612480
|
||||
@@ -1,28 +0,0 @@
|
||||
namespace UnityGoDiceInterface {
|
||||
public enum DieColor : byte {
|
||||
DieColorBlack = 0,
|
||||
DieColorRed = 1,
|
||||
DieColorGreen = 2,
|
||||
DieColorBlue = 3,
|
||||
DieColorYellow = 4,
|
||||
DieColorOrange = 5,
|
||||
}
|
||||
|
||||
public struct DieInfo {
|
||||
public string identifier;
|
||||
public string deviceName;
|
||||
public DieColor color;
|
||||
public bool connected;
|
||||
|
||||
public DieInfo(
|
||||
string identifier,
|
||||
string deviceName,
|
||||
DieColor color,
|
||||
bool connected = false) {
|
||||
this.identifier = identifier;
|
||||
this.deviceName = deviceName;
|
||||
this.color = color;
|
||||
this.connected = connected;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2e1000782bb845399869ca7910eebcd6
|
||||
timeCreated: 1703172326
|
||||
-177
@@ -1,177 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using AOT;
|
||||
|
||||
namespace UnityGoDiceInterface {
|
||||
public class NativeDiceInterfaceImports {
|
||||
public delegate void DeviceFoundDelegateMessage(string identifier, string name);
|
||||
public delegate void DataDelegateMessage(string identifier, List<byte> bytes);
|
||||
public delegate void DeviceConnectedDelegateMessage(string identifier);
|
||||
public delegate void DeviceConnectionFailedDelegateMessage(string identifier);
|
||||
public delegate void DeviceDisconnectedDelegateMessage(string identifier);
|
||||
public delegate void ListenerStoppedDelegateMessage();
|
||||
public delegate void LoggerDelegateMessage(string log);
|
||||
|
||||
private static DeviceFoundDelegateMessage deviceFoundDelegate;
|
||||
private static DataDelegateMessage dataDelegate;
|
||||
private static DeviceConnectedDelegateMessage deviceConnectedDelegate;
|
||||
private static DeviceConnectionFailedDelegateMessage deviceConnectionFailedDelegate;
|
||||
private static DeviceDisconnectedDelegateMessage deviceDisconnectedDelegate;
|
||||
private static ListenerStoppedDelegateMessage listenerStoppedDelegate;
|
||||
private static LoggerDelegateMessage loggerDelegateMessage;
|
||||
|
||||
#if UNITY_IOS
|
||||
private const string BundleName = "__Internal";
|
||||
#elif UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX
|
||||
private const string BundleName = "DarwinGodiceBundle";
|
||||
#elif UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN
|
||||
private const string BundleName = "GoDiceDll.dll";
|
||||
#endif
|
||||
|
||||
private delegate void MonoDeviceFoundDelegateMessage(string identifier, string name);
|
||||
private delegate void
|
||||
MonoDataDelegateMessage(string identifier, UInt32 byteCount, IntPtr bytePtr);
|
||||
private delegate void MonoDeviceConnectedDelegateMessage(string identifier);
|
||||
private delegate void MonoDeviceConnectionFailedDelegateMessage(string identifier);
|
||||
private delegate void MonoDeviceDisconnectedDelegateMessage(string identifier);
|
||||
private delegate void MonoListenerStoppedDelegateMessage();
|
||||
private delegate void MonoLoggerDelegateMessage(string log);
|
||||
|
||||
[DllImport(dllName: BundleName, EntryPoint = "godice_start_listening")]
|
||||
private static extern void _NativeBridgeStartListening();
|
||||
|
||||
[DllImport(dllName: BundleName, EntryPoint = "godice_stop_listening")]
|
||||
private static extern void _NativeBridgeStopListening();
|
||||
|
||||
[DllImport(dllName: BundleName, EntryPoint = "godice_set_callbacks")]
|
||||
private static extern void _NativeBridgeSetCallbacks(
|
||||
MonoDeviceFoundDelegateMessage deviceFoundDelegate,
|
||||
MonoDataDelegateMessage dataDelegate,
|
||||
MonoDeviceConnectedDelegateMessage deviceConnectedDelegate,
|
||||
MonoDeviceConnectionFailedDelegateMessage deviceConnectionFailedDelegateMessage,
|
||||
MonoDeviceDisconnectedDelegateMessage deviceDisconnectedDelegate,
|
||||
MonoListenerStoppedDelegateMessage listenerStoppedDelegate);
|
||||
|
||||
[DllImport(dllName: BundleName, EntryPoint = "godice_connect")]
|
||||
private static extern void _NativeBridgeConnect(string identifier);
|
||||
|
||||
[DllImport(dllName: BundleName, EntryPoint = "godice_disconnect")]
|
||||
private static extern void _NativeBridgeDisconnect(string identifier);
|
||||
|
||||
[DllImport(dllName: BundleName, EntryPoint = "godice_send")]
|
||||
private static extern void
|
||||
_NativeBridgeSend(string identifier, UInt32 byteCount, IntPtr bytePtr);
|
||||
|
||||
[DllImport(dllName: BundleName, EntryPoint = "godice_set_logger")]
|
||||
private static extern void _NativeBridgeSetLogger(MonoLoggerDelegateMessage loggerDelegate);
|
||||
|
||||
[DllImport(dllName: BundleName, EntryPoint = "godice_reset")]
|
||||
private static extern void _NativeBridgeReset();
|
||||
|
||||
private static List<byte> BytesFromRawPointer(UInt32 byteCount, IntPtr bytes) {
|
||||
byte[] array = new byte[byteCount];
|
||||
if (byteCount > 0) { Marshal.Copy(bytes, array, 0, (int)byteCount); }
|
||||
return new List<byte>(array);
|
||||
}
|
||||
|
||||
/*
|
||||
* Callback methods
|
||||
*/
|
||||
[MonoPInvokeCallback(typeof(MonoDeviceFoundDelegateMessage))]
|
||||
private static void MonoDeviceFoundMessageReceived(string identifier, string name) {
|
||||
if (deviceFoundDelegate != null) { deviceFoundDelegate(identifier, name); }
|
||||
}
|
||||
|
||||
[MonoPInvokeCallback(typeof(MonoDataDelegateMessage))]
|
||||
private static void
|
||||
MonoDataMessageReceived(string identifier, UInt32 byteCount, IntPtr bytePtr) {
|
||||
if (dataDelegate != null) {
|
||||
dataDelegate(identifier, BytesFromRawPointer(byteCount, bytePtr));
|
||||
}
|
||||
}
|
||||
|
||||
[MonoPInvokeCallback(typeof(MonoDeviceConnectedDelegateMessage))]
|
||||
private static void MonoDeviceConnected(string identifier) {
|
||||
if (deviceConnectedDelegate != null) { deviceConnectedDelegate(identifier); }
|
||||
}
|
||||
|
||||
[MonoPInvokeCallback(typeof(MonoDeviceConnectionFailedDelegateMessage))]
|
||||
private static void MonoDeviceConnectionFailed(string identifier) {
|
||||
if (deviceConnectionFailedDelegate != null) {
|
||||
deviceConnectionFailedDelegate(identifier);
|
||||
}
|
||||
}
|
||||
|
||||
[MonoPInvokeCallback(typeof(MonoDeviceDisconnectedDelegateMessage))]
|
||||
private static void MonoDeviceDisconnected(string identifier) {
|
||||
if (deviceDisconnectedDelegate != null) { deviceDisconnectedDelegate(identifier); }
|
||||
}
|
||||
|
||||
[MonoPInvokeCallback(typeof(MonoListenerStoppedDelegateMessage))]
|
||||
private static void MonoListenerStopped() {
|
||||
if (listenerStoppedDelegate != null) { listenerStoppedDelegate(); }
|
||||
}
|
||||
|
||||
[MonoPInvokeCallback(typeof(MonoLoggerDelegateMessage))]
|
||||
private static void MonoLogger(string log) {
|
||||
if (loggerDelegateMessage != null) { loggerDelegateMessage(log); }
|
||||
}
|
||||
|
||||
public void StartListening() {
|
||||
_NativeBridgeSetCallbacks(
|
||||
MonoDeviceFoundMessageReceived,
|
||||
MonoDataMessageReceived,
|
||||
MonoDeviceConnected,
|
||||
MonoDeviceConnectionFailed,
|
||||
MonoDeviceDisconnected,
|
||||
MonoListenerStopped);
|
||||
_NativeBridgeSetLogger(MonoLogger);
|
||||
_NativeBridgeStartListening();
|
||||
}
|
||||
|
||||
public void StopListening() { _NativeBridgeStopListening(); }
|
||||
|
||||
public void Connect(string identifier) { _NativeBridgeConnect(identifier); }
|
||||
|
||||
public void Disconnect(string identifier) { _NativeBridgeDisconnect(identifier); }
|
||||
|
||||
public void Send(string identifier, List<byte> data) {
|
||||
byte[] byteArray = data.ToArray();
|
||||
GCHandle pinnedArray = GCHandle.Alloc(byteArray, GCHandleType.Pinned);
|
||||
IntPtr pointer = pinnedArray.AddrOfPinnedObject();
|
||||
_NativeBridgeSend(identifier, (uint)data.Count, pointer);
|
||||
pinnedArray.Free();
|
||||
}
|
||||
|
||||
public void SetDeviceFoundCallback(DeviceFoundDelegateMessage deviceFound) {
|
||||
deviceFoundDelegate = deviceFound;
|
||||
}
|
||||
|
||||
public void SetDataCallback(DataDelegateMessage data) { dataDelegate = data; }
|
||||
|
||||
public void SetDeviceConnectedCallback(DeviceConnectedDelegateMessage deviceConnected) {
|
||||
deviceConnectedDelegate = deviceConnected;
|
||||
}
|
||||
|
||||
public void SetDeviceConnectionFailedCallback(
|
||||
DeviceConnectionFailedDelegateMessage deviceConnectionFailed) {
|
||||
deviceConnectionFailedDelegate = deviceConnectionFailed;
|
||||
}
|
||||
|
||||
public void SetDeviceDisconnectedCallback(
|
||||
DeviceDisconnectedDelegateMessage deviceDisconnected) {
|
||||
deviceDisconnectedDelegate = deviceDisconnected;
|
||||
}
|
||||
|
||||
public void SetListenerStoppedCallback(ListenerStoppedDelegateMessage listenerStopped) {
|
||||
listenerStoppedDelegate = listenerStopped;
|
||||
}
|
||||
|
||||
public void SetLoggerCallback(LoggerDelegateMessage logger) {
|
||||
loggerDelegateMessage = logger;
|
||||
}
|
||||
|
||||
public void Reset() { _NativeBridgeReset(); }
|
||||
}
|
||||
}
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 424476bb5b18c47039d649c28a58a8fa
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
-447
@@ -1,447 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!1 &2154262805260441194
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 7181142147825592926}
|
||||
- component: {fileID: 4917217417751722011}
|
||||
- component: {fileID: 8876282893591316834}
|
||||
- component: {fileID: 1837103631940551462}
|
||||
- component: {fileID: 7123394496182663843}
|
||||
m_Layer: 0
|
||||
m_Name: OneDiceResult
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &7181142147825592926
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2154262805260441194}
|
||||
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: 145111913694346175}
|
||||
- {fileID: 6185631331142294830}
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 100, y: 100}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &4917217417751722011
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2154262805260441194}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 59f8146938fff824cb5fd77236b75775, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Padding:
|
||||
m_Left: 0
|
||||
m_Right: 0
|
||||
m_Top: 0
|
||||
m_Bottom: 0
|
||||
m_ChildAlignment: 4
|
||||
m_Spacing: 0
|
||||
m_ChildForceExpandWidth: 0
|
||||
m_ChildForceExpandHeight: 0
|
||||
m_ChildControlWidth: 1
|
||||
m_ChildControlHeight: 1
|
||||
m_ChildScaleWidth: 0
|
||||
m_ChildScaleHeight: 0
|
||||
m_ReverseArrangement: 0
|
||||
--- !u!114 &8876282893591316834
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2154262805260441194}
|
||||
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: -1
|
||||
m_PreferredWidth: -1
|
||||
m_PreferredHeight: -1
|
||||
m_FlexibleWidth: -1
|
||||
m_FlexibleHeight: -1
|
||||
m_LayoutPriority: 1
|
||||
--- !u!114 &1837103631940551462
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2154262805260441194}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 680b7d1179eb34a0fa338286236db2e9, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
nameLabel: {fileID: 9015301821484574050}
|
||||
d6Label: {fileID: 0}
|
||||
d10Label: {fileID: 4483020407950697354}
|
||||
--- !u!222 &7123394496182663843
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2154262805260441194}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!1 &6943825177879701675
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 6185631331142294830}
|
||||
- component: {fileID: 4032026062387785650}
|
||||
- component: {fileID: 4483020407950697354}
|
||||
- component: {fileID: 742703408719749652}
|
||||
- component: {fileID: 4292989854150447437}
|
||||
m_Layer: 0
|
||||
m_Name: result
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &6185631331142294830
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 6943825177879701675}
|
||||
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: 7181142147825592926}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &4032026062387785650
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 6943825177879701675}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &4483020407950697354
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 6943825177879701675}
|
||||
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: 9
|
||||
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: 4278190080
|
||||
m_fontColor: {r: 0, g: 0, b: 0, 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: 48
|
||||
m_fontSizeBase: 48
|
||||
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_wordSpacing: 0
|
||||
m_lineSpacing: 0
|
||||
m_lineSpacingMax: 0
|
||||
m_paragraphSpacing: 0
|
||||
m_charWidthMaxAdj: 0
|
||||
m_enableWordWrapping: 1
|
||||
m_wordWrappingRatios: 0.4
|
||||
m_overflowMode: 0
|
||||
m_linkedTextComponent: {fileID: 0}
|
||||
parentLinkedComponent: {fileID: 0}
|
||||
m_enableKerning: 1
|
||||
m_enableExtraPadding: 0
|
||||
checkPaddingRequired: 0
|
||||
m_isRichText: 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!114 &742703408719749652
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 6943825177879701675}
|
||||
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: -1
|
||||
m_PreferredWidth: -1
|
||||
m_PreferredHeight: -1
|
||||
m_FlexibleWidth: -1
|
||||
m_FlexibleHeight: -1
|
||||
m_LayoutPriority: 1
|
||||
--- !u!114 &4292989854150447437
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 6943825177879701675}
|
||||
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: -1
|
||||
m_PreferredWidth: -1
|
||||
m_PreferredHeight: -1
|
||||
m_FlexibleWidth: 1
|
||||
m_FlexibleHeight: -1
|
||||
m_LayoutPriority: 1
|
||||
--- !u!1 &8198927819804470251
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 145111913694346175}
|
||||
- component: {fileID: 8510898822381381753}
|
||||
- component: {fileID: 9015301821484574050}
|
||||
- component: {fileID: 4918688670369599320}
|
||||
m_Layer: 0
|
||||
m_Name: Name
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &145111913694346175
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 8198927819804470251}
|
||||
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: 7181142147825592926}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &8510898822381381753
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 8198927819804470251}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &9015301821484574050
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 8198927819804470251}
|
||||
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: 'Tens
|
||||
|
||||
'
|
||||
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: 4278190080
|
||||
m_fontColor: {r: 0, g: 0, b: 0, 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: 16
|
||||
m_fontSizeBase: 16
|
||||
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_wordSpacing: 0
|
||||
m_lineSpacing: 0
|
||||
m_lineSpacingMax: 0
|
||||
m_paragraphSpacing: 0
|
||||
m_charWidthMaxAdj: 0
|
||||
m_enableWordWrapping: 1
|
||||
m_wordWrappingRatios: 0.4
|
||||
m_overflowMode: 0
|
||||
m_linkedTextComponent: {fileID: 0}
|
||||
parentLinkedComponent: {fileID: 0}
|
||||
m_enableKerning: 1
|
||||
m_enableExtraPadding: 0
|
||||
checkPaddingRequired: 0
|
||||
m_isRichText: 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!114 &4918688670369599320
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 8198927819804470251}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_IgnoreLayout: 0
|
||||
m_MinWidth: 200
|
||||
m_MinHeight: -1
|
||||
m_PreferredWidth: -1
|
||||
m_PreferredHeight: -1
|
||||
m_FlexibleWidth: 1
|
||||
m_FlexibleHeight: 1
|
||||
m_LayoutPriority: 1
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
using TMPro;
|
||||
|
||||
public class OneDiceRollController : TableRowController {
|
||||
private string _identifier;
|
||||
|
||||
public TMP_Text nameLabel;
|
||||
public TMP_Text d6Label;
|
||||
public TMP_Text d10Label;
|
||||
|
||||
// Start is called before the first frame update
|
||||
void Start() {}
|
||||
|
||||
// Update is called once per frame
|
||||
void Update() {}
|
||||
|
||||
public void SetUp(string identifier, string deviceName) {
|
||||
_identifier = identifier;
|
||||
|
||||
nameLabel.text = deviceName;
|
||||
d6Label.text = "?";
|
||||
d10Label.text = "?";
|
||||
}
|
||||
|
||||
public void SetRoll(string identifier, string d6, string d10) {
|
||||
_identifier = identifier;
|
||||
|
||||
d6Label.text = d6;
|
||||
d10Label.text = d10;
|
||||
}
|
||||
|
||||
public string GetIdentifier() { return _identifier; }
|
||||
}
|
||||
@@ -1,281 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!1 &3756143511112798868
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 5260824885446700081}
|
||||
- component: {fileID: 757087991837136050}
|
||||
- component: {fileID: 8474069203316727232}
|
||||
m_Layer: 0
|
||||
m_Name: Text (TMP)
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &5260824885446700081
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 3756143511112798868}
|
||||
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: 3793282440965013679}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 200, y: 50}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &757087991837136050
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 3756143511112798868}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &8474069203316727232
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 3756143511112798868}
|
||||
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: Orange
|
||||
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: 4278229493
|
||||
m_fontColor: {r: 0.9607843, g: 0.6, b: 0, 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_wordSpacing: 0
|
||||
m_lineSpacing: 0
|
||||
m_lineSpacingMax: 0
|
||||
m_paragraphSpacing: 0
|
||||
m_charWidthMaxAdj: 0
|
||||
m_enableWordWrapping: 1
|
||||
m_wordWrappingRatios: 0.4
|
||||
m_overflowMode: 0
|
||||
m_linkedTextComponent: {fileID: 0}
|
||||
parentLinkedComponent: {fileID: 0}
|
||||
m_enableKerning: 1
|
||||
m_enableExtraPadding: 0
|
||||
checkPaddingRequired: 0
|
||||
m_isRichText: 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!1 &5800426311821892848
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 3793282440965013679}
|
||||
- component: {fileID: 6926687573176944590}
|
||||
- component: {fileID: -2050598564750628712}
|
||||
- component: {fileID: 2781819584472077043}
|
||||
- component: {fileID: -2772541110306127347}
|
||||
- component: {fileID: 5171470299130298381}
|
||||
m_Layer: 0
|
||||
m_Name: PickerRow
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &3793282440965013679
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5800426311821892848}
|
||||
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: 5260824885446700081}
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 200, y: 50}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &6926687573176944590
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5800426311821892848}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &-2050598564750628712
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5800426311821892848}
|
||||
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 &2781819584472077043
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5800426311821892848}
|
||||
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: 0}
|
||||
m_Type: 0
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
m_FillMethod: 4
|
||||
m_FillAmount: 1
|
||||
m_FillClockwise: 1
|
||||
m_FillOrigin: 0
|
||||
m_UseSpriteMesh: 0
|
||||
m_PixelsPerUnitMultiplier: 1
|
||||
--- !u!114 &-2772541110306127347
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5800426311821892848}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: d7b0b988957134dbc9c5e270bd5e9e70, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
nameLabel: {fileID: 8474069203316727232}
|
||||
--- !u!114 &5171470299130298381
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5800426311821892848}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 876f18cbcbaea4cba9510a977f343c42, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
onClick:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
onRightClick:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
onLeftDown:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
onLeftUp:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
onRightDown:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
onRightUp:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
selectable: 1
|
||||
rightSelectable: 0
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
using TMPro;
|
||||
using UnityEngine.UI;
|
||||
|
||||
public class PickerRowController : TableRowController {
|
||||
public TMP_Text nameLabel;
|
||||
|
||||
public string Text {
|
||||
get => nameLabel.text;
|
||||
set => nameLabel.text = value;
|
||||
}
|
||||
|
||||
override public void ColorRow() { gameObject.GetComponent<Image>().color = BackgroundColor; }
|
||||
|
||||
// Start is called before the first frame update
|
||||
void Start() {}
|
||||
|
||||
// Update is called once per frame
|
||||
void Update() {}
|
||||
}
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d7b0b988957134dbc9c5e270bd5e9e70
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,9 +0,0 @@
|
||||
using Net.Eagle0.Shardok.Api;
|
||||
|
||||
namespace UnityGoDiceInterface {
|
||||
public interface RollFetcher {
|
||||
public delegate void RollResultCallback(int? result);
|
||||
|
||||
public void GetRoll(RollRequest rollType, RollResultCallback cb);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aade83941cf241b398049ff3fe0475cb
|
||||
timeCreated: 1703812608
|
||||
-375
@@ -1,375 +0,0 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Net.Eagle0.Shardok.Api;
|
||||
using Net.Eagle0.Shardok.Common;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityGoDiceInterface;
|
||||
|
||||
public class RollPanelController : MonoBehaviour, RollFetcher {
|
||||
public DiceConfigurationPanelController diceConfigurationPanelController;
|
||||
public Button skipContinueButton;
|
||||
public TMP_Text skipContinueButtonText;
|
||||
private DiceInterface _diceInterface;
|
||||
|
||||
public TMP_Text tensResultLabel;
|
||||
public TMP_Text onesResultLabel;
|
||||
|
||||
public TMP_Text header;
|
||||
public TMP_Text reconnectionLabel;
|
||||
public TMP_Text runningTotalLabel;
|
||||
|
||||
private RollFetcher.RollResultCallback _rollResultCallback;
|
||||
private RollRequest _rollRequest;
|
||||
|
||||
private string logLocation;
|
||||
|
||||
enum OpenEndedState { None, High, Low, RollComplete }
|
||||
private OpenEndedState _openEndedState = OpenEndedState.None;
|
||||
|
||||
private readonly int _d100OpenEndedHighThreshold = 96;
|
||||
private readonly int _d100OpenEndedLowThreshold = 5;
|
||||
|
||||
private DateTime _lastRollTime = DateTime.MinValue;
|
||||
private readonly TimeSpan _disconnectionDelay = TimeSpan.FromMinutes(5);
|
||||
private readonly TimeSpan _displayTime = TimeSpan.FromSeconds(3);
|
||||
private readonly TimeSpan _openEndedResetTime = TimeSpan.FromSeconds(2);
|
||||
|
||||
public void GetRoll(RollRequest rollRequest, RollFetcher.RollResultCallback cb) {
|
||||
// Check for dice enabled
|
||||
if (PlayerPrefs.GetInt(SettingsPanelController.UseGoDiceKey, 0) == 0) {
|
||||
cb(null);
|
||||
return;
|
||||
}
|
||||
|
||||
skipContinueButton.gameObject.SetActive(true);
|
||||
if (_diceInterface == null) { _diceInterface = GetComponentInParent<DiceInterface>(); }
|
||||
|
||||
_onesResult = null;
|
||||
_tensResult = null;
|
||||
_runningTotal = 0;
|
||||
_openEndedState = OpenEndedState.None;
|
||||
|
||||
skipContinueButtonText.text = "Skip";
|
||||
|
||||
_rollResultCallback = cb;
|
||||
_rollRequest = rollRequest;
|
||||
|
||||
SetResultLabels();
|
||||
|
||||
logLocation = NewLogLocation();
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(logLocation));
|
||||
|
||||
if (string.IsNullOrEmpty(_tensDieInfo.identifier) ||
|
||||
string.IsNullOrEmpty(_onesDieInfo.identifier)) {
|
||||
GetConfiguration();
|
||||
} else {
|
||||
_diceInterface.deviceFoundCallback = DeviceFoundCallback;
|
||||
_diceInterface.connectionCallback = ConnectionCallback;
|
||||
_diceInterface.rollCallback = RollCallback;
|
||||
_diceInterface.logger = LogFromNative;
|
||||
|
||||
// _diceInterface.StartListening();
|
||||
SetAlphas();
|
||||
|
||||
if (!_tensDieInfo.connected) { _diceInterface.Connect(_tensDieInfo.identifier); }
|
||||
if (!_onesDieInfo.connected) { _diceInterface.Connect(_onesDieInfo.identifier); }
|
||||
}
|
||||
|
||||
gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
private DieInfo _tensDieInfo;
|
||||
private DieInfo _onesDieInfo;
|
||||
|
||||
private int? _tensResult = null;
|
||||
private int? _onesResult = null;
|
||||
private int _runningTotal = 0;
|
||||
|
||||
private void SetAlphas() {
|
||||
tensResultLabel.alpha = _tensDieInfo.connected ? 1.0f : 0.25f;
|
||||
onesResultLabel.alpha = _onesDieInfo.connected ? 1.0f : 0.25f;
|
||||
}
|
||||
|
||||
void OnEnable() {}
|
||||
|
||||
private void GetConfiguration() {
|
||||
diceConfigurationPanelController.GetConfiguration(ConfigurationCallback);
|
||||
}
|
||||
|
||||
public void ConfigureClicked() { GetConfiguration(); }
|
||||
|
||||
public void SkipClicked() {
|
||||
gameObject.SetActive(false);
|
||||
_rollResultCallback(_runningTotal);
|
||||
_diceInterface.StopListening();
|
||||
}
|
||||
|
||||
void ConfigurationCallback(DieInfo? tensOpt, DieInfo? onesOpt) {
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
if (tensOpt is DieInfo tens && onesOpt is DieInfo ones) {
|
||||
gameObject.SetActive(true);
|
||||
_diceInterface.connectionCallback = ConnectionCallback;
|
||||
_diceInterface.connectionFailedCallback = ConnectionFailedCallback;
|
||||
_diceInterface.disconnectionCallback = DisconnectionCallback;
|
||||
_diceInterface.listenerStoppedCallback = ListenerStoppedCallback;
|
||||
_diceInterface.rollCallback = RollCallback;
|
||||
|
||||
_tensDieInfo = tens;
|
||||
_onesDieInfo = ones;
|
||||
|
||||
reconnectionLabel.gameObject.SetActive(false);
|
||||
|
||||
if (!_tensDieInfo.connected) _diceInterface.Connect(_tensDieInfo.identifier);
|
||||
if (!_onesDieInfo.connected) _diceInterface.Connect(_onesDieInfo.identifier);
|
||||
|
||||
SetAlphas();
|
||||
SetResultLabels();
|
||||
|
||||
tensResultLabel.color = UnityDieColors.ColorFromDieColor(_tensDieInfo.color);
|
||||
onesResultLabel.color = UnityDieColors.ColorFromDieColor(_onesDieInfo.color);
|
||||
} else {
|
||||
gameObject.SetActive(false);
|
||||
_rollResultCallback(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void DeviceFoundCallback(string identifier) {
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
if ((_tensDieInfo.identifier == identifier && !_tensDieInfo.connected) ||
|
||||
(_onesDieInfo.identifier == identifier && !_onesDieInfo.connected)) {
|
||||
_diceInterface.Connect(identifier);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void ConnectionCallback(string identifier, string deviceName) {
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
if (identifier == _tensDieInfo.identifier) {
|
||||
_tensDieInfo.connected = true;
|
||||
} else if (identifier == _onesDieInfo.identifier) {
|
||||
_onesDieInfo.connected = true;
|
||||
}
|
||||
|
||||
SetAlphas();
|
||||
|
||||
if (_tensDieInfo.connected && _onesDieInfo.connected) {
|
||||
reconnectionLabel.gameObject.SetActive(false);
|
||||
_diceInterface.StopListening();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void ConnectionFailedCallback(string identifier) {
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
if (identifier == _onesDieInfo.identifier || identifier == _tensDieInfo.identifier) {
|
||||
reconnectionLabel.gameObject.SetActive(true);
|
||||
_diceInterface.StartListening();
|
||||
} else {
|
||||
Debug.Log("Got connection failed for unknown identifier");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void DisconnectionCallback(string identifier) {
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
if (identifier == _tensDieInfo.identifier) {
|
||||
_tensDieInfo.connected = false;
|
||||
SetAlphas();
|
||||
_diceInterface.Connect(identifier);
|
||||
} else if (identifier == _onesDieInfo.identifier) {
|
||||
_onesDieInfo.connected = false;
|
||||
SetAlphas();
|
||||
_diceInterface.Connect(identifier);
|
||||
} else {
|
||||
Debug.Log("Got disconnection callback for unknown identifier");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void ListenerStoppedCallback() {
|
||||
MainQueue.Q.Enqueue(() => { reconnectionLabel.text = "Unable to connect."; });
|
||||
}
|
||||
|
||||
private readonly Dictionary<CommandType, string> _baseHeaderText = new() {
|
||||
{ CommandType.ArcheryCommand, "Archery Attack" },
|
||||
{ CommandType.BuildBridgeCommand, "Build Bridge" },
|
||||
{ CommandType.ChargeCommand, "Charge" },
|
||||
{ CommandType.BraveWaterCommand, "Brave Water" },
|
||||
{ CommandType.MeleeCommand, "Melee Attack" }
|
||||
};
|
||||
|
||||
private string BaseHeaderText(CommandType commandType) {
|
||||
if (_baseHeaderText.ContainsKey(commandType)) { return _baseHeaderText[commandType]; }
|
||||
|
||||
return "Action";
|
||||
}
|
||||
|
||||
void SetResultLabels() {
|
||||
var baseText = BaseHeaderText(_rollRequest.CommandType);
|
||||
string fullText = "";
|
||||
|
||||
if (_openEndedState == OpenEndedState.High) {
|
||||
fullText = $"You rolled OPEN ENDED HIGH! Keep rolling for {baseText}!";
|
||||
} else if (_openEndedState == OpenEndedState.Low) {
|
||||
fullText = $"You rolled OPEN ENDED LOW! Keep rolling for {baseText}!";
|
||||
} else {
|
||||
switch (_rollRequest.RollType) {
|
||||
case RollType.D100: fullText = $"Roll D100 for {baseText}"; break;
|
||||
case RollType.D100OpenEnded:
|
||||
fullText = $"Roll Open Ended D100 for {baseText}";
|
||||
break;
|
||||
case RollType.D100OpenEndedHigh:
|
||||
fullText = $"Roll Open Ended High D100 for {baseText}";
|
||||
break;
|
||||
case RollType.D100OpenEndedLow:
|
||||
fullText = $"Roll Open Ended Low D100 for {baseText}";
|
||||
break;
|
||||
case RollType.Unspecified:
|
||||
default: throw new ArgumentException("Invalid roll type");
|
||||
}
|
||||
}
|
||||
|
||||
if (_rollRequest.Odds is OddsView odds) {
|
||||
int minRoll = 101 - odds.SuccessChance;
|
||||
fullText += $" ({minRoll}+ to succeed)";
|
||||
}
|
||||
|
||||
header.text = fullText;
|
||||
|
||||
if (_tensResult is int tr) {
|
||||
tensResultLabel.text = tr.ToString();
|
||||
} else {
|
||||
tensResultLabel.text = "?";
|
||||
}
|
||||
|
||||
if (_onesResult is int or) {
|
||||
onesResultLabel.text = or.ToString();
|
||||
} else {
|
||||
onesResultLabel.text = "?";
|
||||
}
|
||||
|
||||
runningTotalLabel.text = $"Running total: {_runningTotal}";
|
||||
runningTotalLabel.gameObject.SetActive(_runningTotal != 0);
|
||||
}
|
||||
|
||||
void RollCallback(string identifier, sbyte x, sbyte y, sbyte z) {
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
if (gameObject.activeSelf) {
|
||||
var d10Roll = DiceVectors.D10Value(x, y, z);
|
||||
|
||||
if (identifier == _tensDieInfo.identifier) {
|
||||
if (_tensResult == null) { _tensResult = d10Roll; }
|
||||
} else if (identifier == _onesDieInfo.identifier) {
|
||||
if (_onesResult == null) { _onesResult = d10Roll; }
|
||||
}
|
||||
|
||||
SetResultLabels();
|
||||
CheckCompletion();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void HandleOneRoll(int newD100Result) {
|
||||
if (_openEndedState == OpenEndedState.Low) {
|
||||
_runningTotal -= newD100Result;
|
||||
} else {
|
||||
_runningTotal += newD100Result;
|
||||
}
|
||||
|
||||
// Rolls in the middle always end the open ended state.
|
||||
if (newD100Result > _d100OpenEndedLowThreshold &&
|
||||
newD100Result < _d100OpenEndedHighThreshold) {
|
||||
_openEndedState = OpenEndedState.RollComplete;
|
||||
return;
|
||||
}
|
||||
|
||||
// For a LOW roll, if we are currently in a None state, move to OpenEnded
|
||||
// Low so we can roll again, but if we were already there, this ends the roll.
|
||||
if (newD100Result <= _d100OpenEndedLowThreshold) {
|
||||
if ((_rollRequest.RollType == RollType.D100OpenEnded ||
|
||||
_rollRequest.RollType == RollType.D100OpenEndedLow) &&
|
||||
_openEndedState == OpenEndedState.None) {
|
||||
_openEndedState = OpenEndedState.Low;
|
||||
} else {
|
||||
_openEndedState = OpenEndedState.RollComplete;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// At this point we know the current roll is high. If we were already in OpenEndedLow,
|
||||
// stay there and keep subtracting, otherwise move into High.
|
||||
if (_openEndedState == OpenEndedState.Low) {
|
||||
_openEndedState = OpenEndedState.Low;
|
||||
} else {
|
||||
_openEndedState = OpenEndedState.High;
|
||||
}
|
||||
}
|
||||
|
||||
void CheckCompletion() {
|
||||
if (_onesResult is int ones && _tensResult is int tens) {
|
||||
// FIXME: open ended rolls
|
||||
var currentResult = tens * 10 + ones;
|
||||
|
||||
HandleOneRoll(currentResult);
|
||||
// TODO: flash the dice here for high rolls
|
||||
|
||||
if (_openEndedState == OpenEndedState.RollComplete) {
|
||||
skipContinueButtonText.text = "Dismiss";
|
||||
SetResultLabels();
|
||||
StartCoroutine(AfterDelay(_displayTime, () => {
|
||||
if (gameObject.activeSelf) {
|
||||
gameObject.SetActive(false);
|
||||
_rollResultCallback(_runningTotal);
|
||||
}
|
||||
}));
|
||||
|
||||
// Disconnect only after _disconnectionDelay time
|
||||
_lastRollTime = DateTime.Now;
|
||||
StartCoroutine(AfterDelay(_disconnectionDelay, () => {
|
||||
if (DateTime.Now - _lastRollTime > _disconnectionDelay) {
|
||||
_diceInterface.StopListening();
|
||||
|
||||
_tensDieInfo.connected = false;
|
||||
_onesDieInfo.connected = false;
|
||||
_diceInterface.Disconnect(_tensDieInfo.identifier);
|
||||
_diceInterface.Disconnect(_onesDieInfo.identifier);
|
||||
}
|
||||
}));
|
||||
} else {
|
||||
// We keep rolling, so reset the dice and keep going.
|
||||
StartCoroutine(AfterDelay(_openEndedResetTime, () => {
|
||||
_tensResult = null;
|
||||
_onesResult = null;
|
||||
SetResultLabels();
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator AfterDelay(TimeSpan timeSpan, Action action) {
|
||||
yield return new WaitForSeconds(timeSpan.Seconds);
|
||||
action();
|
||||
}
|
||||
|
||||
void LogFromNative(string log) {
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
Debug.Log(log);
|
||||
File.AppendAllText(logLocation, log);
|
||||
});
|
||||
}
|
||||
|
||||
string NewLogLocation() {
|
||||
return Path.Combine(
|
||||
Application.persistentDataPath,
|
||||
"eagle0",
|
||||
"Resources",
|
||||
"RollPanelLogs",
|
||||
DateTime.Now.ToString("yyyy-MM-dd-HH-mm-ss") + ".log");
|
||||
}
|
||||
}
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: db0669ab8b7684b2bb7eb30a0e66f5e8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,18 +0,0 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UnityGoDiceInterface {
|
||||
public class UnityDieColors {
|
||||
public static Color ColorFromDieColor(DieColor dieColor) {
|
||||
switch (dieColor) {
|
||||
case DieColor.DieColorBlack: return Color.black;
|
||||
case DieColor.DieColorRed: return Color.red;
|
||||
case DieColor.DieColorGreen: return Color.green;
|
||||
case DieColor.DieColorBlue: return Color.blue;
|
||||
case DieColor.DieColorYellow: return Color.yellow;
|
||||
case DieColor.DieColorOrange: return new Color(1.0f, 0.5f, 0.0f);
|
||||
default: throw new ArgumentOutOfRangeException(nameof(dieColor), dieColor, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d26c35650c67416e86ab5a26906cfb5a
|
||||
timeCreated: 1703173073
|
||||
+63
-113
@@ -86,25 +86,21 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
public GameObject oauthPanel;
|
||||
public Button discordLoginButton;
|
||||
public Button googleLoginButton;
|
||||
public Button githubLoginButton;
|
||||
public Button appleLoginButton;
|
||||
public Button steamLoginButton;
|
||||
public Button twitchLoginButton;
|
||||
public TextMeshProUGUI oauthStatusText;
|
||||
|
||||
[Header("Stored Accounts")]
|
||||
public GameObject storedAccountsContainer;
|
||||
public GameObject storedAccountButtonPrefab;
|
||||
public StoredAccountButton storedAccountButtonPrefab;
|
||||
public Sprite discordProviderIcon;
|
||||
public Sprite googleProviderIcon;
|
||||
|
||||
[Header("Display Name Setup")]
|
||||
public GameObject displayNamePanel;
|
||||
public TMP_InputField displayNameField;
|
||||
public Button setDisplayNameButton;
|
||||
public TextMeshProUGUI displayNameErrorText;
|
||||
|
||||
[Header("Invitation Code Entry")]
|
||||
public GameObject invitationCodePanel;
|
||||
public TMP_InputField invitationCodeField;
|
||||
public Button submitInvitationCodeButton;
|
||||
public TextMeshProUGUI invitationCodeErrorText;
|
||||
public Sprite githubProviderIcon;
|
||||
public Sprite appleProviderIcon;
|
||||
public Sprite steamProviderIcon;
|
||||
public Sprite twitchProviderIcon;
|
||||
|
||||
[Header("Status Display")]
|
||||
public TextMeshProUGUI connectionStatusText;
|
||||
@@ -114,6 +110,7 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
public TMP_Dropdown connectionEnvironmentDropdown;
|
||||
|
||||
public GameObject connectionPanel;
|
||||
public GameObject connectionBackgroundLayer;
|
||||
public GameObject gameSelectionPanel;
|
||||
public GameObject customBattlePanel;
|
||||
public ErrorHandler errorHandler;
|
||||
@@ -124,6 +121,7 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
public Button cancelCustomBattleButton;
|
||||
public TMP_Dropdown lobbyEnvironmentDropdown;
|
||||
public TextMeshProUGUI lobbyUserText;
|
||||
public TextMeshProUGUI lobbyStatusText;
|
||||
|
||||
public GameObject runningGamesListArea;
|
||||
public GameObject runningGamesListItemPrefab;
|
||||
@@ -150,14 +148,12 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
private CancellationTokenSource _cancellationTokenSource;
|
||||
private readonly Object pendingReplyLock = new Object();
|
||||
private int _connectedEnvironmentIndex = -1; // -1 means not connected
|
||||
private List<GameObject> _storedAccountButtons = new List<GameObject>();
|
||||
private List<StoredAccountButton> _storedAccountButtons = new();
|
||||
|
||||
public ClientPregeneratedText clientPregeneratedText;
|
||||
|
||||
const String BaseDomain = "eagle0.net";
|
||||
const String EnvironmentKey = "environmentKey";
|
||||
const String NameKey = "nameKey";
|
||||
const String PasswordKey = "passwordKey";
|
||||
|
||||
// Environment options: index 0 = prod., index 1 = qa., index 2 = (none)
|
||||
private static readonly List<string> EnvironmentOptions = new() { "prod.", "qa.", "" };
|
||||
@@ -213,6 +209,7 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
|
||||
connectionCanvas.enabled = true;
|
||||
connectionPanel.gameObject.SetActive(true);
|
||||
if (connectionBackgroundLayer != null) { connectionBackgroundLayer.SetActive(true); }
|
||||
gameSelectionPanel.gameObject.SetActive(false);
|
||||
customBattlePanel.gameObject.SetActive(false);
|
||||
|
||||
@@ -259,20 +256,24 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
if (googleLoginButton != null) {
|
||||
googleLoginButton.onClick.AddListener(() => OnOAuthLoginClicked(OAuthProvider.Google));
|
||||
}
|
||||
if (setDisplayNameButton != null) {
|
||||
setDisplayNameButton.onClick.AddListener(OnSetDisplayNameClicked);
|
||||
if (githubLoginButton != null) {
|
||||
githubLoginButton.onClick.AddListener(() => OnOAuthLoginClicked(OAuthProvider.Github));
|
||||
}
|
||||
if (submitInvitationCodeButton != null) {
|
||||
submitInvitationCodeButton.onClick.AddListener(OnSubmitInvitationCodeClicked);
|
||||
if (appleLoginButton != null) {
|
||||
appleLoginButton.onClick.AddListener(() => OnOAuthLoginClicked(OAuthProvider.Apple));
|
||||
}
|
||||
if (steamLoginButton != null) {
|
||||
steamLoginButton.onClick.AddListener(() => OnOAuthLoginClicked(OAuthProvider.Steam));
|
||||
}
|
||||
if (twitchLoginButton != null) {
|
||||
twitchLoginButton.onClick.AddListener(() => OnOAuthLoginClicked(OAuthProvider.Twitch));
|
||||
}
|
||||
|
||||
// Subscribe to OAuthManager events
|
||||
if (OAuthManager.Instance != null) {
|
||||
OAuthManager.Instance.OnLoginSuccess += OnOAuthLoginSuccess;
|
||||
OAuthManager.Instance.OnLoginFailed += OnOAuthLoginFailed;
|
||||
OAuthManager.Instance.OnNewUserNeedsDisplayName += OnNewUserNeedsDisplayName;
|
||||
OAuthManager.Instance.OnLogout += OnOAuthLogout;
|
||||
OAuthManager.Instance.OnInvitationRequired += OnInvitationRequired;
|
||||
}
|
||||
|
||||
// Show auth panel with stored accounts
|
||||
@@ -280,10 +281,6 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
}
|
||||
|
||||
private void ShowAuthPanel() {
|
||||
// Hide other panels
|
||||
if (displayNamePanel != null) displayNamePanel.SetActive(false);
|
||||
if (invitationCodePanel != null) invitationCodePanel.SetActive(false);
|
||||
|
||||
// Show OAuth panel
|
||||
if (oauthPanel != null) oauthPanel.SetActive(true);
|
||||
|
||||
@@ -301,9 +298,13 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
}
|
||||
|
||||
private void RefreshStoredAccountButtons() {
|
||||
// Clear existing buttons
|
||||
foreach (var btn in _storedAccountButtons) {
|
||||
if (btn != null) Destroy(btn);
|
||||
// Clear ALL children of the container
|
||||
// Disable immediately (Destroy is deferred to end of frame)
|
||||
if (storedAccountsContainer != null) {
|
||||
foreach (Transform child in storedAccountsContainer.transform) {
|
||||
child.gameObject.SetActive(false);
|
||||
Destroy(child.gameObject);
|
||||
}
|
||||
}
|
||||
_storedAccountButtons.Clear();
|
||||
|
||||
@@ -320,26 +321,33 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
buttonObj.transform.localScale = Vector3.one;
|
||||
|
||||
// Set button text (just display name, icon shows provider)
|
||||
var buttonText = buttonObj.GetComponentInChildren<TextMeshProUGUI>();
|
||||
if (buttonText != null) { buttonText.text = account.DisplayName; }
|
||||
buttonObj.label.text = account.DisplayName;
|
||||
|
||||
// Set provider icon (look for child named "ProviderIcon")
|
||||
var providerIconTransform = buttonObj.transform.Find("ProviderIcon");
|
||||
if (providerIconTransform != null) {
|
||||
var providerImage = providerIconTransform.GetComponent<Image>();
|
||||
if (providerImage != null) {
|
||||
var isGoogle = account.Provider.ToLowerInvariant() == "google";
|
||||
providerImage.sprite = isGoogle ? googleProviderIcon : discordProviderIcon;
|
||||
}
|
||||
var providerImage = buttonObj.icon;
|
||||
if (providerImage != null) {
|
||||
var providerLower = account.Provider.ToLowerInvariant();
|
||||
providerImage.sprite = providerLower switch { "google" => googleProviderIcon,
|
||||
"github" => githubProviderIcon,
|
||||
"apple" => appleProviderIcon,
|
||||
"steam" => steamProviderIcon,
|
||||
"twitch" => twitchProviderIcon,
|
||||
_ => discordProviderIcon };
|
||||
}
|
||||
|
||||
// Set click handler
|
||||
// Set click handler for sign-in
|
||||
var button = buttonObj.GetComponent<Button>();
|
||||
if (button != null) {
|
||||
var accountCopy = account; // Capture for lambda
|
||||
button.onClick.AddListener(() => OnStoredAccountClicked(accountCopy));
|
||||
}
|
||||
|
||||
// Set click handler for delete button
|
||||
if (buttonObj.deleteButton != null) {
|
||||
var accountCopy = account; // Capture for lambda
|
||||
buttonObj.deleteButton.onClick.AddListener(
|
||||
() => OnDeleteStoredAccount(accountCopy));
|
||||
}
|
||||
|
||||
_storedAccountButtons.Add(buttonObj);
|
||||
}
|
||||
}
|
||||
@@ -361,8 +369,13 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDeleteStoredAccount(StoredAccount account) {
|
||||
Debug.Log($"[ConnectionHandler] Deleting stored account: {account.DisplayName}");
|
||||
OAuthManager.Instance?.RemoveStoredAccount(account);
|
||||
RefreshStoredAccountButtons();
|
||||
}
|
||||
|
||||
private void SetupLobbyUI() {
|
||||
if (logoutButton != null) { logoutButton.onClick.AddListener(OnLogoutClicked); }
|
||||
if (customBattleButton != null) { customBattleButton.onClick.AddListener(CustomBattle); }
|
||||
if (cancelCustomBattleButton != null) {
|
||||
cancelCustomBattleButton.onClick.AddListener(CancelCustomBattle);
|
||||
@@ -413,13 +426,16 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
foreach (Transform row in runningGamesListArea.transform) { Destroy(row.gameObject); }
|
||||
foreach (Transform row in availableGamesListArea.transform) { Destroy(row.gameObject); }
|
||||
|
||||
// Show loading indicator
|
||||
if (lobbyStatusText != null) { lobbyStatusText.text = "Switching environment..."; }
|
||||
|
||||
// Reconnect to new environment
|
||||
_createConnection();
|
||||
RequestMaps();
|
||||
StartListeningForLobbyUpdates();
|
||||
}
|
||||
|
||||
private async void OnLogoutClicked() {
|
||||
public async void OnLogoutClicked() {
|
||||
Debug.Log("[ConnectionHandler] Logout button clicked");
|
||||
|
||||
// Clear OAuth tokens
|
||||
@@ -440,6 +456,7 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
// Return to connection screen
|
||||
gameSelectionPanel.SetActive(false);
|
||||
connectionPanel.SetActive(true);
|
||||
if (connectionBackgroundLayer != null) { connectionBackgroundLayer.SetActive(true); }
|
||||
|
||||
// Re-initialize the auth UI
|
||||
ShowAuthPanel();
|
||||
@@ -493,77 +510,6 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
});
|
||||
}
|
||||
|
||||
private void OnNewUserNeedsDisplayName(bool isNewUser) {
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
// Show display name panel
|
||||
if (oauthPanel != null) oauthPanel.SetActive(false);
|
||||
if (invitationCodePanel != null) invitationCodePanel.SetActive(false);
|
||||
if (displayNamePanel != null) displayNamePanel.SetActive(true);
|
||||
if (displayNameErrorText != null) displayNameErrorText.text = "";
|
||||
});
|
||||
}
|
||||
|
||||
private void OnInvitationRequired() {
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
// Show invitation code entry panel
|
||||
if (oauthPanel != null) oauthPanel.SetActive(false);
|
||||
if (displayNamePanel != null) displayNamePanel.SetActive(false);
|
||||
if (invitationCodePanel != null) invitationCodePanel.SetActive(true);
|
||||
if (invitationCodeErrorText != null) {
|
||||
invitationCodeErrorText.text =
|
||||
"An invitation code is required to create a new account.";
|
||||
}
|
||||
if (invitationCodeField != null) invitationCodeField.text = "";
|
||||
});
|
||||
}
|
||||
|
||||
private void OnSubmitInvitationCodeClicked() {
|
||||
if (invitationCodeField == null) return;
|
||||
|
||||
var code = invitationCodeField.text.Trim();
|
||||
if (string.IsNullOrEmpty(code)) {
|
||||
if (invitationCodeErrorText != null) {
|
||||
invitationCodeErrorText.text = "Please enter an invitation code";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Save the code and return to login screen to retry
|
||||
InvitationCodeManager.SetInvitationCode(code);
|
||||
|
||||
if (invitationCodeErrorText != null) {
|
||||
invitationCodeErrorText.text = "Code saved. Please sign in again.";
|
||||
}
|
||||
|
||||
// Return to auth panel after a short delay
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
if (oauthStatusText != null) {
|
||||
oauthStatusText.text = "Invitation code saved. Please sign in to continue.";
|
||||
}
|
||||
ShowAuthPanel();
|
||||
});
|
||||
}
|
||||
|
||||
private async void OnSetDisplayNameClicked() {
|
||||
if (displayNameField == null || OAuthManager.Instance == null) return;
|
||||
|
||||
var displayName = displayNameField.text.Trim();
|
||||
if (string.IsNullOrEmpty(displayName)) {
|
||||
if (displayNameErrorText != null) {
|
||||
displayNameErrorText.text = "Please enter a display name";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (displayNameErrorText != null) { displayNameErrorText.text = "Setting display name..."; }
|
||||
|
||||
var success = await OAuthManager.Instance.SetDisplayNameAsync(displayName);
|
||||
if (!success && displayNameErrorText != null) {
|
||||
displayNameErrorText.text = "Name already taken or invalid. Try another.";
|
||||
}
|
||||
// OnLoginSuccess will be called if successful
|
||||
}
|
||||
|
||||
private void OnOAuthLogout() {
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
ShowAuthPanel();
|
||||
@@ -700,6 +646,7 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
fetchedNewGameLeaders.Select(a => a.ImagePath));
|
||||
|
||||
connectionPanel.gameObject.SetActive(false);
|
||||
if (connectionBackgroundLayer != null) { connectionBackgroundLayer.SetActive(false); }
|
||||
gameSelectionPanel.gameObject.SetActive(true);
|
||||
|
||||
// Update lobby status displays
|
||||
@@ -766,6 +713,9 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
availableGameItem.JoinCallback = this.JoinGame;
|
||||
availableGameItem.DropCallback = this.DropGame;
|
||||
}
|
||||
|
||||
// Clear loading indicator now that lobby is populated
|
||||
if (lobbyStatusText != null) { lobbyStatusText.text = ""; }
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+1
-4
@@ -20,8 +20,6 @@ using Random = System.Random;
|
||||
using VictoryCondition = Net.Eagle0.Common.VictoryCondition;
|
||||
|
||||
public class CustomBattleHandler : MonoBehaviour, IClientConnectionSubscriber {
|
||||
public RollPanelController rollPanelController;
|
||||
|
||||
public GameObject yourArmiesArea;
|
||||
public GameObject aiArmiesArea;
|
||||
public Canvas shardokCanvas;
|
||||
@@ -103,8 +101,7 @@ public class CustomBattleHandler : MonoBehaviour, IClientConnectionSubscriber {
|
||||
players: players,
|
||||
heroNameTextIds: _heroNames,
|
||||
heroImages: _heroImages,
|
||||
battalionNames: new Dictionary<int, string> { { 0, "no name" } },
|
||||
rollFetcher: rollPanelController);
|
||||
battalionNames: new Dictionary<int, string> { { 0, "no name" } });
|
||||
|
||||
shardokCanvas.gameObject.SetActive(true);
|
||||
shardokCanvas.GetComponent<ShardokGameController>().SetUpGame(_shardokModel);
|
||||
|
||||
+4
-4
@@ -602,7 +602,7 @@ MonoBehaviour:
|
||||
m_SelectOnRight: {fileID: 0}
|
||||
m_Transition: 1
|
||||
m_Colors:
|
||||
m_NormalColor: {r: 1, g: 0.42745098, b: 0.42745098, a: 1}
|
||||
m_NormalColor: {r: 1, g: 1, b: 1, a: 0}
|
||||
m_HighlightedColor: {r: 0.84313726, g: 0.84313726, b: 0.84313726, a: 1}
|
||||
m_PressedColor: {r: 0, g: 0, b: 0, a: 1}
|
||||
m_SelectedColor: {r: 0.84313726, g: 0.84313726, b: 0.84313726, a: 1}
|
||||
@@ -1151,7 +1151,7 @@ MonoBehaviour:
|
||||
m_Right: 0
|
||||
m_Top: 0
|
||||
m_Bottom: 0
|
||||
m_ChildAlignment: 0
|
||||
m_ChildAlignment: 3
|
||||
m_Spacing: 0
|
||||
m_ChildForceExpandWidth: 0
|
||||
m_ChildForceExpandHeight: 0
|
||||
@@ -1256,14 +1256,14 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.RawImage
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 0, g: 0, b: 0, a: 1}
|
||||
m_Color: {r: 1, g: 0, b: 0, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Texture: {fileID: 2800000, guid: 8c998213d53883d4b8fdf7a3eb3c2909, type: 3}
|
||||
m_Texture: {fileID: 2800000, guid: 88dca061d041a2a4596563e6a38f66c8, type: 3}
|
||||
m_UVRect:
|
||||
serializedVersion: 2
|
||||
x: 0
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
public class StoredAccountButton : MonoBehaviour {
|
||||
public TMP_Text label;
|
||||
public Image icon;
|
||||
public Button deleteButton;
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c2ab265dfad44a1aac98c4eaba3969bc
|
||||
timeCreated: 1768606277
|
||||
+368
-29
@@ -1,5 +1,62 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!1 &602258008028479859
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 8558900591190483184}
|
||||
- component: {fileID: 5225486432737178588}
|
||||
m_Layer: 5
|
||||
m_Name: ProviderIconContainer
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &8558900591190483184
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 602258008028479859}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 6242087783640990751}
|
||||
m_Father: {fileID: 157686408557551303}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &5225486432737178588
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 602258008028479859}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
|
||||
m_IgnoreLayout: 0
|
||||
m_MinWidth: -1
|
||||
m_MinHeight: -1
|
||||
m_PreferredWidth: 80
|
||||
m_PreferredHeight: 50
|
||||
m_FlexibleWidth: -1
|
||||
m_FlexibleHeight: -1
|
||||
m_LayoutPriority: 1
|
||||
--- !u!1 &1262787222422545696
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -10,7 +67,6 @@ GameObject:
|
||||
m_Component:
|
||||
- component: {fileID: 6242087783640990751}
|
||||
- component: {fileID: 5444015614922696687}
|
||||
- component: {fileID: 8871991993173924918}
|
||||
- component: {fileID: 6965305925514130928}
|
||||
m_Layer: 5
|
||||
m_Name: ProviderIcon
|
||||
@@ -26,17 +82,17 @@ RectTransform:
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1262787222422545696}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 157686408557551303}
|
||||
m_Father: {fileID: 8558900591190483184}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: -10, y: -10}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &5444015614922696687
|
||||
CanvasRenderer:
|
||||
@@ -46,26 +102,6 @@ CanvasRenderer:
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1262787222422545696}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &8871991993173924918
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1262787222422545696}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
|
||||
m_IgnoreLayout: 0
|
||||
m_MinWidth: -1
|
||||
m_MinHeight: -1
|
||||
m_PreferredWidth: 80
|
||||
m_PreferredHeight: 50
|
||||
m_FlexibleWidth: -1
|
||||
m_FlexibleHeight: -1
|
||||
m_LayoutPriority: 1
|
||||
--- !u!114 &6965305925514130928
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -86,7 +122,7 @@ MonoBehaviour:
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 21300000, guid: b6b68c8c37d5cf1419267a80c426ac83, type: 3}
|
||||
m_Sprite: {fileID: 21300000, guid: af9f637b3a1f6433f819a0bcdb326453, type: 3}
|
||||
m_Type: 0
|
||||
m_PreserveAspect: 1
|
||||
m_FillCenter: 1
|
||||
@@ -96,6 +132,62 @@ MonoBehaviour:
|
||||
m_FillOrigin: 0
|
||||
m_UseSpriteMesh: 0
|
||||
m_PixelsPerUnitMultiplier: 1
|
||||
--- !u!1 &4773346950019690658
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 9042872047974783334}
|
||||
- component: {fileID: 1446804990396579382}
|
||||
m_Layer: 5
|
||||
m_Name: Spacer
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &9042872047974783334
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 4773346950019690658}
|
||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 157686408557551303}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1446804990396579382
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 4773346950019690658}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
|
||||
m_IgnoreLayout: 0
|
||||
m_MinWidth: -1
|
||||
m_MinHeight: -1
|
||||
m_PreferredWidth: 5
|
||||
m_PreferredHeight: -1
|
||||
m_FlexibleWidth: -1
|
||||
m_FlexibleHeight: -1
|
||||
m_LayoutPriority: 1
|
||||
--- !u!1 &5166012670666351313
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -110,6 +202,7 @@ GameObject:
|
||||
- component: {fileID: 646268409010340088}
|
||||
- component: {fileID: 3097176747511135857}
|
||||
- component: {fileID: 7691398338434978342}
|
||||
- component: {fileID: 369281190448318618}
|
||||
m_Layer: 5
|
||||
m_Name: StoredAccountButton
|
||||
m_TagString: Untagged
|
||||
@@ -130,13 +223,15 @@ RectTransform:
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 7974514965529143181}
|
||||
- {fileID: 6242087783640990751}
|
||||
- {fileID: 8558900591190483184}
|
||||
- {fileID: 1544713289774970384}
|
||||
- {fileID: 9042872047974783334}
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 400, y: 60}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &3634401029247236684
|
||||
CanvasRenderer:
|
||||
@@ -258,7 +353,7 @@ MonoBehaviour:
|
||||
m_Top: 0
|
||||
m_Bottom: 0
|
||||
m_ChildAlignment: 5
|
||||
m_Spacing: 0
|
||||
m_Spacing: 5
|
||||
m_ChildForceExpandWidth: 0
|
||||
m_ChildForceExpandHeight: 0
|
||||
m_ChildControlWidth: 1
|
||||
@@ -266,6 +361,21 @@ MonoBehaviour:
|
||||
m_ChildScaleWidth: 0
|
||||
m_ChildScaleHeight: 0
|
||||
m_ReverseArrangement: 0
|
||||
--- !u!114 &369281190448318618
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5166012670666351313}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: c2ab265dfad44a1aac98c4eaba3969bc, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::StoredAccountButton
|
||||
label: {fileID: 6210669843299902677}
|
||||
icon: {fileID: 6965305925514130928}
|
||||
deleteButton: {fileID: 5102831562105438405}
|
||||
--- !u!1 &5617793748733146591
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -425,3 +535,232 @@ MonoBehaviour:
|
||||
m_FlexibleWidth: 1
|
||||
m_FlexibleHeight: -1
|
||||
m_LayoutPriority: 1
|
||||
--- !u!1 &5969634697112825658
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 3505386829136548087}
|
||||
- component: {fileID: 4500436679700087771}
|
||||
- component: {fileID: 5081311949443699076}
|
||||
m_Layer: 5
|
||||
m_Name: Image
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &3505386829136548087
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5969634697112825658}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 1544713289774970384}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &4500436679700087771
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5969634697112825658}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &5081311949443699076
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5969634697112825658}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 0, b: 0, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 21300002, guid: 88dca061d041a2a4596563e6a38f66c8, type: 3}
|
||||
m_Type: 0
|
||||
m_PreserveAspect: 1
|
||||
m_FillCenter: 1
|
||||
m_FillMethod: 4
|
||||
m_FillAmount: 1
|
||||
m_FillClockwise: 1
|
||||
m_FillOrigin: 0
|
||||
m_UseSpriteMesh: 0
|
||||
m_PixelsPerUnitMultiplier: 1
|
||||
--- !u!1 &7624557558758837467
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 1544713289774970384}
|
||||
- component: {fileID: 1437169994306891023}
|
||||
- component: {fileID: 4243156207255156076}
|
||||
- component: {fileID: 5102831562105438405}
|
||||
- component: {fileID: 7366748879155754635}
|
||||
m_Layer: 5
|
||||
m_Name: Delete
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &1544713289774970384
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7624557558758837467}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 3505386829136548087}
|
||||
m_Father: {fileID: 157686408557551303}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &1437169994306891023
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7624557558758837467}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &4243156207255156076
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7624557558758837467}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 0}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 0}
|
||||
m_Type: 1
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
m_FillMethod: 4
|
||||
m_FillAmount: 1
|
||||
m_FillClockwise: 1
|
||||
m_FillOrigin: 0
|
||||
m_UseSpriteMesh: 0
|
||||
m_PixelsPerUnitMultiplier: 1
|
||||
--- !u!114 &5102831562105438405
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7624557558758837467}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Button
|
||||
m_Navigation:
|
||||
m_Mode: 3
|
||||
m_WrapAround: 0
|
||||
m_SelectOnUp: {fileID: 0}
|
||||
m_SelectOnDown: {fileID: 0}
|
||||
m_SelectOnLeft: {fileID: 0}
|
||||
m_SelectOnRight: {fileID: 0}
|
||||
m_Transition: 1
|
||||
m_Colors:
|
||||
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
|
||||
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
|
||||
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
|
||||
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
|
||||
m_ColorMultiplier: 1
|
||||
m_FadeDuration: 0.1
|
||||
m_SpriteState:
|
||||
m_HighlightedSprite: {fileID: 0}
|
||||
m_PressedSprite: {fileID: 0}
|
||||
m_SelectedSprite: {fileID: 0}
|
||||
m_DisabledSprite: {fileID: 0}
|
||||
m_AnimationTriggers:
|
||||
m_NormalTrigger: Normal
|
||||
m_HighlightedTrigger: Highlighted
|
||||
m_PressedTrigger: Pressed
|
||||
m_SelectedTrigger: Selected
|
||||
m_DisabledTrigger: Disabled
|
||||
m_Interactable: 1
|
||||
m_TargetGraphic: {fileID: 4243156207255156076}
|
||||
m_OnClick:
|
||||
m_PersistentCalls:
|
||||
m_Calls:
|
||||
- m_Target: {fileID: 5166012670666351313}
|
||||
m_TargetAssemblyTypeName:
|
||||
m_MethodName:
|
||||
m_Mode: 1
|
||||
m_Arguments:
|
||||
m_ObjectArgument: {fileID: 0}
|
||||
m_ObjectArgumentAssemblyTypeName:
|
||||
m_IntArgument: 0
|
||||
m_FloatArgument: 0
|
||||
m_StringArgument:
|
||||
m_BoolArgument: 0
|
||||
m_CallState: 2
|
||||
--- !u!114 &7366748879155754635
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7624557558758837467}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
|
||||
m_IgnoreLayout: 0
|
||||
m_MinWidth: -1
|
||||
m_MinHeight: -1
|
||||
m_PreferredWidth: 40
|
||||
m_PreferredHeight: 40
|
||||
m_FlexibleWidth: -1
|
||||
m_FlexibleHeight: -1
|
||||
m_LayoutPriority: 1
|
||||
|
||||
+4
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Eagle0.Tutorial;
|
||||
using Net.Eagle0.Eagle.Api;
|
||||
using Net.Eagle0.Eagle.Views;
|
||||
using UnityEngine;
|
||||
@@ -88,6 +89,9 @@ namespace eagle {
|
||||
_model = model;
|
||||
_availableCommand = ac;
|
||||
SetUpUI();
|
||||
|
||||
// Trigger tutorial for this command type (fires every time command is selected)
|
||||
TutorialManager.Instance?.TriggerRegistry?.OnCommandPanelShown(CommandType);
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
+13
-11
@@ -22,8 +22,6 @@ namespace eagle {
|
||||
public class EagleGameController : MonoBehaviour {
|
||||
public SoundManager soundManager;
|
||||
|
||||
public RollPanelController rollPanelController;
|
||||
|
||||
public TextMeshProUGUI roundStatusLabel;
|
||||
public TextMeshProUGUI connectionStatusLabel;
|
||||
private bool _connectionStatusUIInitialized = false;
|
||||
@@ -248,11 +246,7 @@ namespace eagle {
|
||||
int? playerId,
|
||||
PersistentClientConnection persistentClientConnection,
|
||||
string environmentName = null) {
|
||||
ModelUpdater = new GameModelUpdater(
|
||||
gameId,
|
||||
playerId,
|
||||
persistentClientConnection,
|
||||
rollPanelController) {
|
||||
ModelUpdater = new GameModelUpdater(gameId, playerId, persistentClientConnection) {
|
||||
UpdateAction = ModelUpdated,
|
||||
NoteRecipient = (title, text, llmId, pids, displayedHeroes) =>
|
||||
notificationPanel.AddNote(title, text, llmId, pids, displayedHeroes)
|
||||
@@ -284,11 +278,13 @@ namespace eagle {
|
||||
MainQueue.Q.EnqueueForNextUpdate(
|
||||
() => { _ = ModelUpdater.StartListeningForUpdates(); });
|
||||
|
||||
// Initialize tutorial system
|
||||
// Initialize tutorial system (onboarding starts after first model update in SwapModel)
|
||||
TutorialManager.Instance?.Initialize(this, null);
|
||||
if (TutorialManager.Instance != null &&
|
||||
!TutorialManager.Instance.State.OnboardingCompleted) {
|
||||
TutorialManager.Instance.StartOnboarding();
|
||||
// Register command panel for tutorial positioning
|
||||
if (TutorialManager.Instance?.TargetRegistry != null && commandPanel != null) {
|
||||
TutorialManager.Instance.TargetRegistry.RegisterTarget(
|
||||
"CommandPanel",
|
||||
commandPanel.GetComponent<RectTransform>());
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
@@ -507,6 +503,12 @@ namespace eagle {
|
||||
|
||||
if (Model == null) { return; }
|
||||
|
||||
// Start onboarding after first model update (UI panels are now populated)
|
||||
if (oldModel == null && TutorialManager.Instance != null &&
|
||||
!TutorialManager.Instance.State.OnboardingCompleted) {
|
||||
TutorialManager.Instance.StartOnboarding();
|
||||
}
|
||||
|
||||
PrefetchHeadshots(Model);
|
||||
|
||||
SetMusic();
|
||||
|
||||
@@ -13,7 +13,6 @@ using Net.Eagle0.Eagle.Views;
|
||||
using Net.Eagle0.Shardok.Common;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityGoDiceInterface;
|
||||
using Logger = common.Logger;
|
||||
using Notification = eagle.Notifications.Notification;
|
||||
|
||||
@@ -58,8 +57,6 @@ namespace eagle {
|
||||
public List<ProvinceId> ProvincesForFaction(FactionId factionId);
|
||||
|
||||
public List<ChronicleEntry> ChronicleEntries { get; }
|
||||
|
||||
RollFetcher RollFetcher { get; }
|
||||
}
|
||||
|
||||
public class GameModelUpdater : IClientConnectionSubscriber, IGameStateProvider {
|
||||
@@ -150,8 +147,6 @@ namespace eagle {
|
||||
private readonly ConcurrentDictionary<string, int> _shardokResultCounts =
|
||||
new ConcurrentDictionary<string, int>();
|
||||
|
||||
private readonly RollFetcher _rollFetcher;
|
||||
|
||||
// State synced with server
|
||||
private struct ConcreteGameModel : IGameModel {
|
||||
public PlayerId? PlayerId { get; set; }
|
||||
@@ -220,8 +215,6 @@ namespace eagle {
|
||||
var faction = MaybeDestroyedFaction(factionId);
|
||||
textUpdater.SetFactionText(textComponent, faction, this, template, fallbackText);
|
||||
}
|
||||
|
||||
public RollFetcher RollFetcher { get; set; }
|
||||
}
|
||||
|
||||
private ConcreteGameModel _currentModel;
|
||||
@@ -229,11 +222,9 @@ namespace eagle {
|
||||
public GameModelUpdater(
|
||||
GameId eagleGameId,
|
||||
FactionId? factionId,
|
||||
PersistentClientConnection pers,
|
||||
RollFetcher rollFetcher) {
|
||||
PersistentClientConnection pers) {
|
||||
GameId = eagleGameId;
|
||||
PersistentConnection = pers;
|
||||
this._rollFetcher = rollFetcher;
|
||||
|
||||
_currentModel.LastPostedToken = -1;
|
||||
_currentModel.PlayerId = factionId;
|
||||
@@ -245,8 +236,6 @@ namespace eagle {
|
||||
new ConcurrentDictionary<ShardokGameId, ShardokGameModel>();
|
||||
|
||||
_currentModel.BattalionTypes = new Dictionary<BattalionTypeId, BattalionType>();
|
||||
|
||||
_currentModel.RollFetcher = rollFetcher;
|
||||
}
|
||||
|
||||
private ShardokGameModel MakeGameModel(ShardokGameId shardokGameId) {
|
||||
@@ -294,8 +283,7 @@ namespace eagle {
|
||||
kv => kv.Value.ImagePath),
|
||||
battalionNames: _currentModel.BattalionNames.ToDictionary(
|
||||
kv => kv.Key,
|
||||
kv => kv.Value),
|
||||
rollFetcher: _rollFetcher);
|
||||
kv => kv.Value));
|
||||
|
||||
return shardokModel;
|
||||
}
|
||||
|
||||
+20
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using common;
|
||||
using Eagle0.Tutorial;
|
||||
using Net.Eagle0.Eagle.Views;
|
||||
using UnityEngine;
|
||||
|
||||
@@ -136,6 +137,25 @@ namespace eagle {
|
||||
row.Hero = hero;
|
||||
SetHeroRowSelections(row, hero);
|
||||
});
|
||||
|
||||
// Register hero rows with tutorial system for highlighting
|
||||
RegisterHeroRowsForTutorial();
|
||||
}
|
||||
|
||||
private void RegisterHeroRowsForTutorial() {
|
||||
var registry = TutorialManager.Instance?.TargetRegistry;
|
||||
if (registry == null) return;
|
||||
|
||||
// Register first row as WarlordRow (index 0)
|
||||
var warlordRow = heroesTable.GetRowRectTransform(0);
|
||||
if (warlordRow != null) { registry.RegisterTarget("WarlordRow", warlordRow); }
|
||||
|
||||
// Register rows 1 and 2 as VassalRows
|
||||
var vassalRow1 = heroesTable.GetRowRectTransform(1);
|
||||
if (vassalRow1 != null) { registry.RegisterTarget("VassalRow1", vassalRow1); }
|
||||
|
||||
var vassalRow2 = heroesTable.GetRowRectTransform(2);
|
||||
if (vassalRow2 != null) { registry.RegisterTarget("VassalRow2", vassalRow2); }
|
||||
}
|
||||
|
||||
private void SetUpBattalionsTable() {
|
||||
|
||||
LFS
BIN
Binary file not shown.
BIN
Binary file not shown.
+1
-1
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8d524cd6fa08d47c99b2462c2686b8d0
|
||||
guid: 2bbeb692cb0204e0c965a4f08dfeffce
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1e69ea88b87d34a48bbb45a1fbad81fc
|
||||
guid: 68f43a341139b445688f2326a98d5567
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
LFS
BIN
Binary file not shown.
+9
-9
@@ -1,12 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1fb1f6deb68f9475281c9f7c427a7024
|
||||
guid: 498227fe7963d42e0b240ce5c37f3665
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 1
|
||||
enableMipMap: 0
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
@@ -37,13 +37,13 @@ TextureImporter:
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 0
|
||||
wrapV: 0
|
||||
wrapU: 1
|
||||
wrapV: 1
|
||||
wrapW: 0
|
||||
nPOTScale: 1
|
||||
nPOTScale: 0
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteMode: 1
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
@@ -52,9 +52,9 @@ TextureImporter:
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 0
|
||||
alphaIsTransparency: 1
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 0
|
||||
textureType: 8
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
@@ -100,7 +100,7 @@ TextureImporter:
|
||||
customData:
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
spriteID: 5e97eb03825dee720800000000000000
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
BIN
Binary file not shown.
+9
-9
@@ -1,12 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1850a7332b6e84e1a9b69f5f5b4cf482
|
||||
guid: da40d75c7a6fb481d9d2cf483c71ae3a
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 1
|
||||
enableMipMap: 0
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
@@ -37,13 +37,13 @@ TextureImporter:
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 0
|
||||
wrapV: 0
|
||||
wrapU: 1
|
||||
wrapV: 1
|
||||
wrapW: 0
|
||||
nPOTScale: 1
|
||||
nPOTScale: 0
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteMode: 1
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
@@ -52,9 +52,9 @@ TextureImporter:
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 0
|
||||
alphaIsTransparency: 1
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 0
|
||||
textureType: 8
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
@@ -100,7 +100,7 @@ TextureImporter:
|
||||
customData:
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
spriteID: 5e97eb03825dee720800000000000000
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
LFS
BIN
Binary file not shown.
+117
@@ -0,0 +1,117 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 564fa5e6e47304264b8d471e1b6f81a8
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 0
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 1
|
||||
wrapV: 1
|
||||
wrapW: 0
|
||||
nPOTScale: 0
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 1
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 1
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 8
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 0
|
||||
swizzle: 50462976
|
||||
cookieLightType: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 4
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
customData:
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID: 5e97eb03825dee720800000000000000
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spriteCustomMetadata:
|
||||
entries: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
LFS
BIN
Binary file not shown.
src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Eagle/OAuth Buttons/web_neutral_sq_ctn@4x.png
LFS
BIN
Binary file not shown.
+30
-7
@@ -810,12 +810,19 @@ namespace eagle {
|
||||
await Task.WhenAny(actionTask, timeoutTask).ConfigureAwait(false);
|
||||
|
||||
if (completedTask == timeoutTask) {
|
||||
// WriteAsync timed out - connection is likely dead
|
||||
// WriteAsync timed out - connection is dead
|
||||
LogConnectionEvent(
|
||||
"write_timeout",
|
||||
$"WriteAsync timed out after {WriteAsyncTimeoutMs}ms");
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"[WRITE_TIMEOUT] WriteAsync timed out after {WriteAsyncTimeoutMs}ms, connection may be dead");
|
||||
$"[WRITE_TIMEOUT] WriteAsync timed out after {WriteAsyncTimeoutMs}ms, triggering reconnect");
|
||||
|
||||
// Dispose the dead connection and schedule reconnect
|
||||
lock (this) {
|
||||
_streamingCall?.Dispose();
|
||||
_streamingCall = null;
|
||||
}
|
||||
ScheduleReconnect("WriteAsyncTimeout");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -823,15 +830,31 @@ namespace eagle {
|
||||
timeoutCts.Cancel();
|
||||
return await actionTask.ConfigureAwait(false);
|
||||
} catch (RpcException e) {
|
||||
if (e.StatusCode == StatusCode.Cancelled) {
|
||||
// This is expected when the connection is closed.
|
||||
return false;
|
||||
} else {
|
||||
throw;
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"[WRITE_ERROR] RpcException: {e.StatusCode} - {e.Message}");
|
||||
// Connection is broken - dispose and reconnect
|
||||
lock (this) {
|
||||
_streamingCall?.Dispose();
|
||||
_streamingCall = null;
|
||||
}
|
||||
if (e.StatusCode != StatusCode.Cancelled) {
|
||||
// Cancelled is expected during normal shutdown, don't reconnect
|
||||
ScheduleReconnect($"RpcException-{e.StatusCode}");
|
||||
}
|
||||
return false;
|
||||
} catch (OperationCanceledException) {
|
||||
// Timeout was triggered
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"[WRITE_ERROR] Exception: {e.GetType().Name} - {e.Message}");
|
||||
// Unknown error - dispose and reconnect
|
||||
lock (this) {
|
||||
_streamingCall?.Dispose();
|
||||
_streamingCall = null;
|
||||
}
|
||||
ScheduleReconnect($"WriteException-{e.GetType().Name}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes Sparkle auto-updater at app startup.
|
||||
/// Uses RuntimeInitializeOnLoadMethod to ensure early initialization.
|
||||
/// </summary>
|
||||
public static class SparkleInitializer {
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
|
||||
private static void Initialize() {
|
||||
Debug.Log("SparkleInitializer: Initializing Sparkle at startup");
|
||||
SparkleUpdater.Initialize();
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -1,11 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e694fb18ea1df4034b392352bf3aa04a
|
||||
guid: 2b4c6d8e0f1a3b5c7d9e1f3a5b7c9d1e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
executionOrder: -100
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,120 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// C# wrapper for the native SparklePlugin on macOS.
|
||||
/// Provides auto-update functionality via the Sparkle framework.
|
||||
/// On non-macOS platforms, all methods are no-ops.
|
||||
/// </summary>
|
||||
public static class SparkleUpdater {
|
||||
#if UNITY_STANDALONE_OSX && !UNITY_EDITOR
|
||||
private const string PluginName = "SparklePlugin";
|
||||
|
||||
[DllImport(PluginName)]
|
||||
private static extern void SparklePlugin_Initialize();
|
||||
|
||||
[DllImport(PluginName)]
|
||||
private static extern void SparklePlugin_CheckForUpdates();
|
||||
|
||||
[DllImport(PluginName)]
|
||||
private static extern void SparklePlugin_CheckForUpdatesInBackground();
|
||||
|
||||
[DllImport(PluginName)]
|
||||
private static extern int SparklePlugin_IsCheckingForUpdates();
|
||||
|
||||
[DllImport(PluginName)]
|
||||
private static extern int SparklePlugin_GetAutomaticallyChecksForUpdates();
|
||||
|
||||
[DllImport(PluginName)]
|
||||
private static extern void SparklePlugin_SetAutomaticallyChecksForUpdates(int enabled);
|
||||
|
||||
private static bool _initialized = false;
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the Sparkle updater. Should be called once at app startup.
|
||||
/// This starts automatic background update checks based on Info.plist settings.
|
||||
/// </summary>
|
||||
public static void Initialize() {
|
||||
if (_initialized) {
|
||||
Debug.Log("SparkleUpdater: Already initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Log("SparkleUpdater: Initializing Sparkle");
|
||||
try {
|
||||
SparklePlugin_Initialize();
|
||||
_initialized = true;
|
||||
Debug.Log("SparkleUpdater: Initialization complete");
|
||||
} catch (System.Exception e) {
|
||||
Debug.LogError($"SparkleUpdater: Failed to initialize: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Manually check for updates. Shows the update UI to the user.
|
||||
/// </summary>
|
||||
public static void CheckForUpdates() {
|
||||
if (!_initialized) {
|
||||
Debug.LogWarning("SparkleUpdater: Not initialized");
|
||||
return;
|
||||
}
|
||||
SparklePlugin_CheckForUpdates();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check for updates silently in the background.
|
||||
/// Only shows UI if an update is found.
|
||||
/// </summary>
|
||||
public static void CheckForUpdatesInBackground() {
|
||||
if (!_initialized) {
|
||||
Debug.LogWarning("SparkleUpdater: Not initialized");
|
||||
return;
|
||||
}
|
||||
SparklePlugin_CheckForUpdatesInBackground();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if an update check is currently in progress.
|
||||
/// </summary>
|
||||
public static bool IsCheckingForUpdates {
|
||||
get {
|
||||
if (!_initialized) return false;
|
||||
return SparklePlugin_IsCheckingForUpdates() != 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether automatic update checks are enabled.
|
||||
/// </summary>
|
||||
public static bool AutomaticallyChecksForUpdates {
|
||||
get {
|
||||
if (!_initialized) return false;
|
||||
return SparklePlugin_GetAutomaticallyChecksForUpdates() != 0;
|
||||
}
|
||||
set {
|
||||
if (!_initialized) return;
|
||||
SparklePlugin_SetAutomaticallyChecksForUpdates(value? 1: 0);
|
||||
}
|
||||
}
|
||||
|
||||
#else
|
||||
// Stub implementations for non-macOS platforms and Editor
|
||||
|
||||
public static void Initialize() { Debug.Log("SparkleUpdater: Not available on this platform"); }
|
||||
|
||||
public static void CheckForUpdates() {
|
||||
Debug.Log("SparkleUpdater: Not available on this platform");
|
||||
}
|
||||
|
||||
public static void CheckForUpdatesInBackground() {
|
||||
Debug.Log("SparkleUpdater: Not available on this platform");
|
||||
}
|
||||
|
||||
public static bool IsCheckingForUpdates => false;
|
||||
|
||||
public static bool AutomaticallyChecksForUpdates {
|
||||
get => false;
|
||||
set {}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
+4
-4
@@ -1,11 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 680b7d1179eb34a0fa338286236db2e9
|
||||
guid: 8a3b5c7d9e1f2a3b4c5d6e7f8a9b0c1d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
BIN
Binary file not shown.
-117
@@ -1,117 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b6b68c8c37d5cf1419267a80c426ac83
|
||||
TextureImporter:
|
||||
fileIDToRecycleName: {}
|
||||
externalObjects: {}
|
||||
serializedVersion: 5
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 0
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
isReadable: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: -1
|
||||
aniso: -1
|
||||
mipBias: -1
|
||||
wrapU: 1
|
||||
wrapV: 1
|
||||
wrapW: 1
|
||||
nPOTScale: 0
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 1
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 1
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 8
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 2
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 128
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
- serializedVersion: 2
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 128
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
- serializedVersion: 2
|
||||
buildTarget: Android
|
||||
maxTextureSize: 128
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
- serializedVersion: 2
|
||||
buildTarget: WebGL
|
||||
maxTextureSize: 128
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID: e8701ed65f69e7b4385fc7a2dcf1f924
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
spritePackingTag:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,3 +1,6 @@
|
||||
System.Runtime.CompilerServices.Unsafe/**
|
||||
!**/*.psd
|
||||
**/*.dll
|
||||
# Native plugins built at CI time
|
||||
DarwinGodiceBundle.bundle/
|
||||
macOS/SparklePlugin.bundle/
|
||||
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b5b6db3c9b9e34b82ae9163a61276c8a
|
||||
PluginImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
iconMap: {}
|
||||
executionOrder: {}
|
||||
defineConstraints: []
|
||||
isPreloaded: 0
|
||||
isOverridable: 0
|
||||
isExplicitlyReferenced: 0
|
||||
validateReferences: 1
|
||||
platformData:
|
||||
- first:
|
||||
Any:
|
||||
second:
|
||||
enabled: 0
|
||||
settings: {}
|
||||
- first:
|
||||
Editor: Editor
|
||||
second:
|
||||
enabled: 1
|
||||
settings:
|
||||
DefaultValueInitialized: true
|
||||
- first:
|
||||
Standalone: OSXUniversal
|
||||
second:
|
||||
enabled: 1
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,27 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1fc634da07f5047c99dba5ce558b5886
|
||||
PluginImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
iconMap: {}
|
||||
executionOrder: {}
|
||||
defineConstraints: []
|
||||
isPreloaded: 0
|
||||
isOverridable: 0
|
||||
isExplicitlyReferenced: 0
|
||||
validateReferences: 1
|
||||
platformData:
|
||||
- first:
|
||||
Any:
|
||||
second:
|
||||
enabled: 1
|
||||
settings: {}
|
||||
- first:
|
||||
Editor: Editor
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
DefaultValueInitialized: true
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3c5d7e9f1a2b4c6d8e0f2a4b6c8d0e2f
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -23,10 +23,6 @@ public class SettingsPanelController : MonoBehaviour {
|
||||
public Slider tileBorderWidthSlider;
|
||||
public TMP_Text tileBorderWidthLabel;
|
||||
|
||||
public Toggle useGoDiceToggle;
|
||||
public Button testGoDice;
|
||||
public RollPanelController rollPanelController;
|
||||
|
||||
public Toggle tooltipHugsCursorToggle;
|
||||
public HoveringTooltip hoveringTooltip;
|
||||
|
||||
@@ -42,7 +38,6 @@ public class SettingsPanelController : MonoBehaviour {
|
||||
|
||||
private const string SoundEffectsVolumeKey = "soundEffectsVolume";
|
||||
private const string MusicVolumeKey = "musicVolume";
|
||||
public const string UseGoDiceKey = "useGoDice";
|
||||
private const string TooltipHugsCursorKey = "tooltipHugsCursor";
|
||||
private const string AnimationToggleKey = "animationToggle";
|
||||
|
||||
@@ -62,7 +57,6 @@ public class SettingsPanelController : MonoBehaviour {
|
||||
|
||||
soundEffectsSlider.value = soundEffectsSource.volume;
|
||||
musicSlider.value = musicSource.volume;
|
||||
useGoDiceToggle.isOn = PlayerPrefs.GetInt(UseGoDiceKey, 0) == 1;
|
||||
|
||||
tooltipHugsCursorToggle.isOn = PlayerPrefs.GetInt(TooltipHugsCursorKey, 0) == 1;
|
||||
hoveringTooltip.HugsCursor = tooltipHugsCursorToggle.isOn;
|
||||
@@ -111,17 +105,6 @@ public class SettingsPanelController : MonoBehaviour {
|
||||
PlayerPrefs.SetFloat(MusicVolumeKey, val);
|
||||
}
|
||||
|
||||
public void OnUseGoDiceToggleChange(bool val) {
|
||||
PlayerPrefs.SetInt(UseGoDiceKey, val ? 1 : 0);
|
||||
testGoDice.interactable = val;
|
||||
}
|
||||
|
||||
public void OnTestGoDiceClick() {
|
||||
rollPanelController.GetRoll(
|
||||
new RollRequest { RollType = RollType.D100OpenEnded },
|
||||
(result) => { Console.Out.WriteLine($"Got ${result}"); });
|
||||
}
|
||||
|
||||
public void OnCursorHugToggleChange(bool val) {
|
||||
PlayerPrefs.SetInt(TooltipHugsCursorKey, val ? 1 : 0);
|
||||
hoveringTooltip.HugsCursor = val;
|
||||
|
||||
BIN
Binary file not shown.
+2
-2
@@ -1,6 +1,6 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9acf3ba64a8c04e6083aae699248b0c4
|
||||
PrefabImporter:
|
||||
guid: e082b4a1a25a044a99b7798107149f77
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
+1
-1
@@ -1160,7 +1160,7 @@ namespace Shardok {
|
||||
int currentMouseGridIndex) {}
|
||||
|
||||
public void HandlePrimaryClick(int clickedIndex) {
|
||||
if (HasPlacementAction(clickedIndex)) {
|
||||
if (_selectedGridIndex != clickedIndex && HasPlacementAction(clickedIndex)) {
|
||||
PerformPlacementAction(clickedIndex);
|
||||
_selectedGridIndex = null;
|
||||
} else if (HasReinforceAction(clickedIndex)) {
|
||||
|
||||
@@ -6,7 +6,6 @@ using eagle0;
|
||||
using UnityEngine;
|
||||
using Net.Eagle0.Shardok.Api;
|
||||
using Net.Eagle0.Shardok.Common;
|
||||
using UnityGoDiceInterface;
|
||||
using BattalionId = System.Int32;
|
||||
using UnitId = System.Int32;
|
||||
using PlayerId = System.Int32;
|
||||
@@ -133,8 +132,6 @@ public class ShardokGameModel {
|
||||
|
||||
private const int StartingHistoryCapacity = 100;
|
||||
|
||||
private readonly RollFetcher _rollFetcher;
|
||||
|
||||
public ShardokGameModel(
|
||||
EagleGameId eagleGameId,
|
||||
ShardokGameId shardokGameId,
|
||||
@@ -146,13 +143,11 @@ public class ShardokGameModel {
|
||||
List<PlayerWithHostility> players,
|
||||
Dictionary<HeroId, String> heroNameTextIds,
|
||||
Dictionary<HeroId, String> heroImages,
|
||||
Dictionary<BattalionId, String> battalionNames,
|
||||
RollFetcher rollFetcher) {
|
||||
Dictionary<BattalionId, String> battalionNames) {
|
||||
EagleGameId = eagleGameId;
|
||||
ShardokGameId = shardokGameId;
|
||||
History = new List<ActionResultView>(StartingHistoryCapacity);
|
||||
_persistentClientConnection = persistentClientConnection;
|
||||
this._rollFetcher = rollFetcher;
|
||||
PlayerId = playerId;
|
||||
this.players = new List<PlayerWithHostility>(players);
|
||||
|
||||
@@ -350,25 +345,14 @@ public class ShardokGameModel {
|
||||
var index = AvailableCommands.IndexOf(action);
|
||||
AvailableCommands.Clear();
|
||||
|
||||
if (action.RollRequest != null) {
|
||||
_rollFetcher.GetRoll(action.RollRequest, roll => {
|
||||
_persistentClientConnection.PostShardokCommand(
|
||||
EagleGameId,
|
||||
ShardokGameId,
|
||||
PlayerId,
|
||||
History.Count(),
|
||||
index,
|
||||
roll);
|
||||
});
|
||||
} else {
|
||||
_persistentClientConnection.PostShardokCommand(
|
||||
EagleGameId,
|
||||
ShardokGameId,
|
||||
PlayerId,
|
||||
History.Count(),
|
||||
index,
|
||||
null);
|
||||
}
|
||||
// Physical dice roll support removed - server generates random rolls
|
||||
_persistentClientConnection.PostShardokCommand(
|
||||
EagleGameId,
|
||||
ShardokGameId,
|
||||
PlayerId,
|
||||
History.Count(),
|
||||
index,
|
||||
null);
|
||||
}
|
||||
|
||||
private void PostGameSetupActions() {
|
||||
|
||||
@@ -2057,6 +2057,21 @@ MonoBehaviour:
|
||||
m_Scale: 1
|
||||
m_AtlasIndex: 0
|
||||
m_ClassDefinitionType: 0
|
||||
- m_Index: 130
|
||||
m_Metrics:
|
||||
m_Width: 0
|
||||
m_Height: 0
|
||||
m_HorizontalBearingX: 0
|
||||
m_HorizontalBearingY: 0
|
||||
m_HorizontalAdvance: 54.015625
|
||||
m_GlyphRect:
|
||||
m_X: 0
|
||||
m_Y: 0
|
||||
m_Width: 0
|
||||
m_Height: 0
|
||||
m_Scale: 1
|
||||
m_AtlasIndex: 0
|
||||
m_ClassDefinitionType: 0
|
||||
m_CharacterTable:
|
||||
- m_ElementType: 1
|
||||
m_Unicode: 2
|
||||
@@ -2582,6 +2597,10 @@ MonoBehaviour:
|
||||
m_Unicode: 8230
|
||||
m_GlyphIndex: 389
|
||||
m_Scale: 1
|
||||
- m_ElementType: 1
|
||||
m_Unicode: 173
|
||||
m_GlyphIndex: 130
|
||||
m_Scale: 1
|
||||
m_AtlasTextures:
|
||||
- {fileID: -953588492498898259}
|
||||
m_AtlasTextureIndex: 0
|
||||
|
||||
+4
-31
File diff suppressed because one or more lines are too long
+509
-66
@@ -16,14 +16,20 @@ namespace Eagle0.Tutorial {
|
||||
var onboarding = CreateOnboardingSequence();
|
||||
manager.OnboardingSequence = onboarding;
|
||||
|
||||
// Register command panel tutorials (shown after onboarding)
|
||||
RegisterCommandPanelTutorials(registry);
|
||||
|
||||
// Register strategic contextual tutorials
|
||||
RegisterStrategicTutorials(registry);
|
||||
|
||||
// Register tactical contextual tutorials
|
||||
RegisterTacticalTutorials(registry);
|
||||
|
||||
// Register game state guidance tutorials
|
||||
RegisterGuidanceTutorials(registry);
|
||||
}
|
||||
|
||||
// ========== ONBOARDING SEQUENCE (13 steps) ==========
|
||||
// ========== ONBOARDING SEQUENCE ==========
|
||||
|
||||
private static TutorialSequence CreateOnboardingSequence() {
|
||||
var sequence = ScriptableObject.CreateInstance<TutorialSequence>();
|
||||
@@ -37,60 +43,146 @@ namespace Eagle0.Tutorial {
|
||||
StepId = "welcome",
|
||||
Title = "Welcome to Eagle0",
|
||||
Description =
|
||||
"Command your armies, recruit heroes, and conquer provinces in this turn-based strategy game.\n\nLet's walk through the basics!",
|
||||
"You are a new ruler in a fractured realm. Build your power, recruit heroes, and expand your domain.\n\n" +
|
||||
"Let's look at your first province!\n\n" +
|
||||
"<size=80%><color=#888888>(You can skip this tutorial and restart it later from Settings.)</color></size>",
|
||||
DisplayMode = TutorialDisplayMode.Modal,
|
||||
CompletionType = TutorialCompletionType.ButtonClick,
|
||||
AllowSkip = true
|
||||
},
|
||||
|
||||
// Step 2: Map overview - select a province
|
||||
// Step 2: Province Info Panel - Stats Overview
|
||||
new TutorialStep {
|
||||
StepId = "select_province",
|
||||
Title = "The Strategic Map",
|
||||
StepId = "province_stats",
|
||||
Title = "Province Statistics",
|
||||
Description =
|
||||
"This is your kingdom. Each colored region is a province.\n\nTap a province you control (shown in your color) to see what you can do there.",
|
||||
DisplayMode = TutorialDisplayMode.Overlay,
|
||||
CompletionType = TutorialCompletionType.GameEvent,
|
||||
CompletionEventId = "province_selected",
|
||||
AllowSkip = true
|
||||
},
|
||||
|
||||
// Step 3: Province info panel
|
||||
new TutorialStep {
|
||||
StepId = "province_panel",
|
||||
Title = "Province Information",
|
||||
Description =
|
||||
"This panel shows province details: its name, terrain, any armies present, and the commands available to you.\n\nCommands let you move troops, recruit heroes, and more.",
|
||||
"This panel shows your province's vital statistics. <b>Hover over icons and numbers</b> for detailed tooltips.\n\n" +
|
||||
"• <b>Agriculture</b> produces Food to feed your troops\n" +
|
||||
"• <b>Economy</b> generates Gold for hiring, gifts, and equipment\n" +
|
||||
"• <b>Infrastructure</b> improves troop armament and disaster resilience\n\n" +
|
||||
"All three stats increase your storage capacity.",
|
||||
DisplayMode = TutorialDisplayMode.Modal,
|
||||
BlocksInteraction = false,
|
||||
PanelAnchor = TutorialPanelAnchor.Right,
|
||||
AdjacentTargetPath = "ProvinceInfoPanel",
|
||||
TargetGameObjectPath = "ProvinceInfoPanel",
|
||||
HighlightPulsing = true,
|
||||
CompletionType = TutorialCompletionType.ButtonClick,
|
||||
AllowSkip = true
|
||||
},
|
||||
|
||||
// Step 4: Try March command
|
||||
// Step 3: Province Info Panel - Support (CRITICAL)
|
||||
new TutorialStep {
|
||||
StepId = "try_march",
|
||||
Title = "Issue a Command",
|
||||
StepId = "province_support",
|
||||
Title = "Support is Critical!",
|
||||
Description =
|
||||
"Try issuing a March command to move your army to an adjacent province.\n\nSelect a destination and confirm the order.",
|
||||
DisplayMode = TutorialDisplayMode.Overlay,
|
||||
// TODO: Add TargetGameObjectPath once we know the actual UI hierarchy
|
||||
CompletionType = TutorialCompletionType.GameEvent,
|
||||
CompletionEventId = "command_issued",
|
||||
"<b>Support</b> measures how much your people trust you. This is your most important number right now!\n\n" +
|
||||
"You need <b>40 Support by January</b> to collect taxes. Taxes provide both <b>Gold</b> and <b>Food</b>:\n" +
|
||||
"• <b>Food</b> feeds your troops and lets you Give Alms\n" +
|
||||
"• <b>Gold</b> pays for hiring, hero gifts, equipment, and more\n\n" +
|
||||
"Use <b>Improve</b> and <b>Give Alms</b> to raise Support quickly.",
|
||||
DisplayMode = TutorialDisplayMode.Modal,
|
||||
BlocksInteraction = false,
|
||||
PanelAnchor = TutorialPanelAnchor.Right,
|
||||
AdjacentTargetPath = "ProvinceInfoPanel",
|
||||
TargetGameObjectPath = "SupportField",
|
||||
HighlightPulsing = true,
|
||||
CompletionType = TutorialCompletionType.ButtonClick,
|
||||
AllowSkip = true
|
||||
},
|
||||
|
||||
// Step 5: Turn cycle explanation
|
||||
// Step 4: Heroes Panel - Warlord
|
||||
new TutorialStep {
|
||||
StepId = "heroes_warlord",
|
||||
Title = "Your Warlord",
|
||||
Description =
|
||||
"The <b>Resident Heroes</b> panel shows heroes in this province. The first hero is your <b>Warlord</b> - this is essentially <i>you</i>.\n\n" +
|
||||
"<b>Hover over a hero</b> to see their backstory, stats, and abilities. Your Warlord has a special profession that grants unique powers.\n\n" +
|
||||
"<color=#FF6666>Your Warlord is very important - protect them!</color>",
|
||||
DisplayMode = TutorialDisplayMode.Modal,
|
||||
BlocksInteraction = false,
|
||||
PanelAnchor = TutorialPanelAnchor.Right,
|
||||
AdjacentTargetPath = "HeroesPanel",
|
||||
TargetGameObjectPath = "WarlordRow",
|
||||
HighlightPulsing = true,
|
||||
CompletionType = TutorialCompletionType.ButtonClick,
|
||||
AllowSkip = true
|
||||
},
|
||||
|
||||
// Step 5: Heroes Panel - Vassals
|
||||
new TutorialStep {
|
||||
StepId = "heroes_vassals",
|
||||
Title = "Your Vassals",
|
||||
Description =
|
||||
"The other heroes in your service are your <b>Vassals</b>. They lead troops, govern provinces, and perform tasks for you.\n\n" +
|
||||
"<b>Watch their Loyalty!</b> At the end of each year, vassal loyalty may drop. If it falls below <b>50</b>, they might leave your service - or worse.\n\n" +
|
||||
"Keep vassals happy with gifts and feasts!",
|
||||
DisplayMode = TutorialDisplayMode.Modal,
|
||||
BlocksInteraction = false,
|
||||
PanelAnchor = TutorialPanelAnchor.Right,
|
||||
AdjacentTargetPath = "HeroesPanel",
|
||||
TargetGameObjectPath = "VassalRow1",
|
||||
AdditionalHighlightTargets = new[] { "VassalRow2" },
|
||||
HighlightPulsing = true,
|
||||
CompletionType = TutorialCompletionType.ButtonClick,
|
||||
AllowSkip = true
|
||||
},
|
||||
|
||||
// Step 6: Command Buttons - Focus on Improve/Alms
|
||||
new TutorialStep {
|
||||
StepId = "command_buttons",
|
||||
Title = "Province Commands",
|
||||
Description =
|
||||
"The buttons below let you issue commands. <b>Feel free to click them</b> - nothing happens until you click <b>Commit</b>!\n\n" +
|
||||
"For now, focus on:\n" +
|
||||
"• <b>Improve</b> - Raises province stats AND Support\n" +
|
||||
"• <b>Give Alms</b> - Quickly boosts Support (costs Food)\n\n" +
|
||||
"Get to <b>40 Support before January</b> to collect taxes!",
|
||||
DisplayMode = TutorialDisplayMode.Modal,
|
||||
BlocksInteraction = false,
|
||||
PanelAnchor = TutorialPanelAnchor.Top,
|
||||
AdjacentTargetPath = "CommandButtonsPanel",
|
||||
TargetGameObjectPath = "CommandButtonsPanel",
|
||||
HighlightPulsing = true,
|
||||
HighlightBoundsFromChildren = true,
|
||||
CompletionType = TutorialCompletionType.ButtonClick,
|
||||
AllowSkip = true
|
||||
},
|
||||
|
||||
// Step 7: Turn cycle explanation
|
||||
new TutorialStep {
|
||||
StepId = "turn_cycle",
|
||||
Title = "The Turn Cycle",
|
||||
Description =
|
||||
"Eagle0 uses simultaneous turns. All players give orders at the same time, then turns resolve together.\n\nWhen all players are ready, the server processes everyone's commands and shows the results.",
|
||||
"Eagle0 uses <b>simultaneous turns</b>. All players give orders at the same time, then everything resolves together.\n\n" +
|
||||
"Your turn ends when you've given an order in each of your provinces. Once all players are ready, the server processes commands and shows results.",
|
||||
DisplayMode = TutorialDisplayMode.Modal,
|
||||
CompletionType = TutorialCompletionType.ButtonClick,
|
||||
AllowSkip = true
|
||||
},
|
||||
|
||||
// Step 6: Hidden wait for battle
|
||||
// Step 8: Completion of strategic intro
|
||||
new TutorialStep {
|
||||
StepId = "strategic_complete",
|
||||
Title = "You're Ready to Begin!",
|
||||
Description =
|
||||
"That's the basics of managing your province. Remember:\n\n" +
|
||||
"• Get <b>Support to 40</b> before January for taxes (Gold + Food)\n" +
|
||||
"• <b>Hover over everything</b> for tooltips\n" +
|
||||
"• Protect your <b>Warlord</b> at all costs!\n" +
|
||||
"• Keep your <b>Vassals</b> loyal with gifts\n\n" +
|
||||
"When armies clash, you'll learn tactical combat. Good luck, ruler!",
|
||||
DisplayMode = TutorialDisplayMode.Modal,
|
||||
CompletionType = TutorialCompletionType.ButtonClick,
|
||||
AllowSkip = false,
|
||||
// Mark onboarding complete here so command tutorials can appear
|
||||
// Tactical tutorial continues in background when battle becomes available
|
||||
MarksOnboardingComplete = true
|
||||
},
|
||||
|
||||
// === TACTICAL TUTORIAL (triggered later when battle occurs) ===
|
||||
|
||||
// Step 9: Hidden wait for battle
|
||||
new TutorialStep {
|
||||
StepId = "wait_for_battle",
|
||||
Title = "",
|
||||
@@ -101,103 +193,413 @@ namespace Eagle0.Tutorial {
|
||||
AllowSkip = true
|
||||
},
|
||||
|
||||
// Step 7: Battle introduction
|
||||
// Step 10: Battle introduction
|
||||
new TutorialStep {
|
||||
StepId = "battle_intro",
|
||||
Title = "Battle Time!",
|
||||
Description =
|
||||
"When armies collide, you'll fight tactical battles on a hex grid.\n\nYou command individual units - infantry, cavalry, archers, and heroes with special abilities.",
|
||||
"When armies collide, you fight tactical battles on a hex grid.\n\n" +
|
||||
"You command individual units - infantry, cavalry, archers, and heroes with special abilities.",
|
||||
DisplayMode = TutorialDisplayMode.Modal,
|
||||
CompletionType = TutorialCompletionType.ButtonClick,
|
||||
AllowSkip = true
|
||||
},
|
||||
|
||||
// Step 8: Battle button highlight
|
||||
// Step 11: Battle button highlight
|
||||
new TutorialStep {
|
||||
StepId = "enter_battle",
|
||||
Title = "Enter the Battle",
|
||||
Description = "Tap the Battle button to enter tactical combat.",
|
||||
Description = "Tap the <b>Battle</b> button to enter tactical combat.",
|
||||
DisplayMode = TutorialDisplayMode.Overlay,
|
||||
// TODO: Add TargetGameObjectPath once we know the actual UI hierarchy
|
||||
CompletionType = TutorialCompletionType.GameEvent,
|
||||
CompletionEventId = "battle_entered",
|
||||
AllowSkip = true
|
||||
},
|
||||
|
||||
// Step 9: Tactical overview
|
||||
// Step 12: Tactical overview
|
||||
new TutorialStep {
|
||||
StepId = "tactical_overview",
|
||||
Title = "Tactical Combat",
|
||||
Description =
|
||||
"Each unit has movement points and attack power. Position your troops wisely!\n\nUnits attack adjacent enemies. Flanking (attacking from multiple sides) deals bonus damage.",
|
||||
"Each unit has movement points and attack power. Position your troops wisely!\n\n" +
|
||||
"Units attack adjacent enemies. <b>Flanking</b> (attacking from multiple sides) deals bonus damage.",
|
||||
DisplayMode = TutorialDisplayMode.Modal,
|
||||
CompletionType = TutorialCompletionType.ButtonClick,
|
||||
AllowSkip = true
|
||||
},
|
||||
|
||||
// Step 10: Move a unit
|
||||
// Step 13: Move a unit
|
||||
new TutorialStep {
|
||||
StepId = "move_unit",
|
||||
Title = "Move Your Units",
|
||||
Description =
|
||||
"Tap one of your units to select it, then tap a highlighted hex to move there.\n\nBlue hexes show where you can move.",
|
||||
"Tap one of your units to select it, then tap a highlighted hex to move there.\n\n" +
|
||||
"<b>Blue hexes</b> show where you can move.",
|
||||
DisplayMode = TutorialDisplayMode.Overlay,
|
||||
CompletionType = TutorialCompletionType.GameEvent,
|
||||
CompletionEventId = "battle_action",
|
||||
AllowSkip = true
|
||||
},
|
||||
|
||||
// Step 11: Attack an enemy
|
||||
// Step 14: Attack an enemy
|
||||
new TutorialStep {
|
||||
StepId = "attack_enemy",
|
||||
Title = "Attack!",
|
||||
Description =
|
||||
"Move next to an enemy unit, then tap the enemy to attack.\n\nRed highlights show valid attack targets.",
|
||||
Description = "Move next to an enemy unit, then tap the enemy to attack.\n\n" +
|
||||
"<b>Red highlights</b> show valid attack targets.",
|
||||
DisplayMode = TutorialDisplayMode.Overlay,
|
||||
CompletionType = TutorialCompletionType.GameEvent,
|
||||
CompletionEventId = "battle_action",
|
||||
AllowSkip = true
|
||||
},
|
||||
|
||||
// Step 12: End turn
|
||||
new TutorialStep {
|
||||
StepId = "end_turn",
|
||||
Title = "End Your Turn",
|
||||
Description =
|
||||
"When you've moved all units or want to pass, tap End Turn.\n\nThe enemy will then take their turn.",
|
||||
DisplayMode = TutorialDisplayMode.Overlay,
|
||||
// TODO: Add TargetGameObjectPath once we know the actual UI hierarchy
|
||||
CompletionType = TutorialCompletionType.GameEvent,
|
||||
CompletionEventId = "turn_ended",
|
||||
AllowSkip = true
|
||||
},
|
||||
|
||||
// Step 13: Completion
|
||||
// Step 15: Final completion
|
||||
new TutorialStep {
|
||||
StepId = "complete",
|
||||
Title = "You're Ready!",
|
||||
Title = "Tutorial Complete!",
|
||||
Description =
|
||||
"You now know the basics of Eagle0!\n\nExplore diplomacy, recruit powerful heroes, and conquer the realm. Good luck, commander!",
|
||||
"You now know the basics of Eagle0!\n\n" +
|
||||
"Explore diplomacy, recruit powerful heroes, and conquer the realm. May your reign be long and prosperous!",
|
||||
DisplayMode = TutorialDisplayMode.Modal,
|
||||
CompletionType = TutorialCompletionType.ButtonClick,
|
||||
AllowSkip = false // Don't allow skipping the final step
|
||||
AllowSkip = false
|
||||
}
|
||||
};
|
||||
|
||||
return sequence;
|
||||
}
|
||||
|
||||
// ========== COMMAND PANEL TUTORIALS ==========
|
||||
// These appear the first time a command panel is shown, after onboarding completes.
|
||||
|
||||
private static void RegisterCommandPanelTutorials(TutorialTriggerRegistry registry) {
|
||||
// Improve command - most important early game command
|
||||
var improve = CreateCommandPanelTutorial(
|
||||
"command_improve",
|
||||
"Improve Province",
|
||||
"Use <b>Improve</b> to develop your province and increase Support.\n\n" +
|
||||
"• <b>Agriculture</b> - Increases food production\n" +
|
||||
"• <b>Economy</b> - Increases gold income\n" +
|
||||
"• <b>Infrastructure</b> - Improves armament and resilience\n" +
|
||||
"• <b>Repair Devastation</b> - Restores damaged stats\n\n" +
|
||||
"<b>Tip:</b> Select a hero using the dropdown, or click a hero in the <b>Resident Heroes</b> panel to the left!");
|
||||
registry.RegisterTutorial(improve, "command_panel_ImproveCommand");
|
||||
|
||||
// Alms command
|
||||
var alms = CreateCommandPanelTutorial(
|
||||
"command_alms",
|
||||
"Give Alms",
|
||||
"Give Alms distributes food to the people, quickly raising <b>Support</b>.\n\n" +
|
||||
"This is the fastest way to boost Support, but costs Food from your stores. Use it when you need Support urgently!");
|
||||
registry.RegisterTutorial(alms, "command_panel_AlmsCommand");
|
||||
|
||||
// March command
|
||||
var march = CreateCommandPanelTutorial(
|
||||
"command_march",
|
||||
"March",
|
||||
"March moves your army to another province.\n\n" +
|
||||
"• Select a <b>destination</b> province\n" +
|
||||
"• Choose which <b>heroes and battalions</b> to take\n" +
|
||||
"• Optionally bring <b>supplies</b> (food and gold)\n\n" +
|
||||
"If you march into enemy territory, battle may ensue!");
|
||||
registry.RegisterTutorial(march, "command_panel_MarchCommand");
|
||||
|
||||
// Defend command
|
||||
var defend = CreateCommandPanelTutorial(
|
||||
"command_defend",
|
||||
"Defend",
|
||||
"Defend prepares your forces to repel attackers.\n\n" +
|
||||
"Choose a <b>rally point</b> where your troops will gather. Units defending have advantages in tactical combat.");
|
||||
registry.RegisterTutorial(defend, "command_panel_DefendCommand");
|
||||
|
||||
// Rest command
|
||||
var rest = CreateCommandPanelTutorial(
|
||||
"command_rest",
|
||||
"Rest",
|
||||
"Rest restores your heroes' <b>Vigor</b>.\n\n" +
|
||||
"Heroes need rest to recover from exertion. Use this when your heroes are tired and you don't need to take other actions.");
|
||||
registry.RegisterTutorial(rest, "command_panel_RestCommand");
|
||||
|
||||
// Trade command
|
||||
var trade = CreateCommandPanelTutorial(
|
||||
"command_trade",
|
||||
"Trade",
|
||||
"Trade lets you exchange <b>Food</b> for <b>Gold</b> or vice versa.\n\n" +
|
||||
"The market takes its cut - buy and sell prices differ. Useful when you have surplus of one resource and need the other.");
|
||||
registry.RegisterTutorial(trade, "command_panel_TradeCommand");
|
||||
|
||||
// Feast command
|
||||
var feast = CreateCommandPanelTutorial(
|
||||
"command_feast",
|
||||
"Hold Feast",
|
||||
"Hold a Feast to boost hero <b>Loyalty</b> and <b>Vigor</b>.\n\n" +
|
||||
"The cost depends on how many heroes are in the province. A feast keeps your vassals happy and well-rested!");
|
||||
registry.RegisterTutorial(feast, "command_panel_FeastCommand");
|
||||
|
||||
// Hero Gift command
|
||||
var heroGift = CreateCommandPanelTutorial(
|
||||
"command_hero_gift",
|
||||
"Give Gift",
|
||||
"Give a gift to increase a hero's <b>Loyalty</b>.\n\n" +
|
||||
"Select a hero and choose how much gold to spend. More expensive gifts have greater effect.");
|
||||
registry.RegisterTutorial(heroGift, "command_panel_HeroGiftCommand");
|
||||
|
||||
// Train command
|
||||
var train = CreateCommandPanelTutorial(
|
||||
"command_train",
|
||||
"Train",
|
||||
"Train improves your troops' <b>combat effectiveness</b>.\n\n" +
|
||||
"Select a hero to lead training. Better trainers produce better results!");
|
||||
registry.RegisterTutorial(train, "command_panel_TrainCommand");
|
||||
|
||||
// Arm Troops command
|
||||
var armTroops = CreateCommandPanelTutorial(
|
||||
"command_arm_troops",
|
||||
"Arm Troops",
|
||||
"Arm Troops equips your battalions with better weapons and armor.\n\n" +
|
||||
"Higher <b>Infrastructure</b> allows better equipment. Well-armed troops are more effective in battle.");
|
||||
registry.RegisterTutorial(armTroops, "command_panel_ArmTroopsCommand");
|
||||
|
||||
// Organize Troops command
|
||||
var organizeTroops = CreateCommandPanelTutorial(
|
||||
"command_organize_troops",
|
||||
"Organize Troops",
|
||||
"Organize Troops lets you <b>hire new battalions</b> and restructure existing ones.\n\n" +
|
||||
"Light Infantry and Longbowmen are always available. Other battalion types require minimum <b>Economy</b> or <b>Agriculture</b> levels. You can also combine, split, or reassign troops between battalions.");
|
||||
registry.RegisterTutorial(organizeTroops, "command_panel_OrganizeTroopsCommand");
|
||||
|
||||
// Recruit Heroes command
|
||||
var recruitHeroes = CreateCommandPanelTutorial(
|
||||
"command_recruit_heroes",
|
||||
"Recruit Heroes",
|
||||
"Recruit a free hero to join your faction!\n\n" +
|
||||
"Heroes lead armies, govern provinces, and have special abilities. Choose wisely - their stats and professions vary.");
|
||||
registry.RegisterTutorial(recruitHeroes, "command_panel_RecruitHeroesCommand");
|
||||
|
||||
// Travel command
|
||||
var travel = CreateCommandPanelTutorial(
|
||||
"command_travel",
|
||||
"Travel",
|
||||
"Travel sends your hero to <b>town</b> within the province.\n\n" +
|
||||
"While traveling, you can do as many actions as you want in a turn: trade, arm troops, divine or recruit heroes, and manage prisoners. Use <b>Return</b> when done.");
|
||||
registry.RegisterTutorial(travel, "command_panel_TravelCommand");
|
||||
|
||||
// Return command
|
||||
var returnCmd = CreateCommandPanelTutorial(
|
||||
"command_return",
|
||||
"Return to Camp",
|
||||
"Return your province leader from town back to camp.\n\n" +
|
||||
"This ends <b>Travel</b> mode and completes your turn. Use it when you're done with town activities.");
|
||||
registry.RegisterTutorial(returnCmd, "command_panel_ReturnCommand");
|
||||
|
||||
// Diplomacy command
|
||||
var diplomacy = CreateCommandPanelTutorial(
|
||||
"command_diplomacy",
|
||||
"Diplomacy",
|
||||
"Send a diplomatic envoy to another faction.\n\n" +
|
||||
"Propose truces to pause hostilities, form alliances for mutual defense, or invite factions to join your cause. Select an ambassador - sending your leader is risky!");
|
||||
registry.RegisterTutorial(diplomacy, "command_panel_DiplomacyCommand");
|
||||
|
||||
// Ransom command (uses DiplomacyCommand type but separate selector)
|
||||
// Note: RansomCommandSelector also uses DiplomacyCommand, triggered by same event
|
||||
|
||||
// Send Supplies command
|
||||
var sendSupplies = CreateCommandPanelTutorial(
|
||||
"command_send_supplies",
|
||||
"Send Supplies",
|
||||
"Send gold and food to another province.\n\n" +
|
||||
"Useful for resupplying distant armies or supporting provinces under siege. Select a hero to escort the supplies.");
|
||||
registry.RegisterTutorial(sendSupplies, "command_panel_SendSuppliesCommand");
|
||||
|
||||
// Recon command
|
||||
var recon = CreateCommandPanelTutorial(
|
||||
"command_recon",
|
||||
"Recon",
|
||||
"Send a hero to scout an enemy province.\n\n" +
|
||||
"Gather intelligence on enemy forces, resources, and defenses. Your scout may encounter danger!");
|
||||
registry.RegisterTutorial(recon, "command_panel_ReconCommand");
|
||||
|
||||
// Divine command
|
||||
var divine = CreateCommandPanelTutorial(
|
||||
"command_divine",
|
||||
"Divine Heroes",
|
||||
"Pay a soothsayer to divine a hero's hidden desires.\n\n" +
|
||||
"Reveals the intentions and aspirations of a free hero, showing you what you must do to win them to your cause.");
|
||||
registry.RegisterTutorial(divine, "command_panel_DivineCommand");
|
||||
|
||||
// Issue Orders command
|
||||
var issueOrders = CreateCommandPanelTutorial(
|
||||
"command_issue_orders",
|
||||
"Issue Orders",
|
||||
"Set standing orders for your provinces.\n\n" +
|
||||
"Orders determine how provinces behave each turn - prioritize military, economy, or defense. You can also designate a Focus Province for special attention.");
|
||||
registry.RegisterTutorial(issueOrders, "command_panel_IssueOrdersCommand");
|
||||
|
||||
// Control Weather command
|
||||
var controlWeather = CreateCommandPanelTutorial(
|
||||
"command_control_weather",
|
||||
"Control Weather",
|
||||
"Use magical power to control the weather.\n\n" +
|
||||
"Create favorable conditions for your forces or harsh weather to hinder enemies. Requires a hero with weather magic.");
|
||||
registry.RegisterTutorial(
|
||||
controlWeather,
|
||||
"command_panel_ControlWeatherAvailableCommand");
|
||||
|
||||
// Swear Brotherhood command
|
||||
var swearBrotherhood = CreateCommandPanelTutorial(
|
||||
"command_swear_brotherhood",
|
||||
"Swear Brotherhood",
|
||||
"Swear an oath of brotherhood with a hero.\n\n" +
|
||||
"Creates a powerful bond that boosts loyalty and grants special bonuses. Choose your sworn brothers wisely - this bond is sacred!");
|
||||
registry.RegisterTutorial(swearBrotherhood, "command_panel_SwearBrotherhoodCommand");
|
||||
|
||||
// Apprehend Outlaw command
|
||||
var apprehendOutlaw = CreateCommandPanelTutorial(
|
||||
"command_apprehend_outlaw",
|
||||
"Apprehend Outlaw",
|
||||
"Capture an outlaw hiding in your province.\n\n" +
|
||||
"Send a hero and optionally a battalion to apprehend the fugitive. Outlaws may be former heroes from defeated factions.");
|
||||
registry.RegisterTutorial(apprehendOutlaw, "command_panel_ApprehendOutlawCommand");
|
||||
|
||||
// Suppress Beasts command
|
||||
var suppressBeasts = CreateCommandPanelTutorial(
|
||||
"command_suppress_beasts",
|
||||
"Suppress Beasts",
|
||||
"Deal with dangerous creatures threatening your province.\n\n" +
|
||||
"Send a hero with a battalion to suppress the beasts. Going without troops is risky - your hero could be killed!");
|
||||
registry.RegisterTutorial(suppressBeasts, "command_panel_SuppressBeastsCommand");
|
||||
|
||||
// Exile Vassal command
|
||||
var exileVassal = CreateCommandPanelTutorial(
|
||||
"command_exile_vassal",
|
||||
"Exile Vassal",
|
||||
"Exile a hero from your faction.\n\n" +
|
||||
"Sometimes a disloyal or troublesome vassal must be removed. The exiled hero becomes an outlaw and may seek revenge!");
|
||||
registry.RegisterTutorial(exileVassal, "command_panel_ExileVassalCommand");
|
||||
|
||||
// Handle Captured Hero command
|
||||
var handleCapturedHero = CreateCommandPanelTutorial(
|
||||
"command_handle_captured_hero",
|
||||
"Handle Captured Hero",
|
||||
"Decide the fate of a captured enemy hero.\n\n" +
|
||||
"You can recruit them to your cause, imprison them for ransom, exile them, execute them, or return them for diplomatic favor.");
|
||||
registry.RegisterTutorial(
|
||||
handleCapturedHero,
|
||||
"command_panel_HandleCapturedHeroCommand");
|
||||
|
||||
// Manage Prisoners command
|
||||
var managePrisoners = CreateCommandPanelTutorial(
|
||||
"command_manage_prisoners",
|
||||
"Manage Prisoners",
|
||||
"Manage heroes you've imprisoned.\n\n" +
|
||||
"Release them, move them between provinces, exile them, execute them, or return them to their faction for diplomatic benefit.");
|
||||
registry.RegisterTutorial(managePrisoners, "command_panel_ManagePrisonerCommand");
|
||||
|
||||
// Decline Quest command
|
||||
var declineQuest = CreateCommandPanelTutorial(
|
||||
"command_decline_quest",
|
||||
"Decline Quest",
|
||||
"Decline a quest offered by a hero.\n\n" +
|
||||
"Some heroes seek specific quests before joining. If you can't fulfill their request, you may decline - but they may leave.");
|
||||
registry.RegisterTutorial(declineQuest, "command_panel_DeclineQuestCommand");
|
||||
|
||||
// Start Epidemic command
|
||||
var startEpidemic = CreateCommandPanelTutorial(
|
||||
"command_start_epidemic",
|
||||
"Start Plague",
|
||||
"Unleash a deadly plague upon an enemy province.\n\n" +
|
||||
"A devastating but dishonorable tactic. Requires a hero with plague magic. The disease will spread and cause great suffering.");
|
||||
registry.RegisterTutorial(startEpidemic, "command_panel_StartEpidemicCommand");
|
||||
|
||||
// Handle Riot - Crack Down command
|
||||
var handleRiotCrackDown = CreateCommandPanelTutorial(
|
||||
"command_handle_riot_crack_down",
|
||||
"Crack Down",
|
||||
"Use force to suppress the rioting populace.\n\n" +
|
||||
"Send a hero with troops to restore order. Effective but may damage your reputation and reduce support.");
|
||||
registry.RegisterTutorial(
|
||||
handleRiotCrackDown,
|
||||
"command_panel_HandleRiotCrackDownAvailableCommand");
|
||||
|
||||
// Handle Riot - Do Nothing command
|
||||
var handleRiotDoNothing = CreateCommandPanelTutorial(
|
||||
"command_handle_riot_do_nothing",
|
||||
"Ignore Riot",
|
||||
"Choose to ignore the riot and let it run its course.\n\n" +
|
||||
"The unrest may subside on its own, or it may worsen. A risky choice that avoids immediate costs.");
|
||||
registry.RegisterTutorial(
|
||||
handleRiotDoNothing,
|
||||
"command_panel_HandleRiotDoNothingAvailableCommand");
|
||||
|
||||
// Handle Riot - Give command
|
||||
var handleRiotGive = CreateCommandPanelTutorial(
|
||||
"command_handle_riot_give",
|
||||
"Give to Populace",
|
||||
"Appease the rioters with gifts of food or gold.\n\n" +
|
||||
"A peaceful solution that costs resources but preserves your reputation. Choose how much to give.");
|
||||
registry.RegisterTutorial(
|
||||
handleRiotGive,
|
||||
"command_panel_HandleRiotGiveAvailableCommand");
|
||||
|
||||
// Attack Decision command
|
||||
var attackDecision = CreateCommandPanelTutorial(
|
||||
"command_attack_decision",
|
||||
"Attack Decision",
|
||||
"Your army has encountered enemy forces! Choose your action.\n\n" +
|
||||
"Advance to attack, demand tribute, request safe passage, or withdraw. Consider your strength before engaging.");
|
||||
registry.RegisterTutorial(attackDecision, "command_panel_AttackDecisionCommand");
|
||||
|
||||
// Free For All Decision command
|
||||
var freeForAllDecision = CreateCommandPanelTutorial(
|
||||
"command_free_for_all_decision",
|
||||
"Free-For-All Battle",
|
||||
"Multiple armies have converged! A chaotic battle is imminent.\n\n" +
|
||||
"You must decide whether to join the fray or attempt to withdraw from this dangerous situation.");
|
||||
registry.RegisterTutorial(
|
||||
freeForAllDecision,
|
||||
"command_panel_FreeForAllDecisionCommand");
|
||||
|
||||
// Resolve Tribute command
|
||||
var resolveTribute = CreateCommandPanelTutorial(
|
||||
"command_resolve_tribute",
|
||||
"Tribute Demanded",
|
||||
"Another faction is demanding tribute from you.\n\n" +
|
||||
"You can pay to avoid conflict, refuse and risk war, or imprison the messenger (a hostile act).");
|
||||
registry.RegisterTutorial(resolveTribute, "command_panel_ResolveTributeCommand");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a command panel tutorial with onboarding as a prerequisite.
|
||||
/// </summary>
|
||||
private static TutorialSequence
|
||||
CreateCommandPanelTutorial(string id, string title, string description) {
|
||||
var sequence = ScriptableObject.CreateInstance<TutorialSequence>();
|
||||
sequence.SequenceId = id;
|
||||
sequence.DisplayName = title;
|
||||
sequence.IsOnboarding = false;
|
||||
|
||||
// Require onboarding completion before showing command tutorials
|
||||
sequence.RequiredCompletedTutorials = new List<string> { "onboarding" };
|
||||
|
||||
sequence.Steps = new List<TutorialStep> { new TutorialStep {
|
||||
StepId = id + "_step",
|
||||
Title = title,
|
||||
Description = description,
|
||||
DisplayMode = TutorialDisplayMode.Modal,
|
||||
BlocksInteraction = false,
|
||||
// Position above the command selector panel
|
||||
PanelAnchor = TutorialPanelAnchor.Top,
|
||||
AdjacentTargetPath = "CommandPanel",
|
||||
CompletionType = TutorialCompletionType.ButtonClick,
|
||||
AllowSkip = true
|
||||
} };
|
||||
|
||||
return sequence;
|
||||
}
|
||||
|
||||
// ========== STRATEGIC CONTEXTUAL TUTORIALS ==========
|
||||
|
||||
private static void RegisterStrategicTutorials(TutorialTriggerRegistry registry) {
|
||||
// Diplomacy introduction
|
||||
var diplomacy = CreateSingleStepTutorial(
|
||||
"diplomacy_intro",
|
||||
"Diplomacy",
|
||||
"You can negotiate with other factions!\n\nOffer alliances, declare war, or propose tribute. Your diplomatic choices shape the realm.",
|
||||
TutorialDisplayMode.Modal);
|
||||
registry.RegisterTutorial(diplomacy, "diplomacy_available");
|
||||
|
||||
// Hero recruitment
|
||||
var heroRecruitment = CreateSingleStepTutorial(
|
||||
"hero_recruitment",
|
||||
@@ -283,6 +685,47 @@ namespace Eagle0.Tutorial {
|
||||
registry.RegisterTutorial(charge, "ability_charge_available");
|
||||
}
|
||||
|
||||
// ========== GAME STATE GUIDANCE TUTORIALS ==========
|
||||
// These trigger based on complex game state analysis, not simple first-encounters.
|
||||
|
||||
private static void RegisterGuidanceTutorials(TutorialTriggerRegistry registry) {
|
||||
// Loyalty danger warning (November)
|
||||
var loyaltyDanger = CreateSingleStepTutorial(
|
||||
"guidance_loyalty_danger",
|
||||
"Loyalty Warning!",
|
||||
"Some of your heroes are unhappy. At year's end, disloyal heroes may leave your service—or worse, betray you.\n\n" +
|
||||
"Use <b>Feast</b> to boost loyalty and vigor for all heroes in a province, or <b>Give Gift</b> to greatly increase one hero's loyalty.",
|
||||
TutorialDisplayMode.Modal);
|
||||
registry.RegisterTutorial(loyaltyDanger, "guidance_loyalty_danger");
|
||||
|
||||
// Neighbor danger warning
|
||||
var neighborDanger = CreateSingleStepTutorial(
|
||||
"guidance_neighbor_danger",
|
||||
"Border Alert!",
|
||||
"Your province borders hostile territory. An enemy army could march in at any time.\n\n" +
|
||||
"Consider <b>Organizing Troops</b> for defense, or use <b>Diplomacy</b> to seek a Truce or Alliance with your neighbor.",
|
||||
TutorialDisplayMode.Modal);
|
||||
registry.RegisterTutorial(neighborDanger, "guidance_neighbor_danger");
|
||||
|
||||
// Recruit heroes guidance
|
||||
var recruitHeroes = CreateSingleStepTutorial(
|
||||
"guidance_recruit_heroes",
|
||||
"Grow Your Court",
|
||||
"Free heroes are nearby, seeking a lord to serve. More heroes means more armies and more provinces you can manage.\n\n" +
|
||||
"Use <b>Travel</b> to visit town, then <b>Divine</b> to learn what a hero desires, or <b>Recruit</b> those already willing to join.",
|
||||
TutorialDisplayMode.Modal);
|
||||
registry.RegisterTutorial(recruitHeroes, "guidance_recruit_heroes");
|
||||
|
||||
// Expand guidance
|
||||
var expand = CreateSingleStepTutorial(
|
||||
"guidance_expand",
|
||||
"Time to Expand",
|
||||
"Your province is stable. To grow your realm, you'll need to claim new territory.\n\n" +
|
||||
"Use <b>March</b> to send your Warlord and another hero to a neighboring province. Once there, develop it just like your first province.",
|
||||
TutorialDisplayMode.Modal);
|
||||
registry.RegisterTutorial(expand, "guidance_expand");
|
||||
}
|
||||
|
||||
// ========== HELPER METHODS ==========
|
||||
|
||||
private static TutorialSequence CreateSingleStepTutorial(
|
||||
|
||||
+31
@@ -64,6 +64,37 @@ namespace Eagle0.Tutorial {
|
||||
/// </summary>
|
||||
public int StepCount => Steps?.Count ?? 0;
|
||||
|
||||
/// <summary>
|
||||
/// Number of visible steps up to and including the first MarksOnboardingComplete step.
|
||||
/// This gives an accurate count for the current "phase" of onboarding.
|
||||
/// </summary>
|
||||
public int VisibleStepCount {
|
||||
get {
|
||||
if (Steps == null) return 0;
|
||||
int count = 0;
|
||||
foreach (var step in Steps) {
|
||||
if (step.DisplayMode != TutorialDisplayMode.None) count++;
|
||||
// Stop counting after the step that marks onboarding complete
|
||||
if (step.MarksOnboardingComplete) break;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the visible step index for a given actual step index.
|
||||
/// Returns 0-based index among visible steps only.
|
||||
/// </summary>
|
||||
public int GetVisibleStepIndex(int actualIndex) {
|
||||
if (Steps == null || actualIndex < 0) return 0;
|
||||
|
||||
int visibleIndex = 0;
|
||||
for (int i = 0; i < actualIndex && i < Steps.Count; i++) {
|
||||
if (Steps[i].DisplayMode != TutorialDisplayMode.None) visibleIndex++;
|
||||
}
|
||||
return visibleIndex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if all prerequisites are met.
|
||||
/// </summary>
|
||||
|
||||
+39
@@ -22,6 +22,26 @@ namespace Eagle0.Tutorial {
|
||||
None
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Where to position the tutorial panel on screen.
|
||||
/// </summary>
|
||||
public enum TutorialPanelAnchor {
|
||||
/// <summary>Centered on screen (default modal behavior).</summary>
|
||||
Center,
|
||||
|
||||
/// <summary>Panel anchored to left side of screen.</summary>
|
||||
Left,
|
||||
|
||||
/// <summary>Panel anchored to right side of screen.</summary>
|
||||
Right,
|
||||
|
||||
/// <summary>Panel anchored to top of screen.</summary>
|
||||
Top,
|
||||
|
||||
/// <summary>Panel anchored to bottom of screen.</summary>
|
||||
Bottom
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// How the tutorial step is completed/advanced.
|
||||
/// </summary>
|
||||
@@ -66,16 +86,31 @@ namespace Eagle0.Tutorial {
|
||||
[Tooltip("How this step should be displayed")]
|
||||
public TutorialDisplayMode DisplayMode = TutorialDisplayMode.Modal;
|
||||
|
||||
[Tooltip("Whether this step blocks game interaction (false = user can interact with game)")]
|
||||
public bool BlocksInteraction = true;
|
||||
|
||||
[Tooltip("Where to position the tutorial panel on screen")]
|
||||
public TutorialPanelAnchor PanelAnchor = TutorialPanelAnchor.Center;
|
||||
|
||||
[Header("UI Targeting")]
|
||||
[Tooltip("Path to GameObject to highlight (e.g., 'Canvas/CommandPanel/MarchButton')")]
|
||||
public string TargetGameObjectPath;
|
||||
|
||||
[Tooltip("Additional targets to highlight together (combined bounding box)")]
|
||||
public string[] AdditionalHighlightTargets;
|
||||
|
||||
[Tooltip("Path to GameObject to position the panel adjacent to (for non-blocking modals)")]
|
||||
public string AdjacentTargetPath;
|
||||
|
||||
[Tooltip("Offset from target element for tooltip/arrow positioning")]
|
||||
public Vector2 HighlightOffset;
|
||||
|
||||
[Tooltip("Whether the highlight should pulse")]
|
||||
public bool HighlightPulsing = true;
|
||||
|
||||
[Tooltip("Compute highlight bounds from active children instead of target's RectTransform")]
|
||||
public bool HighlightBoundsFromChildren;
|
||||
|
||||
[Header("Completion")]
|
||||
[Tooltip("How this step is completed")]
|
||||
public TutorialCompletionType CompletionType = TutorialCompletionType.ButtonClick;
|
||||
@@ -93,6 +128,10 @@ namespace Eagle0.Tutorial {
|
||||
[Tooltip("Jump to specific step ID instead of sequential (empty = next step)")]
|
||||
public string NextStepOverride;
|
||||
|
||||
[Tooltip(
|
||||
"Marks onboarding as complete when this step finishes (for mid-sequence completion)")]
|
||||
public bool MarksOnboardingComplete;
|
||||
|
||||
[Header("Audio")]
|
||||
[Tooltip("Optional narration audio")]
|
||||
public AudioClip NarrationClip;
|
||||
|
||||
+170
@@ -80,6 +80,36 @@ namespace Eagle0.Tutorial {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Command Panel Events ==========
|
||||
|
||||
// Track the last command type shown so we can re-trigger after onboarding
|
||||
private AvailableCommand.SealedValueOneofCase? _lastCommandTypeShown;
|
||||
|
||||
/// <summary>
|
||||
/// Called when a command panel is shown in the strategic view.
|
||||
/// Triggers contextual tutorials for each command type.
|
||||
/// </summary>
|
||||
public void OnCommandPanelShown(AvailableCommand.SealedValueOneofCase commandType) {
|
||||
_lastCommandTypeShown = commandType;
|
||||
string eventId = $"command_panel_{commandType}";
|
||||
OnGameEvent(eventId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Re-triggers the last command panel tutorial.
|
||||
/// Called after onboarding completes to show tutorial for already-selected command.
|
||||
/// </summary>
|
||||
public void RetriggerLastCommandTutorial() {
|
||||
if (_lastCommandTypeShown.HasValue) {
|
||||
Debug.Log(
|
||||
$"[Tutorial] RetriggerLastCommandTutorial: Triggering for {_lastCommandTypeShown.Value}");
|
||||
string eventId = $"command_panel_{_lastCommandTypeShown.Value}";
|
||||
OnGameEvent(eventId);
|
||||
} else {
|
||||
Debug.Log("[Tutorial] RetriggerLastCommandTutorial: No last command type recorded");
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Strategic Layer Events ==========
|
||||
|
||||
/// <summary>
|
||||
@@ -104,6 +134,9 @@ namespace Eagle0.Tutorial {
|
||||
|
||||
// Check for free heroes (recruitment opportunity)
|
||||
CheckHeroRecruitmentAvailable(model, previousModel);
|
||||
|
||||
// Check game state guidance triggers
|
||||
CheckGuidanceTriggers(model);
|
||||
}
|
||||
|
||||
private void CheckStrategicCommandsAvailable(IGameModel model, IGameModel previousModel) {
|
||||
@@ -143,6 +176,143 @@ namespace Eagle0.Tutorial {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Game State Guidance Triggers ==========
|
||||
|
||||
private void CheckGuidanceTriggers(IGameModel model) {
|
||||
CheckLoyaltyDanger(model);
|
||||
CheckNeighborDanger(model);
|
||||
CheckRecruitHeroesGuidance(model);
|
||||
CheckExpandGuidance(model);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if it's November and any heroes have low loyalty.
|
||||
/// </summary>
|
||||
private void CheckLoyaltyDanger(IGameModel model) {
|
||||
if (_manager.State.HasCompletedTutorial("guidance_loyalty_danger")) return;
|
||||
|
||||
// Only warn in November (month 11)
|
||||
if (model.CurrentDate?.Month != 11) return;
|
||||
|
||||
// Get the player's faction ID
|
||||
var playerId = model.PlayerId;
|
||||
if (playerId == null) return;
|
||||
|
||||
// Check all heroes belonging to the player's faction
|
||||
var hasLowLoyaltyHero = model.Heroes?.Values.Any(
|
||||
h => h.FactionId == playerId && h.Loyalty != null && h.Loyalty.Stat < 70);
|
||||
|
||||
if (hasLowLoyaltyHero == true) { OnGameEvent("guidance_loyalty_danger"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if player has provinces adjacent to hostile factions.
|
||||
/// </summary>
|
||||
private void CheckNeighborDanger(IGameModel model) {
|
||||
if (_manager.State.HasCompletedTutorial("guidance_neighbor_danger")) return;
|
||||
|
||||
var playerId = model.PlayerId;
|
||||
if (playerId == null) return;
|
||||
|
||||
// Get player's faction relationships
|
||||
if (!model.ActiveFactions.TryGetValue(playerId.Value, out var playerFaction)) return;
|
||||
|
||||
// Check each province the player owns
|
||||
foreach (var province in model.Provinces.Values) {
|
||||
if (province.RulingFactionId != playerId) continue;
|
||||
|
||||
// Check available march destinations for hostile neighbors
|
||||
if (model.AvailableCommandsByProvince == null) continue;
|
||||
if (!model.AvailableCommandsByProvince.TryGetValue(province.Id, out var commands))
|
||||
continue;
|
||||
|
||||
var marchCommand = commands.Commands.FirstOrDefault(c => c.MarchCommand != null);
|
||||
if (marchCommand?.MarchCommand == null) continue;
|
||||
|
||||
// Check all one-province march commands for adjacent hostile provinces
|
||||
foreach (var oneProvinceCmd in marchCommand.MarchCommand.OneProvinceCommands) {
|
||||
foreach (var dest in oneProvinceCmd.AvailableDestinationProvinces) {
|
||||
var destProvince = model.Provinces[dest.ProvinceId];
|
||||
if (destProvince.RulingFactionId == null ||
|
||||
destProvince.RulingFactionId == playerId)
|
||||
continue;
|
||||
|
||||
// Check if we have a truce or alliance with this faction
|
||||
var relationship = playerFaction.FactionRelationships.FirstOrDefault(
|
||||
fr => fr.TargetFactionId == destProvince.RulingFactionId);
|
||||
|
||||
if (relationship == null ||
|
||||
relationship.RelationshipLevel ==
|
||||
Net.Eagle0.Eagle.Views.FactionRelationshipView.Types
|
||||
.RelationshipLevel.Hostile) {
|
||||
// Found a hostile neighbor!
|
||||
OnGameEvent("guidance_neighbor_danger");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if player should be recruiting heroes.
|
||||
/// </summary>
|
||||
private void CheckRecruitHeroesGuidance(IGameModel model) {
|
||||
if (_manager.State.HasCompletedTutorial("guidance_recruit_heroes")) return;
|
||||
|
||||
var playerId = model.PlayerId;
|
||||
if (playerId == null) return;
|
||||
|
||||
// Check for a province with support >= 40 that has free heroes
|
||||
foreach (var province in model.Provinces.Values) {
|
||||
if (province.RulingFactionId != playerId) continue;
|
||||
if (province.FullInfo?.Support?.Stat < 40) continue;
|
||||
|
||||
// Need at least 100 gold in the province to divine
|
||||
if (province.FullInfo?.Gold < 100) continue;
|
||||
|
||||
// Check for free heroes in this province
|
||||
var freeHeroes = province.FullInfo?.UnaffiliatedHeroes;
|
||||
if (freeHeroes == null || freeHeroes.Count == 0) continue;
|
||||
|
||||
// Check if any are divineable or ready to join
|
||||
var recruitableHeroes = freeHeroes.Where(
|
||||
uh => uh.RecruitmentInfo?.Status ==
|
||||
Net.Eagle0.Eagle.Common.RecruitmentStatus.WouldJoin ||
|
||||
uh.RecruitmentInfo?.Status ==
|
||||
Net.Eagle0.Eagle.Common.RecruitmentStatus.NotDivined);
|
||||
|
||||
if (recruitableHeroes.Any()) {
|
||||
OnGameEvent("guidance_recruit_heroes");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if player should consider expanding to new provinces.
|
||||
/// </summary>
|
||||
private void CheckExpandGuidance(IGameModel model) {
|
||||
if (_manager.State.HasCompletedTutorial("guidance_expand")) return;
|
||||
|
||||
var playerId = model.PlayerId;
|
||||
if (playerId == null) return;
|
||||
|
||||
// Check if player has any province with support >= 40
|
||||
var stableProvince = model.Provinces.Values.FirstOrDefault(
|
||||
p => p.RulingFactionId == playerId && p.FullInfo?.Support?.Stat >= 40);
|
||||
|
||||
if (stableProvince == null) return;
|
||||
|
||||
// Check if player only has one province (hasn't expanded yet)
|
||||
var playerProvinceCount =
|
||||
model.Provinces.Values.Count(p => p.RulingFactionId == playerId);
|
||||
if (playerProvinceCount > 1) return;
|
||||
|
||||
// Player has one stable province but hasn't expanded - suggest expansion
|
||||
OnGameEvent("guidance_expand");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when a province is selected.
|
||||
/// </summary>
|
||||
|
||||
+63
-10
@@ -23,6 +23,9 @@ namespace Eagle0.Tutorial {
|
||||
[Tooltip("Reference to the tutorial UI manager")]
|
||||
public TutorialUIManager UIManager;
|
||||
|
||||
[Tooltip("Registry of UI targets for tutorials")]
|
||||
public TutorialTargetRegistry TargetRegistry;
|
||||
|
||||
[Header("Debug")]
|
||||
[Tooltip("Enable verbose logging")]
|
||||
public bool DebugLogging;
|
||||
@@ -147,15 +150,48 @@ namespace Eagle0.Tutorial {
|
||||
/// </summary>
|
||||
public void TriggerContextualTutorial(string tutorialId) {
|
||||
if (!TutorialsEnabled || _state.AllTutorialsDisabled) return;
|
||||
if (_state.HasCompletedTutorial(tutorialId)) return;
|
||||
|
||||
// Don't interrupt an active tutorial with a contextual one
|
||||
// (onboarding can be interrupted by explicit StartSequence calls)
|
||||
if (_activeSequence != null) {
|
||||
Log($"TriggerContextualTutorial: Skipping '{tutorialId}' - another tutorial is active");
|
||||
bool isAlreadyCompleted = _state.HasCompletedTutorial(tutorialId);
|
||||
bool isCommandTutorial = tutorialId.StartsWith("command_");
|
||||
bool activeIsCommandTutorial =
|
||||
_activeSequence?.SequenceId.StartsWith("command_") ?? false;
|
||||
|
||||
// If switching to a completed command tutorial while another command tutorial
|
||||
// is showing, just hide the current one (don't mark it complete)
|
||||
if (isAlreadyCompleted && isCommandTutorial && activeIsCommandTutorial) {
|
||||
Log($"TriggerContextualTutorial: '{tutorialId}' already completed, " +
|
||||
$"hiding active command tutorial '{_activeSequence.SequenceId}'");
|
||||
StopCurrentSequence(skipped: true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isAlreadyCompleted) return;
|
||||
|
||||
// Check if we should allow interruption
|
||||
if (_activeSequence != null) {
|
||||
var currentStep = CurrentStep;
|
||||
bool isHiddenStep = currentStep?.DisplayMode == TutorialDisplayMode.None;
|
||||
|
||||
// Allow interruption if:
|
||||
// 1. Current step is hidden (DisplayMode.None) - waiting for async events
|
||||
// 2. Both tutorials are command tutorials - allow switching between commands
|
||||
bool bothAreCommandTutorials = isCommandTutorial && activeIsCommandTutorial;
|
||||
|
||||
if (!isHiddenStep && !bothAreCommandTutorials) {
|
||||
Log($"TriggerContextualTutorial: Skipping '{tutorialId}' - another tutorial is active");
|
||||
return;
|
||||
}
|
||||
|
||||
if (bothAreCommandTutorials && _activeSequence.SequenceId == tutorialId) {
|
||||
// Same command tutorial already showing, don't restart
|
||||
return;
|
||||
}
|
||||
|
||||
Log($"TriggerContextualTutorial: Allowing '{tutorialId}' to interrupt " +
|
||||
(isHiddenStep ? "hidden step"
|
||||
: $"command tutorial '{_activeSequence.SequenceId}'"));
|
||||
}
|
||||
|
||||
// Look up the tutorial sequence by ID
|
||||
var sequence = _triggerRegistry?.GetSequenceById(tutorialId);
|
||||
if (sequence != null) { StartSequence(sequence); }
|
||||
@@ -168,6 +204,18 @@ namespace Eagle0.Tutorial {
|
||||
if (_activeSequence == null) return;
|
||||
|
||||
var currentStep = CurrentStep;
|
||||
|
||||
// Check if this step marks onboarding as complete (mid-sequence)
|
||||
bool shouldRetriggerCommandTutorial = false;
|
||||
if (currentStep != null && currentStep.MarksOnboardingComplete &&
|
||||
_activeSequence.IsOnboarding) {
|
||||
Log("Marking onboarding complete (mid-sequence)");
|
||||
_state.CompleteOnboarding();
|
||||
_state.MarkTutorialCompleted(_activeSequence.SequenceId);
|
||||
// Defer re-triggering until after we advance to the hidden step
|
||||
shouldRetriggerCommandTutorial = true;
|
||||
}
|
||||
|
||||
string nextStepId = currentStep?.NextStepOverride;
|
||||
|
||||
// Determine next step index
|
||||
@@ -192,6 +240,11 @@ namespace Eagle0.Tutorial {
|
||||
|
||||
Log($"Advanced to step {_currentStepIndex}: '{CurrentStep?.StepId}'");
|
||||
ShowCurrentStep();
|
||||
|
||||
// Now that we've advanced to the hidden step, re-trigger command tutorial
|
||||
if (shouldRetriggerCommandTutorial) {
|
||||
_triggerRegistry?.RetriggerLastCommandTutorial();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -297,12 +350,14 @@ namespace Eagle0.Tutorial {
|
||||
// Handle different display modes
|
||||
switch (step.DisplayMode) {
|
||||
case TutorialDisplayMode.Modal:
|
||||
// Use visible step index/count for better UX (excludes hidden steps)
|
||||
int visibleIndex = _activeSequence.GetVisibleStepIndex(_currentStepIndex);
|
||||
int visibleCount = _activeSequence.VisibleStepCount;
|
||||
UIManager?.ShowModal(
|
||||
step,
|
||||
_currentStepIndex,
|
||||
_activeSequence.StepCount,
|
||||
visibleIndex,
|
||||
visibleCount,
|
||||
OnStepComplete,
|
||||
OnStepSkip,
|
||||
_activeSequence.IsOnboarding);
|
||||
break;
|
||||
|
||||
@@ -343,8 +398,6 @@ namespace Eagle0.Tutorial {
|
||||
|
||||
private void OnStepComplete() { AdvanceStep(); }
|
||||
|
||||
private void OnStepSkip() { SkipCurrentStep(); }
|
||||
|
||||
private void CompleteCurrentSequence() {
|
||||
if (_activeSequence == null) return;
|
||||
|
||||
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Eagle0.Tutorial {
|
||||
/// <summary>
|
||||
/// Registry of UI elements that tutorials can reference.
|
||||
/// Assign static targets via Inspector, register dynamic targets via code.
|
||||
/// </summary>
|
||||
public class TutorialTargetRegistry : MonoBehaviour {
|
||||
[Header("Province Info Panel")]
|
||||
[Tooltip("The main Province Info panel")]
|
||||
public RectTransform ProvinceInfoPanel;
|
||||
|
||||
[Tooltip("The Support value display")]
|
||||
public RectTransform SupportField;
|
||||
|
||||
[Tooltip("The Agriculture value display")]
|
||||
public RectTransform AgricultureField;
|
||||
|
||||
[Tooltip("The Economy value display")]
|
||||
public RectTransform EconomyField;
|
||||
|
||||
[Tooltip("The Infrastructure value display")]
|
||||
public RectTransform InfrastructureField;
|
||||
|
||||
[Header("Heroes Panel")]
|
||||
[Tooltip("The Resident Heroes panel")]
|
||||
public RectTransform HeroesPanel;
|
||||
// Note: WarlordRow, VassalRow1, VassalRow2 are registered dynamically
|
||||
// by HeroesAndBattalionsPanelController when hero rows are created
|
||||
|
||||
[Header("Command Panel")]
|
||||
[Tooltip("The command selector panel (parent of individual command selectors)")]
|
||||
public RectTransform CommandPanel;
|
||||
|
||||
[Header("Command Buttons")]
|
||||
[Tooltip("Container for command buttons")]
|
||||
public RectTransform CommandButtonsPanel;
|
||||
|
||||
[Tooltip("The Commit button")]
|
||||
public RectTransform CommitButton;
|
||||
|
||||
// Dynamic targets registered at runtime (e.g., command buttons from prefabs)
|
||||
private Dictionary<string, RectTransform> _dynamicTargets =
|
||||
new Dictionary<string, RectTransform>();
|
||||
|
||||
/// <summary>
|
||||
/// Registers a target at runtime. Use for prefab-instantiated UI elements.
|
||||
/// </summary>
|
||||
public void RegisterTarget(string targetId, RectTransform target) {
|
||||
if (string.IsNullOrEmpty(targetId) || target == null) return;
|
||||
_dynamicTargets[targetId] = target;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters a dynamically registered target.
|
||||
/// </summary>
|
||||
public void UnregisterTarget(string targetId) {
|
||||
if (!string.IsNullOrEmpty(targetId)) { _dynamicTargets.Remove(targetId); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a target by its string ID.
|
||||
/// Checks static Inspector fields first, then dynamic registrations.
|
||||
/// </summary>
|
||||
public RectTransform GetTarget(string targetId) {
|
||||
if (string.IsNullOrEmpty(targetId)) return null;
|
||||
|
||||
// Check static targets first
|
||||
var staticTarget = targetId switch { "ProvinceInfoPanel" => ProvinceInfoPanel,
|
||||
"SupportField" => SupportField,
|
||||
"AgricultureField" => AgricultureField,
|
||||
"EconomyField" => EconomyField,
|
||||
"InfrastructureField" => InfrastructureField,
|
||||
"HeroesPanel" => HeroesPanel,
|
||||
"CommandPanel" => CommandPanel,
|
||||
"CommandButtonsPanel" => CommandButtonsPanel,
|
||||
"CommitButton" => CommitButton,
|
||||
_ => null };
|
||||
|
||||
if (staticTarget != null) return staticTarget;
|
||||
|
||||
// Check dynamic targets
|
||||
if (_dynamicTargets.TryGetValue(targetId, out var dynamicTarget)) {
|
||||
return dynamicTarget;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a target, with fallback to GameObject.Find for unregistered paths.
|
||||
/// </summary>
|
||||
public Transform GetTargetOrFind(string targetId) {
|
||||
var registered = GetTarget(targetId);
|
||||
if (registered != null) return registered;
|
||||
|
||||
// Fallback to path-based lookup for unregistered targets
|
||||
var found = GameObject.Find(targetId);
|
||||
if (found != null) {
|
||||
Debug.LogWarning(
|
||||
$"TutorialTargetRegistry: '{targetId}' not registered, using GameObject.Find fallback");
|
||||
return found.transform;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: be44269baefd349e99d5ed5f6004234b
|
||||
+16
-28
@@ -85,7 +85,7 @@ namespace Eagle0.Tutorial {
|
||||
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.sizeDelta = new Vector2(540, 500);
|
||||
panelRect.anchoredPosition = Vector2.zero;
|
||||
|
||||
// Panel Background
|
||||
@@ -100,8 +100,8 @@ namespace Eagle0.Tutorial {
|
||||
|
||||
// Add vertical layout group
|
||||
VerticalLayoutGroup layout = panel.AddComponent<VerticalLayoutGroup>();
|
||||
layout.padding = new RectOffset(40, 40, 30, 30);
|
||||
layout.spacing = 20;
|
||||
layout.padding = new RectOffset(20, 20, 15, 15);
|
||||
layout.spacing = 10;
|
||||
layout.childAlignment = TextAnchor.UpperCenter;
|
||||
layout.childControlHeight = false;
|
||||
layout.childControlWidth = true;
|
||||
@@ -134,18 +134,18 @@ namespace Eagle0.Tutorial {
|
||||
titleObj.transform.SetParent(parent, false);
|
||||
|
||||
RectTransform rect = titleObj.AddComponent<RectTransform>();
|
||||
rect.sizeDelta = new Vector2(720, 60);
|
||||
rect.sizeDelta = new Vector2(460, 36);
|
||||
|
||||
TextMeshProUGUI text = titleObj.AddComponent<TextMeshProUGUI>();
|
||||
text.text = "Tutorial";
|
||||
text.fontSize = 42;
|
||||
text.fontSize = 28;
|
||||
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;
|
||||
layoutElement.preferredHeight = 36;
|
||||
}
|
||||
|
||||
private static void CreateIcon(Transform parent) {
|
||||
@@ -172,11 +172,11 @@ namespace Eagle0.Tutorial {
|
||||
descObj.transform.SetParent(parent, false);
|
||||
|
||||
RectTransform rect = descObj.AddComponent<RectTransform>();
|
||||
rect.sizeDelta = new Vector2(720, 200);
|
||||
rect.sizeDelta = new Vector2(460, 180);
|
||||
|
||||
TextMeshProUGUI text = descObj.AddComponent<TextMeshProUGUI>();
|
||||
text.text = "";
|
||||
text.fontSize = 26;
|
||||
text.fontSize = 18;
|
||||
text.color = TextColor;
|
||||
text.alignment = TextAlignmentOptions.TopLeft;
|
||||
text.enableWordWrapping = true;
|
||||
@@ -184,8 +184,8 @@ namespace Eagle0.Tutorial {
|
||||
if (font != null) text.font = font;
|
||||
|
||||
LayoutElement layoutElement = descObj.AddComponent<LayoutElement>();
|
||||
layoutElement.preferredHeight = 200;
|
||||
layoutElement.minHeight = 100;
|
||||
layoutElement.preferredHeight = 180;
|
||||
layoutElement.minHeight = 80;
|
||||
}
|
||||
|
||||
private static void CreateSpacer(Transform parent, float height) {
|
||||
@@ -233,7 +233,7 @@ namespace Eagle0.Tutorial {
|
||||
|
||||
TextMeshProUGUI progressText = progressTextObj.AddComponent<TextMeshProUGUI>();
|
||||
progressText.text = "Step 1 of 1";
|
||||
progressText.fontSize = 20;
|
||||
progressText.fontSize = 14;
|
||||
progressText.color = TextColor;
|
||||
progressText.alignment = TextAlignmentOptions.Center;
|
||||
if (font != null) progressText.font = font;
|
||||
@@ -308,10 +308,6 @@ namespace Eagle0.Tutorial {
|
||||
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);
|
||||
@@ -334,13 +330,10 @@ namespace Eagle0.Tutorial {
|
||||
layout.childForceExpandWidth = false;
|
||||
|
||||
LayoutElement layoutElement = buttonRow.AddComponent<LayoutElement>();
|
||||
layoutElement.preferredHeight = 60;
|
||||
layoutElement.preferredHeight = 44;
|
||||
|
||||
// Skip Button (left)
|
||||
CreateButton(buttonRow.transform, "SkipButton", "Skip", 150, 50, font);
|
||||
|
||||
// Skip All Button
|
||||
CreateButton(buttonRow.transform, "SkipAllButton", "Skip All", 150, 50, font);
|
||||
// Skip Tutorial Button (left) - only shown during onboarding
|
||||
CreateButton(buttonRow.transform, "SkipAllButton", "Skip Tutorial", 140, 36, font);
|
||||
|
||||
// Spacer
|
||||
GameObject spacer = new GameObject("ButtonSpacer");
|
||||
@@ -350,7 +343,7 @@ namespace Eagle0.Tutorial {
|
||||
spacerLayout.flexibleWidth = 1;
|
||||
|
||||
// Continue Button (right)
|
||||
CreateButton(buttonRow.transform, "ContinueButton", "Continue", 180, 50, font);
|
||||
CreateButton(buttonRow.transform, "ContinueButton", "Continue", 120, 36, font);
|
||||
}
|
||||
|
||||
private static GameObject CreateButton(
|
||||
@@ -398,7 +391,7 @@ namespace Eagle0.Tutorial {
|
||||
|
||||
TextMeshProUGUI buttonText = textObj.AddComponent<TextMeshProUGUI>();
|
||||
buttonText.text = text;
|
||||
buttonText.fontSize = 24;
|
||||
buttonText.fontSize = 16;
|
||||
buttonText.color = ButtonTextColor;
|
||||
buttonText.alignment = TextAlignmentOptions.Center;
|
||||
if (font != null) buttonText.font = font;
|
||||
@@ -450,11 +443,6 @@ namespace Eagle0.Tutorial {
|
||||
// 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>();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user