mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-29 06:35:41 +00:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0484a74fa | ||
|
|
672cb5cb20 | ||
|
|
816d2b6859 | ||
|
|
19b305829b | ||
|
|
3c382349e6 |
@@ -36,7 +36,3 @@ common --java_language_version=17
|
||||
common --java_runtime_version=remotejdk_17
|
||||
common --tool_java_language_version=17
|
||||
common --tool_java_runtime_version=remotejdk_17
|
||||
|
||||
# Workspace status for build stamping (git commit, timestamp)
|
||||
common --workspace_status_command=tools/workspace_status.sh
|
||||
common --stamp
|
||||
|
||||
@@ -348,86 +348,15 @@ jobs:
|
||||
echo "Copying to :latest tag"
|
||||
$CRANE copy "$IMAGE_TAG" "registry.digitalocean.com/eagle0/admin-server:latest"
|
||||
|
||||
build-jfr-sidecar:
|
||||
runs-on: self-hosted
|
||||
outputs:
|
||||
image_tag: ${{ steps.push-jfr-sidecar.outputs.image_tag }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: false
|
||||
|
||||
- name: Build JFR Sidecar Docker image
|
||||
id: build-jfr-sidecar
|
||||
run: |
|
||||
set -ex
|
||||
|
||||
# Build JFR sidecar image (Go binary has explicit goos/goarch in BUILD.bazel)
|
||||
bazel build //ci:jfr_sidecar_image
|
||||
|
||||
# Save the resolved path before any other bazel command changes bazel-bin symlink
|
||||
IMAGE_PATH=$(readlink -f bazel-bin/ci/jfr_sidecar_image)
|
||||
echo "Image path: $IMAGE_PATH"
|
||||
echo "image_path=$IMAGE_PATH" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Login to DigitalOcean Container Registry
|
||||
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
|
||||
env:
|
||||
DO_TOKEN: ${{ secrets.DO_REGISTRY_TOKEN }}
|
||||
run: |
|
||||
mkdir -p ~/.docker
|
||||
AUTH=$(echo -n "${DO_TOKEN}:${DO_TOKEN}" | base64)
|
||||
echo "{\"auths\":{\"registry.digitalocean.com\":{\"auth\":\"${AUTH}\"}}}" > ~/.docker/config.json
|
||||
mkdir -p .docker
|
||||
cp ~/.docker/config.json .docker/
|
||||
|
||||
- name: Push JFR Sidecar image to DO registry
|
||||
id: push-jfr-sidecar
|
||||
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
|
||||
env:
|
||||
DOCKER_CONFIG: ${{ github.workspace }}/.docker
|
||||
run: |
|
||||
set -ex
|
||||
|
||||
JFR_IMAGE="${{ steps.build-jfr-sidecar.outputs.image_path }}"
|
||||
echo "Using JFR Sidecar image: $JFR_IMAGE"
|
||||
|
||||
if [ -z "$JFR_IMAGE" ] || [ ! -d "$JFR_IMAGE" ]; then
|
||||
echo "ERROR: JFR Sidecar image not found at: $JFR_IMAGE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build the push target to get crane in runfiles
|
||||
bazel build //ci:jfr_sidecar_push
|
||||
|
||||
# Use crane directly for push
|
||||
CRANE="bazel-bin/ci/push_jfr_sidecar_push.sh.runfiles/rules_oci~~oci~oci_crane_darwin_arm64/crane"
|
||||
echo "Using crane: $CRANE"
|
||||
|
||||
# Push with SHA tag
|
||||
GIT_SHA=$(git rev-parse --short=8 HEAD)
|
||||
IMAGE_TAG="registry.digitalocean.com/eagle0/jfr-sidecar:${GIT_SHA}"
|
||||
echo "Pushing JFR sidecar image: $IMAGE_TAG"
|
||||
$CRANE push "$JFR_IMAGE" "$IMAGE_TAG"
|
||||
|
||||
# Output the full image tag for deploy step
|
||||
echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT
|
||||
|
||||
# Also update :latest for convenience
|
||||
echo "Copying to :latest tag"
|
||||
$CRANE copy "$IMAGE_TAG" "registry.digitalocean.com/eagle0/jfr-sidecar:latest"
|
||||
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-eagle, build-shardok, build-admin, build-jfr-sidecar]
|
||||
needs: [build-eagle, build-shardok, build-admin]
|
||||
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
|
||||
environment: production
|
||||
env:
|
||||
EAGLE_IMAGE: ${{ needs.build-eagle.outputs.image_tag }}
|
||||
SHARDOK_IMAGE: ${{ needs.build-shardok.outputs.image_tag }}
|
||||
ADMIN_IMAGE: ${{ needs.build-admin.outputs.image_tag }}
|
||||
JFR_SIDECAR_IMAGE: ${{ needs.build-jfr-sidecar.outputs.image_tag }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
GPT_MODEL_NAME: ${{ secrets.GPT_MODEL_NAME }}
|
||||
EAGLE_ENABLE_S3: ${{ secrets.EAGLE_ENABLE_S3 }}
|
||||
@@ -453,7 +382,7 @@ jobs:
|
||||
username: deploy
|
||||
key: ${{ secrets.DO_SSH_KEY }}
|
||||
script_stop: true
|
||||
envs: EAGLE_IMAGE,SHARDOK_IMAGE,ADMIN_IMAGE,JFR_SIDECAR_IMAGE,OPENAI_API_KEY,GPT_MODEL_NAME,EAGLE_ENABLE_S3,DO_SPACES_ACCESS_KEY,DO_SPACES_SECRET_KEY
|
||||
envs: EAGLE_IMAGE,SHARDOK_IMAGE,ADMIN_IMAGE,OPENAI_API_KEY,GPT_MODEL_NAME,EAGLE_ENABLE_S3,DO_SPACES_ACCESS_KEY,DO_SPACES_SECRET_KEY
|
||||
script: |
|
||||
set -x
|
||||
cd /opt/eagle0
|
||||
@@ -473,7 +402,7 @@ jobs:
|
||||
echo "${{ secrets.DO_REGISTRY_TOKEN }}" | docker login registry.digitalocean.com -u "${{ secrets.DO_REGISTRY_TOKEN }}" --password-stdin
|
||||
|
||||
# Use exact image tags passed from build jobs (no :latest fallback)
|
||||
echo "Using images: $EAGLE_IMAGE, $SHARDOK_IMAGE, $ADMIN_IMAGE, $JFR_SIDECAR_IMAGE"
|
||||
echo "Using images: $EAGLE_IMAGE, $SHARDOK_IMAGE, $ADMIN_IMAGE"
|
||||
|
||||
# Use crane to pull images (handles OCI format correctly) then load into Docker
|
||||
# This avoids digest mismatch from DO registry's OCI->Docker format conversion
|
||||
@@ -500,12 +429,6 @@ jobs:
|
||||
docker load -i admin.tar
|
||||
rm admin.tar
|
||||
|
||||
echo "Pulling JFR Sidecar image with crane..."
|
||||
./crane pull "${JFR_SIDECAR_IMAGE}" jfr-sidecar.tar || { echo "ERROR: Failed to pull jfr-sidecar image"; exit 1; }
|
||||
echo "Loading JFR Sidecar image into Docker..."
|
||||
docker load -i jfr-sidecar.tar
|
||||
rm jfr-sidecar.tar
|
||||
|
||||
rm ./crane
|
||||
|
||||
# Also pull other compose images
|
||||
|
||||
@@ -52,14 +52,14 @@ jobs:
|
||||
- name: Persist Library/
|
||||
run: ./ci/github_actions/persist_library.sh
|
||||
- name: Deploy Windows unity
|
||||
if: success() && github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
if: success() #&& github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
env:
|
||||
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
run: bazel run //src/main/go/net/eagle0/build/unity3d_windows_build_handler:unity3d_windows_build_handler -- "/tmp/eagle0/eagle0WIN" "/tmp/unity_manifest.txt"
|
||||
|
||||
- name: Update unified manifest
|
||||
if: success() && github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
if: success() #&& github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
env:
|
||||
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
|
||||
+7
-45
@@ -8,18 +8,6 @@ This document tracks the migration from protobuf types to native Scala models in
|
||||
2. **Keep proto for Shardok communication**: Yes - protobuf is used for Eagle-Shardok gRPC communication
|
||||
3. **Use Scala views inside the library**: Yes - use native Scala types like `ProvinceView`, `FactionView`, `HeroView`, etc. within the library code
|
||||
|
||||
## Recent Completed Work
|
||||
|
||||
### GameState Round-Trip Elimination (PRs #4913, #4914, #4915)
|
||||
|
||||
Eliminated wasteful Scala→proto→Scala conversions in the hot path:
|
||||
|
||||
1. **LLM Pipeline** (#4913): `LlmRequestWithGameState` now uses Scala `GameState` instead of proto. All ~38 prompt generators updated to use Scala model types (`FactionT`, `HeroT`, `ProvinceT`).
|
||||
|
||||
2. **ActionWithResultingState Caching** (#4914): Added `precomputedScalaState: Option[GameState]` to cache Scala state when available, avoiding `fromProto()` conversion in `stateAfter()`.
|
||||
|
||||
3. **PostResults Simplification** (#4915): Changed `PostResults.gameState` from proto to `Option[GameState]` (Scala), eliminating `toProto()` calls when creating PostResults.
|
||||
|
||||
## Migration Pattern
|
||||
|
||||
The codebase follows a **Legacy* pattern** for separating proto-dependent and protoless code:
|
||||
@@ -48,7 +36,6 @@ When migrating a file:
|
||||
- [x] `ExpandCommandSelector`
|
||||
- [x] `HeroGiftCommandSelector`
|
||||
- [x] `ImproveCommandSelector`
|
||||
- [x] `MarchTowardProvinceCommandChooser` - in AI folder, uses native GameState (callers convert)
|
||||
- [x] `OrganizeCommandSelector`
|
||||
- [x] `RansomOfferHelpers`
|
||||
- [x] `SeekMoreLeadersCommandChooser` - in AI folder, uses native GameState
|
||||
@@ -67,47 +54,22 @@ When migrating a file:
|
||||
- [x] `TruceCountQuestCommandChooser`
|
||||
- [x] `TruceWithFactionQuestCommandChooser`
|
||||
|
||||
### Fully Protoless
|
||||
### Dual (both proto and protoless versions)
|
||||
|
||||
- [x] `AIClientUtils` - has protoless overloads (`takenHeroIdsForMarchTowardFocus`, `mostPowerfulHeroes`)
|
||||
- [x] `ProvinceGoldSurplusCalculator` - fully protoless (callers use converters)
|
||||
- [x] `HeroSelector` - fully protoless (removed dead `minimallyFatiguedHeroesProto`)
|
||||
- [~] `ProvinceGoldSurplusCalculator` - protoless `provinceGoldSurplus(province: ProvinceT)` + legacy `provinceGoldSurplus(provinceId, gameState)`
|
||||
- [~] `HeroSelector` - protoless `minimallyFatiguedHeroes` + legacy `minimallyFatiguedHeroesProto`
|
||||
|
||||
### Blocked (still uses proto GameState)
|
||||
|
||||
- [ ] `CommandChoiceHelpers` - main target, uses proto GameState extensively
|
||||
- Only 1 call to `minimallyFatiguedHeroesProto` (HeroSelector)
|
||||
- Many calls to proto `provinceGoldSurplus`
|
||||
- Depends on many Legacy* utils
|
||||
- Central hub called by many command selectors
|
||||
- [ ] `AttackDecisionCommandChooser` - uses proto GameState, converts internally
|
||||
- [ ] `CommandChooser` - trait uses proto GameState in signature
|
||||
- [ ] `AttackDecisionCommandChooser` - uses proto types
|
||||
- [ ] `CommandChooser` - uses proto GameState
|
||||
- [ ] `FulfillQuestsCommandSelector` - takes proto, converts to native immediately
|
||||
- Called by `MidGameAIClient` which uses proto GameState
|
||||
|
||||
## Next Steps
|
||||
|
||||
### Phase 1: CommandChoiceHelpers Migration
|
||||
|
||||
The main blocker is `CommandChoiceHelpers.scala` which uses proto `GameState` extensively. Strategy:
|
||||
|
||||
1. **Add Scala overloads** to `CommandChoiceHelpers` methods that currently take proto GameState
|
||||
2. **Update internal helpers** to use Scala types where possible
|
||||
3. **Migrate callers incrementally** - command selectors that are already protoless can switch to Scala overloads
|
||||
|
||||
### Phase 2: CommandChooser Trait
|
||||
|
||||
Once CommandChoiceHelpers is protoless:
|
||||
|
||||
1. Add Scala `GameState` overload to `CommandChooser.choose()` method
|
||||
2. Update implementations (`AttackDecisionCommandChooser`, etc.) to use Scala internally
|
||||
3. Eventually deprecate proto overloads
|
||||
|
||||
### Phase 3: MidGameAIClient
|
||||
|
||||
The top-level AI client still uses proto GameState. Once lower layers are protoless:
|
||||
|
||||
1. Convert `MidGameAIClient` to use Scala GameState internally
|
||||
2. Only convert at the boundary when receiving from/sending to gRPC
|
||||
|
||||
## Key Files
|
||||
|
||||
### Protoless Model Types
|
||||
|
||||
+2
-2
@@ -130,12 +130,12 @@ bazel_dep(name = "aspect_bazel_lib", version = "2.16.0")
|
||||
|
||||
oci = use_extension("@rules_oci//oci:extensions.bzl", "oci")
|
||||
|
||||
# Base image for Eagle (Java 17 JDK - includes jcmd for JFR dumps)
|
||||
# Base image for Eagle (Java 17)
|
||||
oci.pull(
|
||||
name = "eclipse_temurin_17",
|
||||
digest = "sha256:d286b5352d98777bbf727f54038b04f0145cd9b76ca83f38a67aa111d4303748",
|
||||
image = "docker.io/library/eclipse-temurin",
|
||||
platforms = ["linux/amd64"],
|
||||
tag = "17-jdk",
|
||||
)
|
||||
|
||||
# Base image for Shardok (Ubuntu 24.04 for C++ runtime)
|
||||
|
||||
Generated
+3
-3
@@ -1293,7 +1293,7 @@
|
||||
"@@rules_oci~//oci:extensions.bzl%oci": {
|
||||
"general": {
|
||||
"bzlTransitiveDigest": "FaY+7xb13bB3hmxqwAWaGp3Tf3Q4Nfdlr+F38CP5mcg=",
|
||||
"usagesDigest": "uqsqMpE+yaVuTByR2rIA2v1Vkm1JwwuN6nbXOo1W9G0=",
|
||||
"usagesDigest": "KXZUVR9ea29hTmhxC4+BG0pTXTijLpsFrDVraHG4OyU=",
|
||||
"recordedFileInputs": {},
|
||||
"recordedDirentsInputs": {},
|
||||
"envVariables": {},
|
||||
@@ -1306,7 +1306,7 @@
|
||||
"scheme": "https",
|
||||
"registry": "index.docker.io",
|
||||
"repository": "library/eclipse-temurin",
|
||||
"identifier": "17-jdk",
|
||||
"identifier": "sha256:d286b5352d98777bbf727f54038b04f0145cd9b76ca83f38a67aa111d4303748",
|
||||
"platform": "linux/amd64",
|
||||
"target_name": "eclipse_temurin_17_linux_amd64",
|
||||
"bazel_tags": []
|
||||
@@ -1321,7 +1321,7 @@
|
||||
"scheme": "https",
|
||||
"registry": "index.docker.io",
|
||||
"repository": "library/eclipse-temurin",
|
||||
"identifier": "17-jdk",
|
||||
"identifier": "sha256:d286b5352d98777bbf727f54038b04f0145cd9b76ca83f38a67aa111d4303748",
|
||||
"platforms": {
|
||||
"@@platforms//cpu:x86_64": "@eclipse_temurin_17_linux_amd64"
|
||||
},
|
||||
|
||||
+2
-48
@@ -51,17 +51,13 @@ oci_image(
|
||||
base = "@eclipse_temurin_17_linux_amd64",
|
||||
entrypoint = [
|
||||
"java",
|
||||
"-Xmx2g",
|
||||
"-Xmx4g",
|
||||
"-XX:+UseG1GC",
|
||||
# JFR profiling support
|
||||
"-XX:+UnlockDiagnosticVMOptions",
|
||||
"-XX:+DebugNonSafepoints", # Required for JFR to see through inlined methods
|
||||
"-XX:FlightRecorderOptions=stackdepth=256",
|
||||
"-jar",
|
||||
"/app/eagle_server_deploy.jar",
|
||||
],
|
||||
env = {
|
||||
"JAVA_OPTS": "-Xmx2g -XX:+UseG1GC",
|
||||
"JAVA_OPTS": "-Xmx4g -XX:+UseG1GC",
|
||||
},
|
||||
exposed_ports = ["40032/tcp"],
|
||||
tars = [
|
||||
@@ -195,45 +191,3 @@ oci_push(
|
||||
image = ":admin_server_image",
|
||||
repository = "registry.digitalocean.com/eagle0/admin-server",
|
||||
)
|
||||
|
||||
#
|
||||
# JFR Sidecar Docker Image (Go + JDK for jcmd)
|
||||
#
|
||||
# This sidecar runs with shared PID namespace to access the Eagle JVM.
|
||||
# Build: bazel build //ci:jfr_sidecar_image
|
||||
# Load: bazel run //ci:jfr_sidecar_load
|
||||
# Push: bazel run //ci:jfr_sidecar_push
|
||||
#
|
||||
|
||||
# Package the Go JFR server binary
|
||||
pkg_tar(
|
||||
name = "jfr_sidecar_binary_layer",
|
||||
srcs = ["//src/main/go/net/eagle0/jfr_server:jfr_server_linux_amd64"],
|
||||
package_dir = "/app",
|
||||
)
|
||||
|
||||
oci_image(
|
||||
name = "jfr_sidecar_image",
|
||||
# Use JDK base image - we need jcmd to dump JFR recordings
|
||||
base = "@eclipse_temurin_17_linux_amd64",
|
||||
entrypoint = ["/app/jfr_server_linux_amd64"],
|
||||
exposed_ports = ["8081/tcp"],
|
||||
tars = [
|
||||
":jfr_sidecar_binary_layer",
|
||||
],
|
||||
workdir = "/app",
|
||||
)
|
||||
|
||||
# Load into Docker locally: bazel run //ci:jfr_sidecar_load
|
||||
oci_load(
|
||||
name = "jfr_sidecar_load",
|
||||
image = ":jfr_sidecar_image",
|
||||
repo_tags = ["eagle0/jfr-sidecar:latest"],
|
||||
)
|
||||
|
||||
# Push to DigitalOcean Container Registry
|
||||
oci_push(
|
||||
name = "jfr_sidecar_push",
|
||||
image = ":jfr_sidecar_image",
|
||||
repository = "registry.digitalocean.com/eagle0/jfr-sidecar",
|
||||
)
|
||||
|
||||
@@ -26,8 +26,6 @@ services:
|
||||
DO_SPACES_SECRET_KEY: "${DO_SPACES_SECRET_KEY:-}"
|
||||
volumes:
|
||||
- ./saves:/app/saves
|
||||
- ./jfr:/app/jfr # JFR recordings - dump with: docker exec eagle-server jcmd 1 JFR.dump filename=/app/jfr/profile.jfr
|
||||
- jvm-tmp:/tmp # Shared with jfr-sidecar for JVM attach socket files
|
||||
depends_on:
|
||||
- shardok
|
||||
restart: unless-stopped
|
||||
@@ -92,15 +90,12 @@ services:
|
||||
command:
|
||||
- "--eagle-addr"
|
||||
- "eagle:40032"
|
||||
- "--jfr-sidecar-addr"
|
||||
- "jfr-sidecar:8081"
|
||||
- "--http-port"
|
||||
- "8080"
|
||||
ports:
|
||||
- "8080:8080"
|
||||
depends_on:
|
||||
- eagle
|
||||
- jfr-sidecar
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
driver: "json-file"
|
||||
@@ -114,28 +109,6 @@ services:
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
|
||||
jfr-sidecar:
|
||||
image: ${JFR_SIDECAR_IMAGE:-registry.digitalocean.com/eagle0/jfr-sidecar:latest}
|
||||
container_name: jfr-sidecar
|
||||
# Share PID namespace with Eagle to access its JVM via jcmd
|
||||
pid: "service:eagle"
|
||||
volumes:
|
||||
- jvm-tmp:/tmp # Shared with Eagle for JVM attach socket files
|
||||
depends_on:
|
||||
- eagle
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "2"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -q --spider http://localhost:8081/health || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
|
||||
certbot:
|
||||
image: certbot/certbot
|
||||
container_name: certbot
|
||||
@@ -143,7 +116,3 @@ services:
|
||||
- ./certbot/conf:/etc/letsencrypt
|
||||
- ./certbot/www:/var/www/certbot
|
||||
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"
|
||||
|
||||
volumes:
|
||||
jvm-tmp:
|
||||
# Shared /tmp for JVM attach socket files between Eagle and jfr-sidecar
|
||||
|
||||
@@ -1,471 +0,0 @@
|
||||
# Admin Server Enhancement Plan
|
||||
|
||||
## Overview
|
||||
|
||||
This document outlines enhancements to the Go admin server (`src/main/go/net/eagle0/admin_server/`) to provide a proper web UI for game administration.
|
||||
|
||||
### Current State
|
||||
|
||||
The admin server provides a full web UI with htmx interactivity:
|
||||
- `GET /` - Redirect to games list
|
||||
- `GET /games` - Game list page (HTML)
|
||||
- `GET /games/{id}` - Game detail with action history
|
||||
- `GET /games/{id}/history` - History rows (htmx partial, infinite scroll)
|
||||
- `GET /games/{id}/action/{index}` - Action detail (htmx partial)
|
||||
- `POST /games/{id}/rewind` - Rewind game to target action
|
||||
- `GET /settings` - Settings list with live search
|
||||
- `POST /settings/update` - Update setting value
|
||||
- `GET /health` - Health check (JSON)
|
||||
- `GET /api/games` - JSON API for programmatic access
|
||||
- `GET /api/games/{id}/history` - JSON API for history
|
||||
|
||||
### Goals
|
||||
|
||||
1. **Web UI**: Replace raw JSON with an interactive HTML interface
|
||||
2. **Settings Management**: View and modify the 275+ game settings at runtime
|
||||
3. **Game Rewind**: Restore a game to a previous action count
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
### Technology Choice: Go Templates + htmx
|
||||
|
||||
**Rationale:**
|
||||
- Single binary deployment (no separate frontend build)
|
||||
- htmx provides interactivity without JavaScript framework complexity
|
||||
- Familiar HTML/CSS, minimal learning curve
|
||||
- Excellent for admin tools where SEO and bundle size don't matter
|
||||
|
||||
**Alternatives Considered:**
|
||||
- React/Vue SPA: Adds build complexity, separate deployment artifact
|
||||
- Server-side only: Less interactive, full page reloads
|
||||
|
||||
### Directory Structure
|
||||
|
||||
```
|
||||
src/main/go/net/eagle0/admin_server/
|
||||
├── admin_server.go # Main entry point, HTTP routes
|
||||
├── handlers/
|
||||
│ ├── games.go # Game list and detail handlers
|
||||
│ ├── settings.go # Settings list and update handlers
|
||||
│ └── rewind.go # Game rewind handlers
|
||||
├── templates/
|
||||
│ ├── layout.html # Base layout with nav, htmx includes
|
||||
│ ├── games/
|
||||
│ │ ├── list.html # Game list page
|
||||
│ │ ├── detail.html # Single game view with history
|
||||
│ │ └── history.html # Partial for history table (htmx)
|
||||
│ ├── settings/
|
||||
│ │ ├── list.html # Settings list with search/filter
|
||||
│ │ └── edit.html # Inline edit partial (htmx)
|
||||
│ └── rewind/
|
||||
│ └── confirm.html # Rewind confirmation modal
|
||||
├── static/
|
||||
│ ├── style.css # Minimal CSS (Pico CSS or similar)
|
||||
│ └── htmx.min.js # htmx library
|
||||
└── BUILD.bazel
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Feature 1: Web UI
|
||||
|
||||
### Routes
|
||||
|
||||
| Route | Method | Description |
|
||||
|-------|--------|-------------|
|
||||
| `/` | GET | Redirect to `/games` |
|
||||
| `/games` | GET | Game list page (HTML) |
|
||||
| `/games/{id}` | GET | Game detail page with history |
|
||||
| `/games/{id}/history` | GET | History partial (htmx, for infinite scroll) |
|
||||
| `/api/games` | GET | JSON API (existing, keep for programmatic access) |
|
||||
| `/api/games/{id}/history` | GET | JSON API (existing) |
|
||||
|
||||
### Game List Page
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Eagle Admin [Settings] [Health] │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Running Games (3) │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Game abc123f Round 45 │ │
|
||||
│ │ Players: Liu Bei (Human), Cao Cao (AI), Sun Quan │ │
|
||||
│ │ Actions: 1,234 [View] [Rewind]│ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Game def456a Round 12 │ │
|
||||
│ │ Players: Test Player (Human) │ │
|
||||
│ │ Actions: 456 [View] [Rewind]│ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Game Detail Page
|
||||
|
||||
Shows game info and scrollable action history:
|
||||
- **Reverse chronological order**: Most recent actions displayed first
|
||||
- Each action shows: index, type, round ID
|
||||
- **Clickable actions**: Clicking an action row expands to show JSON representation of the full action data
|
||||
- "Rewind to here" button on each action row
|
||||
- Infinite scroll loads more history via htmx (loading older actions as user scrolls down)
|
||||
|
||||
### Implementation Notes
|
||||
|
||||
1. **Embed static files**: Use `//go:embed` to bundle templates and static files
|
||||
2. **Template functions**: Add helpers for formatting (hex IDs, timestamps, action summaries)
|
||||
3. **CSS framework**: Use Pico CSS (~10KB) for clean defaults without classes
|
||||
|
||||
---
|
||||
|
||||
## Feature 2: Settings Management
|
||||
|
||||
### New gRPC Endpoints (Eagle Server)
|
||||
|
||||
Add to `eagle.proto`:
|
||||
|
||||
```protobuf
|
||||
message Setting {
|
||||
string name = 1;
|
||||
string type = 2; // "Int" or "Double"
|
||||
string value = 3; // Current value as string
|
||||
string default_value = 4; // Default from BUILD.bazel
|
||||
string description = 5; // Optional, for UI hints
|
||||
}
|
||||
|
||||
message GetSettingsRequest {
|
||||
string filter = 1; // Optional name filter (substring match)
|
||||
}
|
||||
|
||||
message GetSettingsResponse {
|
||||
repeated Setting settings = 1;
|
||||
}
|
||||
|
||||
message UpdateSettingRequest {
|
||||
string name = 1;
|
||||
string value = 2;
|
||||
}
|
||||
|
||||
message UpdateSettingResponse {
|
||||
Setting setting = 1; // Updated setting
|
||||
string error = 2; // Empty on success
|
||||
}
|
||||
|
||||
service Eagle {
|
||||
// ... existing methods ...
|
||||
rpc GetSettings(GetSettingsRequest) returns (GetSettingsResponse);
|
||||
rpc UpdateSetting(UpdateSettingRequest) returns (UpdateSettingResponse);
|
||||
}
|
||||
```
|
||||
|
||||
### Eagle Server Implementation
|
||||
|
||||
Create a settings registry that:
|
||||
1. Discovers all `IntSetting` and `DoubleSetting` instances via reflection or explicit registration
|
||||
2. Provides get/set by name
|
||||
3. Validates types on update
|
||||
|
||||
```scala
|
||||
// src/main/scala/net/eagle0/eagle/library/settings/SettingsRegistry.scala
|
||||
object SettingsRegistry {
|
||||
private val settings: Map[String, Either[IntSetting, DoubleSetting]] = Map(
|
||||
"ActionVigorCost" -> Left(ActionVigorCost),
|
||||
"BaseFoodBuyPrice" -> Right(BaseFoodBuyPrice),
|
||||
// ... register all 275 settings
|
||||
)
|
||||
|
||||
def getAll(filter: Option[String]): Seq[Setting] = ...
|
||||
def get(name: String): Option[Setting] = ...
|
||||
def update(name: String, value: String): Either[String, Setting] = ...
|
||||
}
|
||||
```
|
||||
|
||||
**Alternative: Code generation**
|
||||
|
||||
Rather than manually registering 275 settings, modify `setting_rule.bzl` to generate a registry file during build.
|
||||
|
||||
### Admin Server Routes
|
||||
|
||||
| Route | Method | Description |
|
||||
|-------|--------|-------------|
|
||||
| `/settings` | GET | Settings list page with search |
|
||||
| `/settings/{name}` | GET | Single setting detail (htmx partial) |
|
||||
| `/settings/{name}` | PUT | Update setting value |
|
||||
| `/api/settings` | GET | JSON API |
|
||||
| `/api/settings/{name}` | PUT | JSON API |
|
||||
|
||||
### Settings UI
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Eagle Admin [Games] [Health] │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Settings [Search: __________ ] │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ ActionVigorCost (Int) │ │
|
||||
│ │ Current: [15 ] Default: 15 [Save] │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ BaseFoodBuyPrice (Double) │ │
|
||||
│ │ Current: [0.5 ] Default: 0.5 [Save] │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ... (275 settings, virtualized/paginated) ... │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Considerations
|
||||
|
||||
1. **Persistence**: Settings changes are in-memory only. Document that restarts reset to defaults.
|
||||
2. **Validation**: Validate numeric ranges where applicable (e.g., percentages 0-100)
|
||||
3. **Categories**: Consider grouping settings by prefix (AI*, Combat*, Economy*, etc.)
|
||||
4. **Audit log**: Log setting changes with timestamp for debugging
|
||||
|
||||
---
|
||||
|
||||
## Feature 3: Game Rewind
|
||||
|
||||
### Concept
|
||||
|
||||
Restore a game to a previous point in its action history. This is useful for:
|
||||
- Debugging issues that occurred at a specific point
|
||||
- Testing "what if" scenarios
|
||||
- Recovering from bugs that corrupted state
|
||||
|
||||
### New gRPC Endpoint
|
||||
|
||||
Add to `eagle.proto`:
|
||||
|
||||
```protobuf
|
||||
message RewindGameRequest {
|
||||
int64 game_id = 1;
|
||||
int32 target_action_count = 2; // Rewind to state after this many actions
|
||||
}
|
||||
|
||||
message RewindGameResponse {
|
||||
bool success = 1;
|
||||
string error = 2;
|
||||
int32 new_action_count = 3;
|
||||
int32 disconnected_clients = 4; // Number of clients that were disconnected
|
||||
}
|
||||
|
||||
service Eagle {
|
||||
// ... existing methods ...
|
||||
rpc RewindGame(RewindGameRequest) returns (RewindGameResponse);
|
||||
}
|
||||
```
|
||||
|
||||
### Eagle Server Implementation
|
||||
|
||||
The `GameHistory` already stores `ActionWithResultingState` for each action, which includes the `GameState` after that action. Rewinding means:
|
||||
|
||||
1. **Validate**: Check that `target_action_count` is within valid range (0 to current count)
|
||||
2. **Get target state**: Retrieve `GameState` at target action count from history
|
||||
3. **Disconnect clients**: Close all human player connections (they'll need to reconnect)
|
||||
4. **Replace engine**: Create new `EngineImpl` with target state and truncated history
|
||||
5. **Reset AI state**: Clear any cached AI state that depends on current game state
|
||||
|
||||
```scala
|
||||
// GameController.scala (pseudocode)
|
||||
def rewindTo(targetActionCount: Int): Either[String, RewindResult] = {
|
||||
if (targetActionCount < 0 || targetActionCount > engine.history.count)
|
||||
return Left(s"Invalid action count: $targetActionCount")
|
||||
|
||||
// Get state at target point
|
||||
val targetState = engine.history.stateAt(targetActionCount)
|
||||
val truncatedHistory = engine.history.truncateTo(targetActionCount)
|
||||
|
||||
// Disconnect all human clients
|
||||
val disconnectedCount = humanClients.length
|
||||
humanClients.foreach(_.disconnect("Game rewound by admin"))
|
||||
|
||||
// Create new engine at target state
|
||||
val newEngine = EngineImpl(
|
||||
gameId = engine.gameId,
|
||||
currentState = targetState,
|
||||
history = truncatedHistory,
|
||||
// ... other fields
|
||||
)
|
||||
|
||||
// Replace controller's engine
|
||||
this.engine = newEngine
|
||||
|
||||
Right(RewindResult(targetActionCount, disconnectedCount))
|
||||
}
|
||||
```
|
||||
|
||||
### GameHistory Enhancement
|
||||
|
||||
Add method to get state at a specific action count:
|
||||
|
||||
```scala
|
||||
trait GameHistory {
|
||||
// ... existing methods ...
|
||||
|
||||
def stateAt(actionCount: Int): GameState = {
|
||||
if (actionCount == 0) initialState
|
||||
else all(actionCount - 1).resultingState
|
||||
}
|
||||
|
||||
def truncateTo(actionCount: Int): GameHistory = {
|
||||
GameHistoryImpl(
|
||||
initialState = initialState,
|
||||
actions = all.take(actionCount)
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Admin Server Route
|
||||
|
||||
| Route | Method | Description |
|
||||
|-------|--------|-------------|
|
||||
| `/games/{id}/rewind` | POST | Rewind game (form: `target_action_count`) |
|
||||
| `/games/{id}/rewind/confirm` | GET | Confirmation modal (htmx partial) |
|
||||
|
||||
### Rewind UI Flow
|
||||
|
||||
1. User views game history
|
||||
2. User clicks "Rewind to here" on an action row
|
||||
3. Confirmation modal appears via htmx:
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Rewind Game abc123f? │
|
||||
│ │
|
||||
│ This will: │
|
||||
│ • Restore to action 456 (Round 23) │
|
||||
│ • Discard 778 subsequent actions │
|
||||
│ • Disconnect 2 connected players │
|
||||
│ │
|
||||
│ This cannot be undone. │
|
||||
│ │
|
||||
│ [Cancel] [Rewind] │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
4. On confirm, POST to `/games/{id}/rewind`
|
||||
5. Success: redirect to game detail showing new state
|
||||
6. Error: show error message
|
||||
|
||||
### Safety Considerations
|
||||
|
||||
1. **No undo**: Rewinding discards history. Consider optional backup before rewind.
|
||||
2. **Client disconnect**: All connected clients are forcibly disconnected.
|
||||
3. **AI state**: Ensure AI clients restart cleanly after rewind.
|
||||
4. **Concurrent access**: Lock game during rewind to prevent race conditions.
|
||||
5. **Authorization**: In production, require admin authentication.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1: Web UI Foundation
|
||||
|
||||
**Status: Complete**
|
||||
|
||||
1. ✅ Set up Go templates with `embed`
|
||||
2. ✅ Add Pico CSS and htmx
|
||||
3. ✅ Create base layout with navigation
|
||||
4. ✅ Convert `/games` to HTML with styling
|
||||
5. ✅ Add game detail page with history table
|
||||
6. ✅ Implement htmx infinite scroll for history
|
||||
7. ✅ Reverse history order (most recent first)
|
||||
8. ✅ Clickable action rows that expand to show JSON representation
|
||||
9. ✅ Add `/games/{id}/action/{index}` endpoint for fetching action details
|
||||
|
||||
**Deliverable**: Browsable game list and history in HTML with clickable action details
|
||||
|
||||
### Phase 2: Settings Management
|
||||
|
||||
**Status: Complete**
|
||||
|
||||
1. ✅ Add `GetSettings` to `eagle.proto` (uses existing `AddSettings` for updates)
|
||||
2. ✅ Add `getAllSettings` method to auto-generated `SettingsLoader`
|
||||
3. ✅ Implement `getSettings` in `EagleServiceImpl`
|
||||
4. ✅ Create settings list page with live search
|
||||
5. ✅ Add inline editing with htmx
|
||||
6. ✅ Modified settings are highlighted
|
||||
|
||||
**Deliverable**: View and edit settings via admin UI
|
||||
|
||||
### Phase 3: Game Rewind
|
||||
|
||||
**Status: Complete**
|
||||
|
||||
1. ✅ Add `RewindGame` to `eagle.proto`
|
||||
2. ✅ Implement `stateAt` and `truncateTo` in `GameHistory`
|
||||
3. ✅ Implement rewind logic in `Engine` and `GameController`
|
||||
4. ✅ Add rewind confirmation (htmx `hx-confirm` dialog)
|
||||
5. ✅ Handle client disconnection gracefully
|
||||
6. ✅ Add rewind button to history rows
|
||||
7. ✅ Implement `rewindGame` in `GamesManager` and `EagleServiceImpl`
|
||||
8. ✅ Add admin server `/games/{id}/rewind` POST handler
|
||||
9. ✅ Add success/error feedback UI
|
||||
|
||||
**Deliverable**: Rewind games to any previous action
|
||||
|
||||
### Phase 4: Polish
|
||||
|
||||
**Status: Not Started**
|
||||
|
||||
#### High Priority
|
||||
1. **Add tests for rewind functionality**
|
||||
- `PersistedHistory.truncateTo` (handles complex persisted vs recent logic)
|
||||
- `InMemoryHistory.truncateTo`
|
||||
- `EngineImpl.rewindTo`
|
||||
- `GameController.rewindTo`
|
||||
- `GamesManager.rewindGame`
|
||||
|
||||
2. **Improve action history display**
|
||||
- Human-readable action type names (e.g., "New Round" instead of "NewRoundAction")
|
||||
- Show acting faction/province when available
|
||||
- Action summaries from the `summary` field in `GameHistoryEntry`
|
||||
|
||||
#### Medium Priority
|
||||
3. **Settings improvements**
|
||||
- Group settings by category prefix (AI*, Combat*, Economy*, etc.)
|
||||
- Show setting descriptions where available
|
||||
- Pagination for large settings lists
|
||||
|
||||
4. **Error handling improvements**
|
||||
- Better error messages on failed operations
|
||||
- Retry logic for transient gRPC failures
|
||||
|
||||
#### Low Priority (Nice to Have)
|
||||
5. **Basic auth** - HTTP Basic Auth or OAuth for production use
|
||||
6. **Audit logging** - Log admin actions with timestamps
|
||||
7. **Documentation** - Usage guide, deployment notes
|
||||
|
||||
#### Future Considerations
|
||||
- Game creation from admin UI
|
||||
- Player management (view connected players, force disconnect)
|
||||
- Export game history to file
|
||||
- Metrics/stats dashboard
|
||||
|
||||
---
|
||||
|
||||
## Security Notes
|
||||
|
||||
The admin server is intended for local/trusted network use only. For production:
|
||||
|
||||
1. **Do not expose to public internet** without authentication
|
||||
2. Consider adding HTTP Basic Auth or OAuth
|
||||
3. Run on internal network or behind VPN
|
||||
4. Log all admin actions for audit trail
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Settings persistence**: Should we add optional persistence to disk/database?
|
||||
2. **Game snapshots**: Should rewind create a backup first?
|
||||
3. **Multi-admin**: Need locking if multiple admins access simultaneously?
|
||||
4. **Shardok settings**: Are there Shardok (C++) settings to expose too?
|
||||
@@ -25,8 +25,7 @@ auto CalculateTimeBudget(
|
||||
const PlayerId playerId,
|
||||
const GameSettingsSPtr &settings,
|
||||
const GameStateW &state,
|
||||
const size_t numCommands,
|
||||
const bool isAllAiBattle) -> AITimeBudget {
|
||||
const size_t numCommands) -> AITimeBudget {
|
||||
const auto settingsGetter = settings->GetGetter();
|
||||
const auto castleCoords = AllCastleCoords(state->hex_map());
|
||||
|
||||
@@ -35,10 +34,8 @@ auto CalculateTimeBudget(
|
||||
net::eagle0::shardok::storage::fb::GameStatus_::State_SET_UP;
|
||||
|
||||
// Get maximum budget cap from settings (in seconds)
|
||||
// For all-AI battles, use the faster budget limit
|
||||
const double maxBudgetSeconds =
|
||||
isAllAiBattle ? settingsGetter.Backing().all_ai_battle_time_budget_maximum()
|
||||
: settingsGetter.Backing().lookahead_time_budget_maximum_seconds();
|
||||
settingsGetter.Backing().lookahead_time_budget_maximum_seconds();
|
||||
const double maxBudgetMs = maxBudgetSeconds * 1000.0;
|
||||
|
||||
// During setup, use the setup-specific time budget
|
||||
|
||||
@@ -38,13 +38,11 @@ struct AITimeBudget {
|
||||
// Calculate time budget based on proximity to enemies and castles
|
||||
// Time budget is calculated dynamically based on number of available commands:
|
||||
// budget = msPerCommand × numCommands (clamped to 200-5000ms)
|
||||
// If isAllAiBattle is true, uses allAiBattleTimeBudgetMaximum instead of the normal maximum.
|
||||
auto CalculateTimeBudget(
|
||||
PlayerId playerId,
|
||||
const GameSettingsSPtr &settings,
|
||||
const GameStateW &state,
|
||||
size_t numCommands,
|
||||
bool isAllAiBattle) -> AITimeBudget;
|
||||
size_t numCommands) -> AITimeBudget;
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
|
||||
@@ -53,7 +53,6 @@ auto RoundsRemaining(const GameSettingsSPtr &settings, const GameStateView &gsv)
|
||||
ShardokAIClient::ShardokAIClient(
|
||||
const PlayerId playerId,
|
||||
const bool isDefender,
|
||||
const bool isAllAiBattle,
|
||||
const HexMap *hexMap,
|
||||
const SettingsGetter &settings,
|
||||
const AIAlgorithmType aiAlgorithmType,
|
||||
@@ -61,7 +60,6 @@ ShardokAIClient::ShardokAIClient(
|
||||
const mcts::MCTSConfig &mctsConfig)
|
||||
: playerId(playerId),
|
||||
isDefender(isDefender),
|
||||
isAllAiBattle(isAllAiBattle),
|
||||
aiAlgorithmType(aiAlgorithmType),
|
||||
scoringCalculatorType(scoringCalculatorType),
|
||||
alCache(std::make_unique<AttackLocationsCache>(hexMap, settings)),
|
||||
@@ -137,8 +135,7 @@ auto ShardokAIClient::StandardChooseCommandIndex(
|
||||
const auto commandCount = guessedCommands->size();
|
||||
|
||||
// Calculate time budget based on game situation using new dynamic per-command settings
|
||||
const auto timeBudget =
|
||||
CalculateTimeBudget(playerId, settings, guessedState, commandCount, isAllAiBattle);
|
||||
const auto timeBudget = CalculateTimeBudget(playerId, settings, guessedState, commandCount);
|
||||
|
||||
// Configure MCTS based on proximity to enemy
|
||||
// When far from enemy: use AVERAGING with maxPlayerFlips=0 (single-player lookahead)
|
||||
|
||||
@@ -41,7 +41,6 @@ class ShardokAIClient {
|
||||
private:
|
||||
const PlayerId playerId;
|
||||
const bool isDefender;
|
||||
const bool isAllAiBattle; // Whether this battle has only AI players (for faster time budgets)
|
||||
const AIAlgorithmType aiAlgorithmType;
|
||||
const ScoringCalculatorType scoringCalculatorType;
|
||||
|
||||
@@ -70,7 +69,6 @@ public:
|
||||
explicit ShardokAIClient(
|
||||
PlayerId playerId,
|
||||
bool isDefender,
|
||||
bool isAllAiBattle,
|
||||
const HexMap* hexMap,
|
||||
const SettingsGetter& settings,
|
||||
AIAlgorithmType aiAlgorithmType,
|
||||
|
||||
@@ -24,6 +24,9 @@ using std::scoped_lock;
|
||||
using std::string;
|
||||
using std::unique_lock;
|
||||
using std::weak_ptr;
|
||||
using std::chrono::duration;
|
||||
|
||||
static constexpr duration kWaitForUpdatesDuration = std::chrono::milliseconds(5000);
|
||||
|
||||
using net::eagle0::shardok::common::GameStatus;
|
||||
|
||||
@@ -71,17 +74,11 @@ vector<shared_ptr<ShardokAIClient>> ShardokGameController::MakeAIClients(
|
||||
mctsConfig.maxSimulationFlips = 1;
|
||||
mctsConfig.simulationPolicy = mcts::MCTSSimulationPolicy::WEIGHTED_HEURISTIC;
|
||||
|
||||
// Check if all players are AI - if so, use faster time budgets
|
||||
const auto &playerInfos = e->GetPlayerInfos();
|
||||
const bool isAllAiBattle =
|
||||
std::ranges::all_of(playerInfos, [](const auto &pi) { return pi.is_ai(); });
|
||||
|
||||
for (const auto &pi : playerInfos) {
|
||||
for (const auto &pi : e->GetPlayerInfos()) {
|
||||
if (pi.is_ai()) {
|
||||
auto newClient = std::make_shared<ShardokAIClient>(
|
||||
pi.player_id(),
|
||||
pi.is_defender(),
|
||||
isAllAiBattle,
|
||||
e->GetCurrentGameState()->hex_map(),
|
||||
e->GetGameSettings()->GetGetter(),
|
||||
AIAlgorithmType::ITERATIVE_DEEPENING,
|
||||
@@ -281,9 +278,15 @@ auto ShardokGameController::GetUpdates(const int64_t startingActionId) -> AllUpd
|
||||
awrs = engine->GetGameHistory(startingActionId);
|
||||
incomingRegistrations--;
|
||||
|
||||
// Note: Previously this had a 5-second wait for AI players to support long-polling.
|
||||
// With streaming (WaitForUpdatesAndPush), the caller already waits for updates,
|
||||
// so this wait is no longer needed. GetGameStatus is deprecated in favor of streaming.
|
||||
// If the current player is an AI, wait for some results to post. Otherwise go ahead and
|
||||
// return, we might be telling the caller about available commands.
|
||||
if (awrs.empty() && !engine->GameIsOver() &&
|
||||
engine->GetPlayerInfos()[engine->GetCurrentPlayerId()].is_ai()) {
|
||||
updateCondition.wait_for(guard, kWaitForUpdatesDuration);
|
||||
incomingRegistrations++;
|
||||
awrs = engine->GetGameHistory(startingActionId);
|
||||
incomingRegistrations--;
|
||||
}
|
||||
|
||||
updates.mainResults.reserve(awrs.size());
|
||||
std::ranges::transform(
|
||||
@@ -403,10 +406,12 @@ auto ShardokGameController::WaitForUpdatesAndPush(
|
||||
}
|
||||
// Lock released - GetUpdates will acquire its own lock
|
||||
|
||||
// IMPORTANT: Send any pending updates FIRST, including the final actions
|
||||
// that caused the game to end. Previously, we returned early on gameOver
|
||||
// without sending these final updates, causing the client to never see
|
||||
// the last few battle actions.
|
||||
if (gameOver) {
|
||||
subscriber->OnGameOver(gameOverInfo);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get updates outside the lock (GetUpdates acquires masterLock internally)
|
||||
AllUpdates updates = GetUpdates(lastPushedActionId);
|
||||
lastPushedActionId = updates.newUnfilteredCount;
|
||||
|
||||
@@ -417,12 +422,6 @@ auto ShardokGameController::WaitForUpdatesAndPush(
|
||||
updates.newUnfilteredCount,
|
||||
updates.currentGameState);
|
||||
}
|
||||
|
||||
// Now send gameOver notification after all updates have been sent
|
||||
if (gameOver) {
|
||||
subscriber->OnGameOver(gameOverInfo);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false; // Subscriber disconnected
|
||||
|
||||
@@ -322,6 +322,23 @@ void EagleInterfaceImpl::PopulateGameStatusResponse(
|
||||
}
|
||||
}
|
||||
|
||||
auto EagleInterfaceImpl::GetGameStatus(
|
||||
ServerContext * /*context*/,
|
||||
const GameStatusRequest *request,
|
||||
GameStatusResponse *response) -> Status {
|
||||
shared_ptr<ShardokGameController> controller;
|
||||
try {
|
||||
controller = ControllerForGame(request->game_id(), request->game_setup_info());
|
||||
} catch (NewGameException &e) { return e.GetStatus(); }
|
||||
|
||||
PopulateGameStatusResponse(
|
||||
controller,
|
||||
request->game_setup_info().known_result_count(),
|
||||
response);
|
||||
|
||||
return Status::OK;
|
||||
}
|
||||
|
||||
auto EagleInterfaceImpl::GetHexMap(
|
||||
ServerContext * /*context*/,
|
||||
const HexMapRequest *request,
|
||||
|
||||
@@ -31,6 +31,7 @@ namespace shardok {
|
||||
using grpc::ServerContext;
|
||||
using grpc::Status;
|
||||
using net::eagle0::common::GameSetupInfo;
|
||||
using net::eagle0::common::GameStatusRequest;
|
||||
using net::eagle0::common::GameStatusResponse;
|
||||
using net::eagle0::common::GameSubscriptionRequest;
|
||||
using net::eagle0::common::HexMapNamesRequest;
|
||||
@@ -67,6 +68,10 @@ public:
|
||||
ServerContext* context,
|
||||
const PlacementCommandsRequest* request,
|
||||
GameStatusResponse* response) -> Status override;
|
||||
auto GetGameStatus(
|
||||
ServerContext* context,
|
||||
const GameStatusRequest* request,
|
||||
GameStatusResponse* response) -> Status override;
|
||||
|
||||
auto GetHexMap(ServerContext* context, const HexMapRequest* request, HexMapResponse* response)
|
||||
-> Status override;
|
||||
|
||||
@@ -68,7 +68,6 @@
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/ResolveInvitationCommandSelector.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Slider/SliderManager.cs" />
|
||||
<Compile Include="Assets/common/GUIUtils/TableRowHoverDetector.cs" />
|
||||
<Compile Include="Assets/ConnectionHandler/DropGameConfirmationPanel.cs" />
|
||||
<Compile Include="Assets/Eagle/Table Rows/DynamicHeroTextUpdater.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/util/HeroDropdownController.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/PrisonerReturnedNotificationGenerator.cs" />
|
||||
@@ -126,7 +125,6 @@
|
||||
<Compile Include="Assets/Eagle/Notifications/Notification.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/QuestFailedDetailsNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/QuestFulfilledDetailsNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Auth/OAuthManager.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/ProfessionGainedDetailsNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Eagle/TextureList.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Modal Window/ModalWindowManager.cs" />
|
||||
@@ -148,7 +146,6 @@
|
||||
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/ProvinceHeldDetailsNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Bluetooth/DieInfo.cs" />
|
||||
<Compile Include="Assets/common/ResourceFetcher.cs" />
|
||||
<Compile Include="Assets/Auth/AuthClient.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/TruceRejectedDetailsNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/RiotSuppressedNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Modal Window/ModalWindowManagerEditor.cs" />
|
||||
@@ -156,7 +153,6 @@
|
||||
<Compile Include="Assets/Bluetooth/NativeDiceInterfaceImports.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/ProvinceConqueredNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Eagle/Table Rows/ExtraTroopsRowController.cs" />
|
||||
<Compile Include="Assets/Auth/JwtAuthInterceptor.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandPanelController.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/ShatteredArmyDetailsNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Shardok/TurnHistoryPanelController.cs" />
|
||||
@@ -262,7 +258,6 @@
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/ResolveTruceCommandSelector.cs" />
|
||||
<Compile Include="Assets/Eagle/ClientPregeneratedText.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/SwearBrotherhoodCommandSelector.cs" />
|
||||
<Compile Include="Assets/Auth/TokenStorage.cs" />
|
||||
<Compile Include="Assets/Eagle/Table Rows/TableRowController.cs" />
|
||||
<Compile Include="Assets/Eagle/DisplayNames.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/UI Manager/UIManagerSliderEditor.cs" />
|
||||
@@ -368,7 +363,6 @@
|
||||
<Compile Include="Assets/Eagle/Table Rows/AvailableHeroTableRow.cs" />
|
||||
<Compile Include="Assets/Eagle/Table Rows/HeroRowController.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/UI Manager/UIManagerTooltip.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/NewFactionHeadDetailsNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Shardok/HexGrid.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Tooltip/TooltipManager.cs" />
|
||||
<None Include="Assets/TextMesh Pro/Resources/Shaders/TMPro.cginc" />
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 683e069fbc3074e35ac6fc3881a03f4a
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,161 +0,0 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Cysharp.Net.Http;
|
||||
using Grpc.Core;
|
||||
using Grpc.Core.Interceptors;
|
||||
using Grpc.Net.Client;
|
||||
using Net.Eagle0.Eagle.Api.Auth;
|
||||
using UnityEngine;
|
||||
using GrpcAuthClient = Net.Eagle0.Eagle.Api.Auth.Auth.AuthClient;
|
||||
|
||||
namespace Auth {
|
||||
/// <summary>
|
||||
/// gRPC client for the Auth service.
|
||||
/// Handles OAuth URL generation, status polling, and token refresh.
|
||||
/// </summary>
|
||||
public class AuthClient : IDisposable {
|
||||
private const int PollIntervalMs = 2000; // Poll every 2 seconds
|
||||
private const int PollTimeoutMs = 300000; // 5 minute timeout
|
||||
|
||||
private readonly GrpcAuthClient _client;
|
||||
private readonly GrpcChannel _channel;
|
||||
|
||||
/// <summary>
|
||||
/// Create auth client for the given server URL.
|
||||
/// </summary>
|
||||
/// <param name="serverUrl">Full URL with scheme, e.g. "http://localhost:40032" or
|
||||
/// "https://prod.eagle0.net"</param>
|
||||
public AuthClient(string serverUrl) {
|
||||
_channel = GrpcChannel.ForAddress(
|
||||
serverUrl,
|
||||
new GrpcChannelOptions {
|
||||
HttpHandler = new YetAnotherHttpHandler { Http2Only = true },
|
||||
DisposeHttpClient = true
|
||||
});
|
||||
|
||||
// Create invoker with JWT interceptor for authenticated requests
|
||||
CallInvoker invoker = _channel.Intercept(new JwtAuthInterceptor());
|
||||
_client = new GrpcAuthClient(invoker);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get OAuth URL to open in system browser.
|
||||
/// Returns both the URL and the state token for polling.
|
||||
/// </summary>
|
||||
/// <param name="provider">Discord or Google</param>
|
||||
/// <returns>Tuple of (URL to open in browser, state token for polling)</returns>
|
||||
public async Task<(string authUrl, string state)> GetOAuthUrlAsync(OAuthProvider provider) {
|
||||
var request = new GetOAuthUrlRequest { Provider = provider };
|
||||
|
||||
var response = await _client.GetOAuthUrlAsync(request);
|
||||
|
||||
Debug.Log($"[AuthClient] Got OAuth URL for {provider}, state={response.State}");
|
||||
return (response.AuthUrl, response.State);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Poll for OAuth completion. Blocks until success, failure, or timeout.
|
||||
/// The server handles the OAuth callback and token exchange.
|
||||
/// </summary>
|
||||
/// <param name="state">State token from GetOAuthUrlAsync</param>
|
||||
/// <returns>Response with tokens and user info on success</returns>
|
||||
public async Task<CheckOAuthStatusResponse> PollForOAuthCompletionAsync(string state) {
|
||||
var startTime = DateTime.UtcNow;
|
||||
|
||||
while (true) {
|
||||
var request = new CheckOAuthStatusRequest { State = state };
|
||||
var response = await _client.CheckOAuthStatusAsync(request);
|
||||
|
||||
switch (response.Status) {
|
||||
case OAuthStatus.Success:
|
||||
Debug.Log(
|
||||
$"[AuthClient] OAuth successful, user={response.User?.DisplayName}, isNew={response.IsNewUser}");
|
||||
return response;
|
||||
|
||||
case OAuthStatus.Failed:
|
||||
throw new Exception($"OAuth failed: {response.ErrorMessage}");
|
||||
|
||||
case OAuthStatus.Expired:
|
||||
throw new Exception("OAuth session expired. Please try again.");
|
||||
|
||||
case OAuthStatus.Pending:
|
||||
// Check timeout
|
||||
if ((DateTime.UtcNow - startTime).TotalMilliseconds > PollTimeoutMs) {
|
||||
throw new TimeoutException("OAuth login timed out. Please try again.");
|
||||
}
|
||||
// Wait before polling again
|
||||
await Task.Delay(PollIntervalMs);
|
||||
break;
|
||||
|
||||
default: throw new Exception($"Unknown OAuth status: {response.Status}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refresh access token using stored refresh token.
|
||||
/// </summary>
|
||||
/// <returns>New access token and expiry, or null if refresh failed</returns>
|
||||
public async Task<RefreshTokenResponse> RefreshTokenAsync() {
|
||||
var refreshToken = TokenStorage.RefreshToken;
|
||||
if (string.IsNullOrEmpty(refreshToken)) {
|
||||
throw new InvalidOperationException("No refresh token available");
|
||||
}
|
||||
|
||||
var request = new RefreshTokenRequest { RefreshToken = refreshToken };
|
||||
|
||||
var response = await _client.RefreshTokenAsync(request);
|
||||
|
||||
// Update stored access token
|
||||
TokenStorage.UpdateAccessToken(response.AccessToken, response.ExpiresAt);
|
||||
|
||||
Debug.Log($"[AuthClient] Token refreshed, expires at {response.ExpiresAt}");
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set display name for new users.
|
||||
/// Requires valid JWT in interceptor.
|
||||
/// </summary>
|
||||
public async Task<SetDisplayNameResponse> SetDisplayNameAsync(string displayName) {
|
||||
var request = new SetDisplayNameRequest { DisplayName = displayName };
|
||||
var response = await _client.SetDisplayNameAsync(request);
|
||||
|
||||
if (response.Success) {
|
||||
TokenStorage.UpdateDisplayName(displayName);
|
||||
Debug.Log($"[AuthClient] Display name set to: {displayName}");
|
||||
} else {
|
||||
Debug.LogWarning(
|
||||
$"[AuthClient] Failed to set display name: {response.ErrorMessage}");
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get current user info. Validates the stored JWT.
|
||||
/// </summary>
|
||||
public async Task<GetCurrentUserResponse> GetCurrentUserAsync() {
|
||||
var response = await _client.GetCurrentUserAsync(new GetCurrentUserRequest());
|
||||
Debug.Log($"[AuthClient] Current user: {response.User?.DisplayName}");
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logout - invalidates refresh token on server.
|
||||
/// </summary>
|
||||
public async Task LogoutAsync() {
|
||||
try {
|
||||
await _client.LogoutAsync(new LogoutRequest());
|
||||
} catch (Exception ex) {
|
||||
Debug.LogWarning($"[AuthClient] Logout RPC failed (may be expected): {ex.Message}");
|
||||
}
|
||||
|
||||
// Clear local tokens regardless of server response
|
||||
TokenStorage.Clear();
|
||||
Debug.Log("[AuthClient] Logged out, tokens cleared");
|
||||
}
|
||||
|
||||
public void Dispose() { _channel?.Dispose(); }
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4ba94126c2d2145b8875612eaf5bc371
|
||||
@@ -1,75 +0,0 @@
|
||||
using System;
|
||||
using Grpc.Core;
|
||||
using Grpc.Core.Interceptors;
|
||||
|
||||
namespace Auth {
|
||||
/// <summary>
|
||||
/// gRPC interceptor that attaches JWT Bearer token to requests.
|
||||
/// Reads token from TokenStorage on each request.
|
||||
/// </summary>
|
||||
public class JwtAuthInterceptor : Interceptor {
|
||||
private const string HeaderName = "Authorization";
|
||||
|
||||
private string GetBearerHeader() {
|
||||
var token = TokenStorage.AccessToken;
|
||||
return string.IsNullOrEmpty(token) ? null : $"Bearer {token}";
|
||||
}
|
||||
|
||||
public override TResponse BlockingUnaryCall<TRequest, TResponse>(
|
||||
TRequest request,
|
||||
ClientInterceptorContext<TRequest, TResponse> context,
|
||||
BlockingUnaryCallContinuation<TRequest, TResponse> continuation) {
|
||||
return continuation(request, ContextWithHeaders(context));
|
||||
}
|
||||
|
||||
public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
|
||||
TRequest request,
|
||||
ClientInterceptorContext<TRequest, TResponse> context,
|
||||
AsyncUnaryCallContinuation<TRequest, TResponse> continuation) {
|
||||
return continuation(request, ContextWithHeaders(context));
|
||||
}
|
||||
|
||||
public override AsyncServerStreamingCall<TResponse>
|
||||
AsyncServerStreamingCall<TRequest, TResponse>(
|
||||
TRequest request,
|
||||
ClientInterceptorContext<TRequest, TResponse> context,
|
||||
AsyncServerStreamingCallContinuation<TRequest, TResponse> continuation) {
|
||||
return continuation(request, ContextWithHeaders(context));
|
||||
}
|
||||
|
||||
public override AsyncClientStreamingCall<TRequest, TResponse>
|
||||
AsyncClientStreamingCall<TRequest, TResponse>(
|
||||
ClientInterceptorContext<TRequest, TResponse> context,
|
||||
AsyncClientStreamingCallContinuation<TRequest, TResponse> continuation) {
|
||||
return continuation(ContextWithHeaders(context));
|
||||
}
|
||||
|
||||
public override AsyncDuplexStreamingCall<TRequest, TResponse>
|
||||
AsyncDuplexStreamingCall<TRequest, TResponse>(
|
||||
ClientInterceptorContext<TRequest, TResponse> context,
|
||||
AsyncDuplexStreamingCallContinuation<TRequest, TResponse> continuation) {
|
||||
return continuation(ContextWithHeaders(context));
|
||||
}
|
||||
|
||||
private ClientInterceptorContext<TRequest, TResponse>
|
||||
ContextWithHeaders<TRequest, TResponse>(
|
||||
ClientInterceptorContext<TRequest, TResponse> original)
|
||||
where TRequest : class
|
||||
where TResponse : class {
|
||||
var bearerHeader = GetBearerHeader();
|
||||
if (string.IsNullOrEmpty(bearerHeader)) {
|
||||
// No token available - return original context
|
||||
// Auth service endpoints don't require token
|
||||
return original;
|
||||
}
|
||||
|
||||
var headers = new Metadata();
|
||||
headers.Add(HeaderName, bearerHeader);
|
||||
|
||||
return new ClientInterceptorContext<TRequest, TResponse>(
|
||||
original.Method,
|
||||
original.Host,
|
||||
original.Options.WithHeaders(headers));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8528d8ee967e042ecb2a5e8285c425e0
|
||||
@@ -1,183 +0,0 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Net.Eagle0.Eagle.Api.Auth;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Auth {
|
||||
/// <summary>
|
||||
/// Manages OAuth authentication flow using server-mediated polling:
|
||||
/// - Opens system browser for OAuth consent
|
||||
/// - Polls server for OAuth completion (no deep links needed)
|
||||
/// - Token storage and refresh
|
||||
/// </summary>
|
||||
public class OAuthManager : MonoBehaviour {
|
||||
public static OAuthManager Instance { get; private set; }
|
||||
|
||||
private AuthClient _authClient;
|
||||
private string _currentServerUrl;
|
||||
|
||||
// Events for UI updates
|
||||
public event Action<UserInfo> OnLoginSuccess;
|
||||
public event Action<string> OnLoginFailed;
|
||||
public event Action OnLogout;
|
||||
public event Action<bool> OnNewUserNeedsDisplayName; // true if new user
|
||||
|
||||
public bool IsAuthenticated => TokenStorage.HasValidToken;
|
||||
public string DisplayName => TokenStorage.DisplayName;
|
||||
public string UserId => TokenStorage.UserId;
|
||||
|
||||
private void Awake() {
|
||||
if (Instance != null && Instance != this) {
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
Instance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
// AuthClient is created lazily when SetServerUrl is called
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the server URL for OAuth requests. Call this before any OAuth operations.
|
||||
/// </summary>
|
||||
/// <param name="serverUrl">Full URL with scheme, e.g. "https://prod.eagle0.net"</param>
|
||||
public void SetServerUrl(string serverUrl) {
|
||||
if (_currentServerUrl == serverUrl && _authClient != null) {
|
||||
return; // Already configured for this URL
|
||||
}
|
||||
|
||||
_authClient?.Dispose();
|
||||
_currentServerUrl = serverUrl;
|
||||
_authClient = new AuthClient(serverUrl);
|
||||
Debug.Log($"[OAuthManager] Configured for server: {serverUrl}");
|
||||
}
|
||||
|
||||
private void OnDestroy() {
|
||||
_authClient?.Dispose();
|
||||
if (Instance == this) { Instance = null; }
|
||||
}
|
||||
|
||||
private void EnsureConfigured() {
|
||||
if (_authClient == null) {
|
||||
throw new InvalidOperationException(
|
||||
"OAuthManager not configured. Call SetServerUrl() first.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start OAuth login with specified provider.
|
||||
/// Opens system browser for user consent, then polls for completion.
|
||||
/// </summary>
|
||||
public async Task<CheckOAuthStatusResponse> LoginAsync(OAuthProvider provider) {
|
||||
EnsureConfigured();
|
||||
try {
|
||||
// Get OAuth URL and state from server
|
||||
var (authUrl, state) = await _authClient.GetOAuthUrlAsync(provider);
|
||||
|
||||
// Open system browser
|
||||
Debug.Log($"[OAuthManager] Opening browser: {authUrl}");
|
||||
Application.OpenURL(authUrl);
|
||||
|
||||
// Poll for OAuth completion (server handles the callback)
|
||||
var response = await _authClient.PollForOAuthCompletionAsync(state);
|
||||
|
||||
// Store tokens
|
||||
TokenStorage.StoreTokens(
|
||||
response.AccessToken,
|
||||
response.RefreshToken,
|
||||
response.ExpiresAt,
|
||||
response.User.UserId,
|
||||
response.User.DisplayName ?? "");
|
||||
|
||||
// Notify listeners
|
||||
if (response.IsNewUser) {
|
||||
OnNewUserNeedsDisplayName?.Invoke(true);
|
||||
} else {
|
||||
OnLoginSuccess?.Invoke(response.User);
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (Exception ex) {
|
||||
Debug.LogError($"[OAuthManager] Login failed: {ex.Message}");
|
||||
OnLoginFailed?.Invoke(ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set display name for new user after OAuth.
|
||||
/// </summary>
|
||||
public async Task<bool> SetDisplayNameAsync(string displayName) {
|
||||
EnsureConfigured();
|
||||
var response = await _authClient.SetDisplayNameAsync(displayName);
|
||||
|
||||
if (response.Success) { OnLoginSuccess?.Invoke(response.User); }
|
||||
|
||||
return response.Success;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to restore session from stored tokens.
|
||||
/// Returns true if valid session exists.
|
||||
/// Must call SetServerUrl() before this method.
|
||||
/// </summary>
|
||||
public async Task<bool> TryRestoreSessionAsync() {
|
||||
if (_authClient == null) {
|
||||
Debug.Log("[OAuthManager] Not configured yet, skipping session restore");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TokenStorage.HasValidToken && !TokenStorage.HasRefreshToken) {
|
||||
Debug.Log("[OAuthManager] No stored session");
|
||||
return false;
|
||||
}
|
||||
|
||||
// If token is expired but we have refresh token, try to refresh
|
||||
if (!TokenStorage.HasValidToken && TokenStorage.HasRefreshToken) {
|
||||
try {
|
||||
await _authClient.RefreshTokenAsync();
|
||||
} catch (Exception ex) {
|
||||
Debug.LogWarning($"[OAuthManager] Token refresh failed: {ex.Message}");
|
||||
TokenStorage.Clear();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate session with server
|
||||
try {
|
||||
var response = await _authClient.GetCurrentUserAsync();
|
||||
Debug.Log($"[OAuthManager] Session restored for {response.User.DisplayName}");
|
||||
OnLoginSuccess?.Invoke(response.User);
|
||||
return true;
|
||||
} catch (Exception ex) {
|
||||
Debug.LogWarning($"[OAuthManager] Session validation failed: {ex.Message}");
|
||||
TokenStorage.Clear();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refresh access token if needed.
|
||||
/// Call this before making API requests.
|
||||
/// </summary>
|
||||
public async Task EnsureValidTokenAsync() {
|
||||
EnsureConfigured();
|
||||
if (TokenStorage.NeedsRefresh && TokenStorage.HasRefreshToken) {
|
||||
await _authClient.RefreshTokenAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logout and clear stored tokens.
|
||||
/// </summary>
|
||||
public async Task LogoutAsync() {
|
||||
// LogoutAsync can work even without server connection - just clear local tokens
|
||||
if (_authClient != null) {
|
||||
await _authClient.LogoutAsync();
|
||||
} else {
|
||||
TokenStorage.Clear();
|
||||
}
|
||||
OnLogout?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0e9d5b0cb9f26477eaf26e8a09c41707
|
||||
@@ -1,122 +0,0 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Auth {
|
||||
/// <summary>
|
||||
/// Secure storage for OAuth tokens using Unity's PlayerPrefs.
|
||||
/// In production, consider using platform-specific secure storage
|
||||
/// (Keychain on iOS, Keystore on Android).
|
||||
/// </summary>
|
||||
public static class TokenStorage {
|
||||
private const string AccessTokenKey = "eagle0_access_token";
|
||||
private const string RefreshTokenKey = "eagle0_refresh_token";
|
||||
private const string ExpiresAtKey = "eagle0_expires_at";
|
||||
private const string UserIdKey = "eagle0_user_id";
|
||||
private const string DisplayNameKey = "eagle0_display_name";
|
||||
|
||||
public static bool HasValidToken {
|
||||
get {
|
||||
var token = AccessToken;
|
||||
var expiresAt = ExpiresAt;
|
||||
return !string.IsNullOrEmpty(token) &&
|
||||
expiresAt > DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
}
|
||||
}
|
||||
|
||||
public static bool HasRefreshToken => !string.IsNullOrEmpty(RefreshToken);
|
||||
|
||||
public static string AccessToken {
|
||||
get => PlayerPrefs.GetString(AccessTokenKey, null);
|
||||
private
|
||||
set => PlayerPrefs.SetString(AccessTokenKey, value ?? "");
|
||||
}
|
||||
|
||||
public static string RefreshToken {
|
||||
get => PlayerPrefs.GetString(RefreshTokenKey, null);
|
||||
private
|
||||
set => PlayerPrefs.SetString(RefreshTokenKey, value ?? "");
|
||||
}
|
||||
|
||||
public static long ExpiresAt {
|
||||
get => long.TryParse(PlayerPrefs.GetString(ExpiresAtKey, "0"), out var val) ? val : 0;
|
||||
private
|
||||
set => PlayerPrefs.SetString(ExpiresAtKey, value.ToString());
|
||||
}
|
||||
|
||||
public static string UserId {
|
||||
get => PlayerPrefs.GetString(UserIdKey, null);
|
||||
private
|
||||
set => PlayerPrefs.SetString(UserIdKey, value ?? "");
|
||||
}
|
||||
|
||||
public static string DisplayName {
|
||||
get => PlayerPrefs.GetString(DisplayNameKey, null);
|
||||
private
|
||||
set => PlayerPrefs.SetString(DisplayNameKey, value ?? "");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Store tokens received from OAuth exchange.
|
||||
/// </summary>
|
||||
public static void StoreTokens(
|
||||
string accessToken,
|
||||
string refreshToken,
|
||||
long expiresAt,
|
||||
string userId,
|
||||
string displayName) {
|
||||
AccessToken = accessToken;
|
||||
RefreshToken = refreshToken;
|
||||
ExpiresAt = expiresAt;
|
||||
UserId = userId;
|
||||
DisplayName = displayName;
|
||||
PlayerPrefs.Save();
|
||||
|
||||
Debug.Log(
|
||||
$"[TokenStorage] Stored tokens for user {displayName} (expires at {expiresAt})");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update access token after refresh.
|
||||
/// </summary>
|
||||
public static void UpdateAccessToken(string accessToken, long expiresAt) {
|
||||
AccessToken = accessToken;
|
||||
ExpiresAt = expiresAt;
|
||||
PlayerPrefs.Save();
|
||||
|
||||
Debug.Log($"[TokenStorage] Updated access token (expires at {expiresAt})");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update display name after user sets it.
|
||||
/// </summary>
|
||||
public static void UpdateDisplayName(string displayName) {
|
||||
DisplayName = displayName;
|
||||
PlayerPrefs.Save();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear all stored tokens (logout).
|
||||
/// </summary>
|
||||
public static void Clear() {
|
||||
PlayerPrefs.DeleteKey(AccessTokenKey);
|
||||
PlayerPrefs.DeleteKey(RefreshTokenKey);
|
||||
PlayerPrefs.DeleteKey(ExpiresAtKey);
|
||||
PlayerPrefs.DeleteKey(UserIdKey);
|
||||
PlayerPrefs.DeleteKey(DisplayNameKey);
|
||||
PlayerPrefs.Save();
|
||||
|
||||
Debug.Log("[TokenStorage] Cleared all tokens");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if token needs refresh (expires within 5 minutes).
|
||||
/// </summary>
|
||||
public static bool NeedsRefresh {
|
||||
get {
|
||||
var expiresAt = ExpiresAt;
|
||||
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
return expiresAt > 0 && expiresAt - now < 300; // 5 minutes
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4b1f130cec656466893fa04ba789d34f
|
||||
+13
-321
@@ -29,9 +29,9 @@ RectTransform:
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 616210323856016607}
|
||||
m_RootOrder: 0
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
@@ -105,23 +105,20 @@ MonoBehaviour:
|
||||
m_VerticalAlignment: 512
|
||||
m_textAlignment: 65535
|
||||
m_characterSpacing: 0
|
||||
m_characterHorizontalScale: 1
|
||||
m_wordSpacing: 0
|
||||
m_lineSpacing: 0
|
||||
m_lineSpacingMax: 0
|
||||
m_paragraphSpacing: 0
|
||||
m_charWidthMaxAdj: 0
|
||||
m_TextWrappingMode: 1
|
||||
m_enableWordWrapping: 1
|
||||
m_wordWrappingRatios: 0.4
|
||||
m_overflowMode: 0
|
||||
m_linkedTextComponent: {fileID: 0}
|
||||
parentLinkedComponent: {fileID: 0}
|
||||
m_enableKerning: 1
|
||||
m_ActiveFontFeatures: 6e72656b
|
||||
m_enableExtraPadding: 0
|
||||
checkPaddingRequired: 0
|
||||
m_isRichText: 1
|
||||
m_EmojiFallbackSupport: 1
|
||||
m_parseCtrlCharacters: 1
|
||||
m_isOrthographic: 1
|
||||
m_isCullingEnabled: 0
|
||||
@@ -188,9 +185,9 @@ RectTransform:
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 616210323856016607}
|
||||
m_RootOrder: 2
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
@@ -264,23 +261,20 @@ MonoBehaviour:
|
||||
m_VerticalAlignment: 512
|
||||
m_textAlignment: 65535
|
||||
m_characterSpacing: 0
|
||||
m_characterHorizontalScale: 1
|
||||
m_wordSpacing: 0
|
||||
m_lineSpacing: 0
|
||||
m_lineSpacingMax: 0
|
||||
m_paragraphSpacing: 0
|
||||
m_charWidthMaxAdj: 0
|
||||
m_TextWrappingMode: 1
|
||||
m_enableWordWrapping: 1
|
||||
m_wordWrappingRatios: 0.4
|
||||
m_overflowMode: 0
|
||||
m_linkedTextComponent: {fileID: 0}
|
||||
parentLinkedComponent: {fileID: 0}
|
||||
m_enableKerning: 1
|
||||
m_ActiveFontFeatures: 6e72656b
|
||||
m_enableExtraPadding: 0
|
||||
checkPaddingRequired: 0
|
||||
m_isRichText: 1
|
||||
m_EmojiFallbackSupport: 1
|
||||
m_parseCtrlCharacters: 1
|
||||
m_isOrthographic: 1
|
||||
m_isCullingEnabled: 0
|
||||
@@ -350,10 +344,10 @@ RectTransform:
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 1806945232979884877}
|
||||
m_Father: {fileID: 616210323856016607}
|
||||
m_RootOrder: 3
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
@@ -431,7 +425,6 @@ MonoBehaviour:
|
||||
atime6: 0
|
||||
atime7: 0
|
||||
m_Mode: 0
|
||||
m_ColorSpace: -1
|
||||
m_NumColorKeys: 2
|
||||
m_NumAlphaKeys: 2
|
||||
_gradientType: 0
|
||||
@@ -571,9 +564,9 @@ RectTransform:
|
||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 7057983938046569337}
|
||||
m_RootOrder: 0
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
@@ -647,23 +640,20 @@ MonoBehaviour:
|
||||
m_VerticalAlignment: 512
|
||||
m_textAlignment: 65535
|
||||
m_characterSpacing: 0
|
||||
m_characterHorizontalScale: 1
|
||||
m_wordSpacing: 0
|
||||
m_lineSpacing: 0
|
||||
m_lineSpacingMax: 0
|
||||
m_paragraphSpacing: 0
|
||||
m_charWidthMaxAdj: 0
|
||||
m_TextWrappingMode: 1
|
||||
m_enableWordWrapping: 1
|
||||
m_wordWrappingRatios: 0.4
|
||||
m_overflowMode: 0
|
||||
m_linkedTextComponent: {fileID: 0}
|
||||
parentLinkedComponent: {fileID: 0}
|
||||
m_enableKerning: 1
|
||||
m_ActiveFontFeatures: 6e72656b
|
||||
m_enableExtraPadding: 0
|
||||
checkPaddingRequired: 0
|
||||
m_isRichText: 1
|
||||
m_EmojiFallbackSupport: 1
|
||||
m_parseCtrlCharacters: 1
|
||||
m_isOrthographic: 1
|
||||
m_isCullingEnabled: 0
|
||||
@@ -711,15 +701,13 @@ RectTransform:
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 2291298599107070085}
|
||||
- {fileID: 8906549728612534620}
|
||||
- {fileID: 6686534948942979506}
|
||||
- {fileID: 6700597635350808166}
|
||||
- {fileID: 8218751192785770547}
|
||||
- {fileID: 7057983938046569337}
|
||||
m_Father: {fileID: 0}
|
||||
m_RootOrder: 0
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
@@ -808,295 +796,31 @@ MonoBehaviour:
|
||||
playerCountField: {fileID: 8350902200036365361}
|
||||
joinButton: {fileID: 437043893514773017}
|
||||
buttonText: {fileID: 2170549136187147757}
|
||||
dropButton: {fileID: 19903828017934214}
|
||||
--- !u!1 &7127645211350815762
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 8218751192785770547}
|
||||
- component: {fileID: 3533697289353260343}
|
||||
m_Layer: 5
|
||||
m_Name: spacer
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &8218751192785770547
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7127645211350815762}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 616210323856016607}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &3533697289353260343
|
||||
--- !u!114 &2580770409874757745
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7127645211350815762}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
|
||||
m_IgnoreLayout: 0
|
||||
m_MinWidth: 10
|
||||
m_MinHeight: -1
|
||||
m_PreferredWidth: 10
|
||||
m_PreferredHeight: -1
|
||||
m_FlexibleWidth: -1
|
||||
m_FlexibleHeight: -1
|
||||
m_LayoutPriority: 1
|
||||
--- !u!1 &7934454793097069971
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 6700597635350808166}
|
||||
- component: {fileID: 5023923008883954850}
|
||||
- component: {fileID: 19903828017934214}
|
||||
- component: {fileID: 2151248441631091217}
|
||||
- component: {fileID: 7406323257372084547}
|
||||
m_Layer: 5
|
||||
m_Name: Drop Game Button
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &6700597635350808166
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7934454793097069971}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 1846732971937087380}
|
||||
m_Father: {fileID: 616210323856016607}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &5023923008883954850
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7934454793097069971}
|
||||
m_CullTransparentMesh: 0
|
||||
--- !u!114 &19903828017934214
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7934454793097069971}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Navigation:
|
||||
m_Mode: 0
|
||||
m_WrapAround: 0
|
||||
m_SelectOnUp: {fileID: 0}
|
||||
m_SelectOnDown: {fileID: 0}
|
||||
m_SelectOnLeft: {fileID: 0}
|
||||
m_SelectOnRight: {fileID: 0}
|
||||
m_Transition: 1
|
||||
m_Colors:
|
||||
m_NormalColor: {r: 1, g: 0.42745098, b: 0.42745098, a: 1}
|
||||
m_HighlightedColor: {r: 0.84313726, g: 0.84313726, b: 0.84313726, a: 1}
|
||||
m_PressedColor: {r: 0, g: 0, b: 0, a: 1}
|
||||
m_SelectedColor: {r: 0.84313726, g: 0.84313726, b: 0.84313726, a: 1}
|
||||
m_DisabledColor: {r: 1, g: 1, b: 1, a: 0.39215687}
|
||||
m_ColorMultiplier: 1
|
||||
m_FadeDuration: 0.1
|
||||
m_SpriteState:
|
||||
m_HighlightedSprite: {fileID: 0}
|
||||
m_PressedSprite: {fileID: 0}
|
||||
m_SelectedSprite: {fileID: 0}
|
||||
m_DisabledSprite: {fileID: 0}
|
||||
m_AnimationTriggers:
|
||||
m_NormalTrigger: Normal
|
||||
m_HighlightedTrigger: Highlighted
|
||||
m_PressedTrigger: Pressed
|
||||
m_SelectedTrigger: Highlighted
|
||||
m_DisabledTrigger: Disabled
|
||||
m_Interactable: 1
|
||||
m_TargetGraphic: {fileID: 2151248441631091217}
|
||||
m_OnClick:
|
||||
m_PersistentCalls:
|
||||
m_Calls:
|
||||
- m_Target: {fileID: 7332722509251528870}
|
||||
m_TargetAssemblyTypeName: AvailableGameItem, Assembly-CSharp
|
||||
m_MethodName: DropClicked
|
||||
m_Mode: 1
|
||||
m_Arguments:
|
||||
m_ObjectArgument: {fileID: 0}
|
||||
m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine
|
||||
m_IntArgument: 0
|
||||
m_FloatArgument: 0
|
||||
m_StringArgument:
|
||||
m_BoolArgument: 0
|
||||
m_CallState: 2
|
||||
--- !u!114 &2151248441631091217
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7934454793097069971}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 21300000, guid: f03f625cc7127f244a2f4da835b92720, type: 3}
|
||||
m_Type: 1
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
m_FillMethod: 4
|
||||
m_FillAmount: 1
|
||||
m_FillClockwise: 1
|
||||
m_FillOrigin: 0
|
||||
m_UseSpriteMesh: 0
|
||||
m_PixelsPerUnitMultiplier: 1
|
||||
--- !u!114 &7406323257372084547
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7934454793097069971}
|
||||
m_GameObject: {fileID: 8906549728612534621}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_IgnoreLayout: 0
|
||||
m_MinWidth: 50
|
||||
m_MinWidth: 700
|
||||
m_MinHeight: -1
|
||||
m_PreferredWidth: 50
|
||||
m_PreferredWidth: 700
|
||||
m_PreferredHeight: -1
|
||||
m_FlexibleWidth: -1
|
||||
m_FlexibleWidth: 1
|
||||
m_FlexibleHeight: -1
|
||||
m_LayoutPriority: 1
|
||||
--- !u!1 &8839297347726735035
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 1846732971937087380}
|
||||
- component: {fileID: 2464975641276684442}
|
||||
- component: {fileID: 4051352560264867900}
|
||||
m_Layer: 5
|
||||
m_Name: Icon
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &1846732971937087380
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 8839297347726735035}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 6700597635350808166}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &2464975641276684442
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 8839297347726735035}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &4051352560264867900
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 8839297347726735035}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 1344c3c82d62a2a41a3576d8abb8e3ea, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.RawImage
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 0, g: 0, b: 0, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Texture: {fileID: 2800000, guid: 8c998213d53883d4b8fdf7a3eb3c2909, type: 3}
|
||||
m_UVRect:
|
||||
serializedVersion: 2
|
||||
x: 0
|
||||
y: 0
|
||||
width: 1
|
||||
height: 1
|
||||
--- !u!1001 &3324269289066796519
|
||||
PrefabInstance:
|
||||
m_ObjectHideFlags: 0
|
||||
serializedVersion: 2
|
||||
m_Modification:
|
||||
serializedVersion: 3
|
||||
m_TransformParent: {fileID: 616210323856016607}
|
||||
m_Modifications:
|
||||
- target: {fileID: 6176764421848850767, guid: 59599ef87ad824f52ada88f126d58c5f,
|
||||
@@ -1109,11 +833,6 @@ PrefabInstance:
|
||||
propertyPath: m_fontSizeBase
|
||||
value: 24
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 6176764421848850767, guid: 59599ef87ad824f52ada88f126d58c5f,
|
||||
type: 3}
|
||||
propertyPath: 'm_ActiveFontFeatures.Array.data[0]'
|
||||
value: 1801810542
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 6176764421927073094, guid: 59599ef87ad824f52ada88f126d58c5f,
|
||||
type: 3}
|
||||
propertyPath: m_fontSize
|
||||
@@ -1240,13 +959,6 @@ PrefabInstance:
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
m_RemovedComponents: []
|
||||
m_RemovedGameObjects: []
|
||||
m_AddedGameObjects: []
|
||||
m_AddedComponents:
|
||||
- targetCorrespondingSourceObject: {fileID: 6176764422977772730, guid: 59599ef87ad824f52ada88f126d58c5f,
|
||||
type: 3}
|
||||
insertIndex: -1
|
||||
addedObject: {fileID: 2580770409874757745}
|
||||
m_SourcePrefab: {fileID: 100100000, guid: 59599ef87ad824f52ada88f126d58c5f, type: 3}
|
||||
--- !u!114 &8906549728612534595 stripped
|
||||
MonoBehaviour:
|
||||
@@ -1272,23 +984,3 @@ GameObject:
|
||||
type: 3}
|
||||
m_PrefabInstance: {fileID: 3324269289066796519}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
--- !u!114 &2580770409874757745
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 8906549728612534621}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_IgnoreLayout: 0
|
||||
m_MinWidth: 700
|
||||
m_MinHeight: -1
|
||||
m_PreferredWidth: 700
|
||||
m_PreferredHeight: -1
|
||||
m_FlexibleWidth: 1
|
||||
m_FlexibleHeight: -1
|
||||
m_LayoutPriority: 1
|
||||
|
||||
-4
@@ -16,10 +16,8 @@ public class AvailableGameItem : MonoBehaviour {
|
||||
public TextMeshProUGUI playerCountField;
|
||||
public Button joinButton;
|
||||
public TextMeshProUGUI buttonText;
|
||||
public Button dropButton;
|
||||
|
||||
public Action<long, string> JoinCallback;
|
||||
public Action<long> DropCallback;
|
||||
|
||||
private List<AvailableLeader> availableLeaders;
|
||||
private long gameId;
|
||||
@@ -40,8 +38,6 @@ public class AvailableGameItem : MonoBehaviour {
|
||||
|
||||
public void JoinClicked() { JoinCallback(gameId, SelectedLeaderTextId); }
|
||||
|
||||
public void DropClicked() { DropCallback?.Invoke(gameId); }
|
||||
|
||||
public void
|
||||
SetAvailableNewGame(long gameId, string playersString, List<AvailableLeader> leaders) {
|
||||
availableLeaders = leaders;
|
||||
|
||||
+13
-253
@@ -7,18 +7,15 @@ using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Auth;
|
||||
using common;
|
||||
using common.GUIUtils;
|
||||
using eagle;
|
||||
using Grpc.Core;
|
||||
using Grpc.Core.Interceptors;
|
||||
using Net.Eagle0.Eagle.Api;
|
||||
using Net.Eagle0.Eagle.Api.Auth;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityEngine.UI;
|
||||
using Object = System.Object;
|
||||
|
||||
public class AuthInterceptor : Interceptor {
|
||||
@@ -85,23 +82,6 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
public TMP_InputField passwordField;
|
||||
public TMP_Dropdown resolutionDropdown;
|
||||
|
||||
[Header("OAuth UI")]
|
||||
public GameObject oauthPanel;
|
||||
public Button discordLoginButton;
|
||||
public Button googleLoginButton;
|
||||
public TextMeshProUGUI oauthStatusText;
|
||||
|
||||
[Header("Display Name Setup")]
|
||||
public GameObject displayNamePanel;
|
||||
public TMP_InputField displayNameField;
|
||||
public Button setDisplayNameButton;
|
||||
public TextMeshProUGUI displayNameErrorText;
|
||||
|
||||
[Header("Legacy Auth (for testing)")]
|
||||
public GameObject legacyAuthPanel;
|
||||
public Button useLegacyAuthButton;
|
||||
public TextMeshProUGUI useLegacyAuthButtonText;
|
||||
|
||||
[Header("Status Display")]
|
||||
public TextMeshProUGUI connectionStatusText;
|
||||
|
||||
@@ -118,8 +98,6 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
public GameObject waitingGamesListItemPrefab;
|
||||
public GameObject createGameListItemPrefab;
|
||||
|
||||
public DropGameConfirmationPanel dropGameConfirmationPanel;
|
||||
|
||||
public Canvas connectionCanvas;
|
||||
public Canvas eagleCanvas;
|
||||
public Canvas shardokCanvas;
|
||||
@@ -134,8 +112,6 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
private bool listen = false;
|
||||
private CancellationTokenSource _cancellationTokenSource;
|
||||
private readonly Object pendingReplyLock = new Object();
|
||||
private int _connectedEnvironmentIndex = -1; // -1 means not connected
|
||||
private bool _useOAuth = false; // Default to legacy until server-side OAuth is ready
|
||||
|
||||
public ClientPregeneratedText clientPregeneratedText;
|
||||
|
||||
@@ -149,27 +125,12 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
private static readonly List<string> EnvironmentDisplayNames =
|
||||
new() { "prod.", "qa.", "(none)" };
|
||||
|
||||
/// <summary>
|
||||
/// Returns the connected environment name ("prod." or "qa.") or null if not connected.
|
||||
/// </summary>
|
||||
public string ConnectedEnvironmentName =>
|
||||
_connectedEnvironmentIndex >= 0 &&
|
||||
_connectedEnvironmentIndex < EnvironmentDisplayNames.Count
|
||||
? EnvironmentDisplayNames[_connectedEnvironmentIndex]
|
||||
: null;
|
||||
|
||||
private string GetUrlFromEnvironment() {
|
||||
int envIndex = environmentDropdown.value;
|
||||
string prefix = EnvironmentOptions[envIndex];
|
||||
return prefix + BaseDomain;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get full URL with scheme for the selected environment.
|
||||
/// Remote servers use HTTPS, could be extended for local HTTP.
|
||||
/// </summary>
|
||||
private string GetFullUrlFromEnvironment() { return "https://" + GetUrlFromEnvironment(); }
|
||||
|
||||
public void ReceiveLobbyUpdate(LobbyResponse lobbyResponse) {
|
||||
_handleLobbyResponse(lobbyResponse);
|
||||
}
|
||||
@@ -205,164 +166,8 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
|
||||
errorHandler.gameObject.SetActive(true);
|
||||
|
||||
// Initialize OAuth UI
|
||||
SetupOAuthUI();
|
||||
|
||||
// Initialize status text
|
||||
UpdateConnectionStatus();
|
||||
|
||||
// Try to restore existing OAuth session
|
||||
TryRestoreSession();
|
||||
}
|
||||
|
||||
private void SetupOAuthUI() {
|
||||
// Set up OAuth button click handlers
|
||||
if (discordLoginButton != null) {
|
||||
discordLoginButton.onClick.AddListener(
|
||||
() => OnOAuthLoginClicked(OAuthProvider.Discord));
|
||||
}
|
||||
if (googleLoginButton != null) {
|
||||
googleLoginButton.onClick.AddListener(() => OnOAuthLoginClicked(OAuthProvider.Google));
|
||||
}
|
||||
if (setDisplayNameButton != null) {
|
||||
setDisplayNameButton.onClick.AddListener(OnSetDisplayNameClicked);
|
||||
}
|
||||
if (useLegacyAuthButton != null) {
|
||||
useLegacyAuthButton.onClick.AddListener(OnUseLegacyAuthClicked);
|
||||
}
|
||||
|
||||
// Subscribe to OAuthManager events
|
||||
if (OAuthManager.Instance != null) {
|
||||
OAuthManager.Instance.OnLoginSuccess += OnOAuthLoginSuccess;
|
||||
OAuthManager.Instance.OnLoginFailed += OnOAuthLoginFailed;
|
||||
OAuthManager.Instance.OnNewUserNeedsDisplayName += OnNewUserNeedsDisplayName;
|
||||
OAuthManager.Instance.OnLogout += OnOAuthLogout;
|
||||
}
|
||||
|
||||
// Show appropriate panel based on auth state
|
||||
ShowAuthPanel();
|
||||
}
|
||||
|
||||
private void ShowAuthPanel() {
|
||||
// Hide all auth panels first
|
||||
if (oauthPanel != null) oauthPanel.SetActive(false);
|
||||
if (displayNamePanel != null) displayNamePanel.SetActive(false);
|
||||
if (legacyAuthPanel != null) legacyAuthPanel.SetActive(false);
|
||||
|
||||
// Show appropriate panel based on _useOAuth
|
||||
if (_useOAuth) {
|
||||
if (oauthPanel != null) oauthPanel.SetActive(true);
|
||||
if (useLegacyAuthButtonText != null)
|
||||
useLegacyAuthButtonText.text = "Use Classic Sign-in";
|
||||
} else {
|
||||
if (legacyAuthPanel != null) legacyAuthPanel.SetActive(true);
|
||||
if (useLegacyAuthButtonText != null) useLegacyAuthButtonText.text = "Use OAuth Sign-in";
|
||||
}
|
||||
}
|
||||
|
||||
private async void TryRestoreSession() {
|
||||
if (OAuthManager.Instance == null) return;
|
||||
|
||||
// Configure OAuthManager with current environment URL
|
||||
OAuthManager.Instance.SetServerUrl(GetFullUrlFromEnvironment());
|
||||
|
||||
if (oauthStatusText != null) { oauthStatusText.text = "Checking for existing session..."; }
|
||||
|
||||
var restored = await OAuthManager.Instance.TryRestoreSessionAsync();
|
||||
if (restored) {
|
||||
// Session restored, connect to lobby
|
||||
ConnectWithOAuth();
|
||||
} else if (oauthStatusText != null) {
|
||||
oauthStatusText.text = "";
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnOAuthLoginClicked(OAuthProvider provider) {
|
||||
// Configure OAuthManager with current environment URL (user may have changed dropdown)
|
||||
OAuthManager.Instance.SetServerUrl(GetFullUrlFromEnvironment());
|
||||
|
||||
if (oauthStatusText != null) {
|
||||
oauthStatusText.text = $"Opening browser for {provider} login...";
|
||||
}
|
||||
|
||||
try {
|
||||
await OAuthManager.Instance.LoginAsync(provider);
|
||||
// OnLoginSuccess or OnNewUserNeedsDisplayName will be called
|
||||
} catch (Exception ex) {
|
||||
Debug.LogError($"OAuth login failed: {ex.Message}");
|
||||
if (oauthStatusText != null) { oauthStatusText.text = $"Login failed: {ex.Message}"; }
|
||||
}
|
||||
}
|
||||
|
||||
private void OnOAuthLoginSuccess(UserInfo user) {
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
if (oauthStatusText != null) { oauthStatusText.text = $"Welcome, {user.DisplayName}!"; }
|
||||
ConnectWithOAuth();
|
||||
});
|
||||
}
|
||||
|
||||
private void OnOAuthLoginFailed(string error) {
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
if (oauthStatusText != null) { oauthStatusText.text = $"Login failed: {error}"; }
|
||||
});
|
||||
}
|
||||
|
||||
private void OnNewUserNeedsDisplayName(bool isNewUser) {
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
// Show display name panel
|
||||
if (oauthPanel != null) oauthPanel.SetActive(false);
|
||||
if (displayNamePanel != null) displayNamePanel.SetActive(true);
|
||||
if (displayNameErrorText != null) displayNameErrorText.text = "";
|
||||
});
|
||||
}
|
||||
|
||||
private async void OnSetDisplayNameClicked() {
|
||||
if (displayNameField == null || OAuthManager.Instance == null) return;
|
||||
|
||||
var displayName = displayNameField.text.Trim();
|
||||
if (string.IsNullOrEmpty(displayName)) {
|
||||
if (displayNameErrorText != null) {
|
||||
displayNameErrorText.text = "Please enter a display name";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (displayNameErrorText != null) { displayNameErrorText.text = "Setting display name..."; }
|
||||
|
||||
var success = await OAuthManager.Instance.SetDisplayNameAsync(displayName);
|
||||
if (!success && displayNameErrorText != null) {
|
||||
displayNameErrorText.text = "Name already taken or invalid. Try another.";
|
||||
}
|
||||
// OnLoginSuccess will be called if successful
|
||||
}
|
||||
|
||||
private void OnOAuthLogout() {
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
ShowAuthPanel();
|
||||
if (oauthStatusText != null) { oauthStatusText.text = ""; }
|
||||
});
|
||||
}
|
||||
|
||||
private void OnUseLegacyAuthClicked() {
|
||||
_useOAuth = !_useOAuth; // Toggle instead of just setting false
|
||||
|
||||
if (_useOAuth) {
|
||||
// Show OAuth panel
|
||||
if (oauthPanel != null) oauthPanel.SetActive(true);
|
||||
if (legacyAuthPanel != null) legacyAuthPanel.SetActive(false);
|
||||
if (useLegacyAuthButtonText != null)
|
||||
useLegacyAuthButtonText.text = "Use Classic Sign-in";
|
||||
} else {
|
||||
// Show legacy panel
|
||||
if (oauthPanel != null) oauthPanel.SetActive(false);
|
||||
if (legacyAuthPanel != null) legacyAuthPanel.SetActive(true);
|
||||
if (useLegacyAuthButtonText != null) useLegacyAuthButtonText.text = "Use OAuth Sign-in";
|
||||
}
|
||||
}
|
||||
|
||||
private void ConnectWithOAuth() {
|
||||
_useOAuth = true;
|
||||
_internalConnectEagle();
|
||||
}
|
||||
|
||||
private float _statusUpdateTimer = 0f;
|
||||
@@ -457,8 +262,6 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
_persistentClientConnection = null;
|
||||
eagleConnection?.Dispose();
|
||||
eagleConnection = null;
|
||||
|
||||
_connectedEnvironmentIndex = -1;
|
||||
}
|
||||
|
||||
public void OnResolutionChanged(int dropdownIndex) {
|
||||
@@ -494,10 +297,10 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
runningGamesListArea.transform,
|
||||
true);
|
||||
listItem.transform.localScale = new Vector3 { x = 1.0f, y = 1.0f, z = 1.0f };
|
||||
var runningGameItem = listItem.GetComponent<RunningGameItem>();
|
||||
runningGameItem.SetRunningGame(runningGame.GameId, runningGame.Leader);
|
||||
runningGameItem.GoCallback = this.SelectEagleGame;
|
||||
runningGameItem.DropCallback = this.DropGame;
|
||||
listItem.GetComponent<RunningGameItem>().SetRunningGame(
|
||||
runningGame.GameId,
|
||||
runningGame.Leader);
|
||||
listItem.GetComponent<RunningGameItem>().GoCallback = this.SelectEagleGame;
|
||||
}
|
||||
|
||||
// Set up available/waiting games table
|
||||
@@ -533,16 +336,14 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
availableGamesListArea.transform,
|
||||
true);
|
||||
listItem.transform.localScale = new Vector3 { x = 1.0f, y = 1.0f, z = 1.0f };
|
||||
var availableGameItem = listItem.GetComponent<AvailableGameItem>();
|
||||
availableGameItem.SetAvailableNewGame(
|
||||
listItem.GetComponent<AvailableGameItem>().SetAvailableNewGame(
|
||||
availableGame.GameId,
|
||||
string.Format(
|
||||
"{0} / {1}",
|
||||
availableGame.CurrentHumanPlayerCount,
|
||||
availableGame.MaxHumanPlayerCount),
|
||||
availableGame.AvailableLeaders.ToList());
|
||||
availableGameItem.JoinCallback = this.JoinGame;
|
||||
availableGameItem.DropCallback = this.DropGame;
|
||||
listItem.GetComponent<AvailableGameItem>().JoinCallback = this.JoinGame;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -556,8 +357,7 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
}
|
||||
|
||||
private void _createConnection() {
|
||||
_connectedEnvironmentIndex = environmentDropdown.value;
|
||||
PlayerPrefs.SetInt(EnvironmentKey, _connectedEnvironmentIndex);
|
||||
PlayerPrefs.SetInt(EnvironmentKey, environmentDropdown.value);
|
||||
PlayerPrefs.SetString(NameKey, nameField.text);
|
||||
PlayerPrefs.SetString(PasswordKey, passwordField.text);
|
||||
|
||||
@@ -571,26 +371,7 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
string url = GetUrlFromEnvironment();
|
||||
|
||||
if (_useOAuth && TokenStorage.HasValidToken) {
|
||||
// Use JWT-based connection
|
||||
eagleConnection = EagleConnection.CreateWithJwt(url);
|
||||
|
||||
_httpClient = new HttpClient();
|
||||
_httpClient.DefaultRequestHeaders.Authorization =
|
||||
new AuthenticationHeaderValue("Bearer", TokenStorage.AccessToken);
|
||||
} else {
|
||||
// Legacy Basic Auth connection
|
||||
PlayerPrefs.SetString(NameKey, nameField.text);
|
||||
PlayerPrefs.SetString(PasswordKey, passwordField.text);
|
||||
|
||||
eagleConnection = new EagleConnection(nameField.text, passwordField.text, url);
|
||||
|
||||
_httpClient = new HttpClient();
|
||||
_httpClient.DefaultRequestHeaders.Authorization =
|
||||
new AuthenticationHeaderValue("Basic", eagleConnection.authHeader);
|
||||
}
|
||||
|
||||
eagleConnection = new EagleConnection(nameField.text, passwordField.text, url);
|
||||
_persistentClientConnection = new PersistentClientConnection(
|
||||
eagleConnection.EagleGrpcClient,
|
||||
eagleConnection.credentials,
|
||||
@@ -598,6 +379,10 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
_persistentClientConnection.Connect();
|
||||
|
||||
errorHandler.PersistentClientConnection = _persistentClientConnection;
|
||||
|
||||
_httpClient = new HttpClient();
|
||||
_httpClient.DefaultRequestHeaders.Authorization =
|
||||
new AuthenticationHeaderValue("Basic", eagleConnection.authHeader);
|
||||
ResourceFetcher.SetUpConnection(_httpClient);
|
||||
}
|
||||
|
||||
@@ -655,18 +440,6 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
var game = _internalJoinEagleGame(gameId, leaderName);
|
||||
}
|
||||
|
||||
private void DropGame(long gameId) {
|
||||
if (dropGameConfirmationPanel != null) {
|
||||
dropGameConfirmationPanel.OnConfirm = ConfirmDropGame;
|
||||
dropGameConfirmationPanel.Show(gameId, "Are you sure you want to leave this game?");
|
||||
} else {
|
||||
// Fallback if no confirmation panel is configured
|
||||
ConfirmDropGame(gameId);
|
||||
}
|
||||
}
|
||||
|
||||
private void ConfirmDropGame(long gameId) { _internalDropGame(gameId); }
|
||||
|
||||
private void CreateGame(string leaderNameTextId, int humanPlayerCount, int totalPlayerCount) {
|
||||
var game = _internalCreateGame(leaderNameTextId, humanPlayerCount, totalPlayerCount);
|
||||
}
|
||||
@@ -743,18 +516,6 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> _internalDropGame(long gameId) {
|
||||
try {
|
||||
return await _persistentClientConnection.SendUpdateStreamRequestAsync(
|
||||
new UpdateStreamRequest {
|
||||
DropGameRequest = new DropGameRequest { GameId = gameId }
|
||||
});
|
||||
} catch (WebException e) {
|
||||
Debug.Log(String.Format("Caught exception in DropGame {0}", e.Message));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetLobbyActive(bool active) {
|
||||
runningGamesListArea.SetActive(active);
|
||||
availableGamesListArea.SetActive(active);
|
||||
@@ -772,8 +533,7 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
eagleCanvas.GetComponent<EagleGameController>().SetUpGame(
|
||||
gameInfo.GameId,
|
||||
gameInfo.FactionId,
|
||||
_persistentClientConnection,
|
||||
ConnectedEnvironmentName);
|
||||
_persistentClientConnection);
|
||||
connectionCanvas.enabled = false;
|
||||
connectionCanvas.gameObject.SetActive(false);
|
||||
eagleCanvas.gameObject.SetActive(true);
|
||||
|
||||
-24
@@ -1,24 +0,0 @@
|
||||
using System;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
public class DropGameConfirmationPanel : MonoBehaviour {
|
||||
public TextMeshProUGUI messageText;
|
||||
|
||||
public Action<long> OnConfirm;
|
||||
|
||||
private long _pendingGameId;
|
||||
|
||||
public void Show(long gameId, string message) {
|
||||
_pendingGameId = gameId;
|
||||
if (messageText != null) { messageText.text = message; }
|
||||
gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
public void ConfirmClicked() {
|
||||
gameObject.SetActive(false);
|
||||
OnConfirm?.Invoke(_pendingGameId);
|
||||
}
|
||||
|
||||
public void CancelClicked() { gameObject.SetActive(false); }
|
||||
}
|
||||
-2
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 558d4c7321daa4fe18dc4759a130d919
|
||||
+9
-304
@@ -29,9 +29,9 @@ RectTransform:
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 909271087739711220}
|
||||
m_RootOrder: 0
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
@@ -105,23 +105,20 @@ MonoBehaviour:
|
||||
m_VerticalAlignment: 512
|
||||
m_textAlignment: 65535
|
||||
m_characterSpacing: 0
|
||||
m_characterHorizontalScale: 1
|
||||
m_wordSpacing: 0
|
||||
m_lineSpacing: 0
|
||||
m_lineSpacingMax: 0
|
||||
m_paragraphSpacing: 0
|
||||
m_charWidthMaxAdj: 0
|
||||
m_TextWrappingMode: 1
|
||||
m_enableWordWrapping: 1
|
||||
m_wordWrappingRatios: 0.4
|
||||
m_overflowMode: 0
|
||||
m_linkedTextComponent: {fileID: 0}
|
||||
parentLinkedComponent: {fileID: 0}
|
||||
m_enableKerning: 1
|
||||
m_ActiveFontFeatures: 6e72656b
|
||||
m_enableExtraPadding: 0
|
||||
checkPaddingRequired: 0
|
||||
m_isRichText: 1
|
||||
m_EmojiFallbackSupport: 1
|
||||
m_parseCtrlCharacters: 1
|
||||
m_isOrthographic: 1
|
||||
m_isCullingEnabled: 0
|
||||
@@ -188,9 +185,9 @@ RectTransform:
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 909271087739711220}
|
||||
m_RootOrder: 1
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
@@ -264,23 +261,20 @@ MonoBehaviour:
|
||||
m_VerticalAlignment: 512
|
||||
m_textAlignment: 65535
|
||||
m_characterSpacing: 0
|
||||
m_characterHorizontalScale: 1
|
||||
m_wordSpacing: 0
|
||||
m_lineSpacing: 0
|
||||
m_lineSpacingMax: 0
|
||||
m_paragraphSpacing: 0
|
||||
m_charWidthMaxAdj: 0
|
||||
m_TextWrappingMode: 1
|
||||
m_enableWordWrapping: 1
|
||||
m_wordWrappingRatios: 0.4
|
||||
m_overflowMode: 0
|
||||
m_linkedTextComponent: {fileID: 0}
|
||||
parentLinkedComponent: {fileID: 0}
|
||||
m_enableKerning: 1
|
||||
m_ActiveFontFeatures: 6e72656b
|
||||
m_enableExtraPadding: 0
|
||||
checkPaddingRequired: 0
|
||||
m_isRichText: 1
|
||||
m_EmojiFallbackSupport: 1
|
||||
m_parseCtrlCharacters: 1
|
||||
m_isOrthographic: 1
|
||||
m_isCullingEnabled: 0
|
||||
@@ -318,216 +312,6 @@ MonoBehaviour:
|
||||
m_FlexibleWidth: 1
|
||||
m_FlexibleHeight: -1
|
||||
m_LayoutPriority: 1
|
||||
--- !u!1 &4189605341159952945
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 7336928442537586311}
|
||||
- component: {fileID: 3344684898730655831}
|
||||
m_Layer: 5
|
||||
m_Name: spacer
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &7336928442537586311
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 4189605341159952945}
|
||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 909271087739711220}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &3344684898730655831
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 4189605341159952945}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
|
||||
m_IgnoreLayout: 0
|
||||
m_MinWidth: 10
|
||||
m_MinHeight: -1
|
||||
m_PreferredWidth: 10
|
||||
m_PreferredHeight: -1
|
||||
m_FlexibleWidth: -1
|
||||
m_FlexibleHeight: -1
|
||||
m_LayoutPriority: 1
|
||||
--- !u!1 &4678275134637000934
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 1348356180262758599}
|
||||
- component: {fileID: 8617884038396701234}
|
||||
- component: {fileID: 4081637582368108930}
|
||||
- component: {fileID: 5167949463556339992}
|
||||
- component: {fileID: 7482580409419131173}
|
||||
m_Layer: 5
|
||||
m_Name: Drop Game Button
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &1348356180262758599
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 4678275134637000934}
|
||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 6454652926630703861}
|
||||
m_Father: {fileID: 909271087739711220}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &8617884038396701234
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 4678275134637000934}
|
||||
m_CullTransparentMesh: 0
|
||||
--- !u!114 &4081637582368108930
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 4678275134637000934}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Navigation:
|
||||
m_Mode: 0
|
||||
m_WrapAround: 0
|
||||
m_SelectOnUp: {fileID: 0}
|
||||
m_SelectOnDown: {fileID: 0}
|
||||
m_SelectOnLeft: {fileID: 0}
|
||||
m_SelectOnRight: {fileID: 0}
|
||||
m_Transition: 1
|
||||
m_Colors:
|
||||
m_NormalColor: {r: 1, g: 0.42745098, b: 0.42745098, a: 1}
|
||||
m_HighlightedColor: {r: 0.84313726, g: 0.84313726, b: 0.84313726, a: 1}
|
||||
m_PressedColor: {r: 0, g: 0, b: 0, a: 1}
|
||||
m_SelectedColor: {r: 0.84313726, g: 0.84313726, b: 0.84313726, a: 1}
|
||||
m_DisabledColor: {r: 1, g: 1, b: 1, a: 0.39215687}
|
||||
m_ColorMultiplier: 1
|
||||
m_FadeDuration: 0.1
|
||||
m_SpriteState:
|
||||
m_HighlightedSprite: {fileID: 0}
|
||||
m_PressedSprite: {fileID: 0}
|
||||
m_SelectedSprite: {fileID: 0}
|
||||
m_DisabledSprite: {fileID: 0}
|
||||
m_AnimationTriggers:
|
||||
m_NormalTrigger: Normal
|
||||
m_HighlightedTrigger: Highlighted
|
||||
m_PressedTrigger: Pressed
|
||||
m_SelectedTrigger: Highlighted
|
||||
m_DisabledTrigger: Disabled
|
||||
m_Interactable: 1
|
||||
m_TargetGraphic: {fileID: 5167949463556339992}
|
||||
m_OnClick:
|
||||
m_PersistentCalls:
|
||||
m_Calls:
|
||||
- m_Target: {fileID: 7788402836057688155}
|
||||
m_TargetAssemblyTypeName: RunningGameItem, Assembly-CSharp
|
||||
m_MethodName: DropClicked
|
||||
m_Mode: 1
|
||||
m_Arguments:
|
||||
m_ObjectArgument: {fileID: 0}
|
||||
m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine
|
||||
m_IntArgument: 0
|
||||
m_FloatArgument: 0
|
||||
m_StringArgument:
|
||||
m_BoolArgument: 0
|
||||
m_CallState: 2
|
||||
--- !u!114 &5167949463556339992
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 4678275134637000934}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 21300000, guid: f03f625cc7127f244a2f4da835b92720, type: 3}
|
||||
m_Type: 1
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
m_FillMethod: 4
|
||||
m_FillAmount: 1
|
||||
m_FillClockwise: 1
|
||||
m_FillOrigin: 0
|
||||
m_UseSpriteMesh: 0
|
||||
m_PixelsPerUnitMultiplier: 1
|
||||
--- !u!114 &7482580409419131173
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 4678275134637000934}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_IgnoreLayout: 0
|
||||
m_MinWidth: 0
|
||||
m_MinHeight: -1
|
||||
m_PreferredWidth: 50
|
||||
m_PreferredHeight: -1
|
||||
m_FlexibleWidth: -1
|
||||
m_FlexibleHeight: -1
|
||||
m_LayoutPriority: 1
|
||||
--- !u!1 &4703848583262435150
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -556,9 +340,9 @@ RectTransform:
|
||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 7341333336313446738}
|
||||
m_RootOrder: 0
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
@@ -632,23 +416,20 @@ MonoBehaviour:
|
||||
m_VerticalAlignment: 512
|
||||
m_textAlignment: 65535
|
||||
m_characterSpacing: 0
|
||||
m_characterHorizontalScale: 1
|
||||
m_wordSpacing: 0
|
||||
m_lineSpacing: 0
|
||||
m_lineSpacingMax: 0
|
||||
m_paragraphSpacing: 0
|
||||
m_charWidthMaxAdj: 0
|
||||
m_TextWrappingMode: 1
|
||||
m_enableWordWrapping: 1
|
||||
m_wordWrappingRatios: 0.4
|
||||
m_overflowMode: 0
|
||||
m_linkedTextComponent: {fileID: 0}
|
||||
parentLinkedComponent: {fileID: 0}
|
||||
m_enableKerning: 1
|
||||
m_ActiveFontFeatures: 6e72656b
|
||||
m_enableExtraPadding: 0
|
||||
checkPaddingRequired: 0
|
||||
m_isRichText: 1
|
||||
m_EmojiFallbackSupport: 1
|
||||
m_parseCtrlCharacters: 1
|
||||
m_isOrthographic: 1
|
||||
m_isCullingEnabled: 0
|
||||
@@ -698,10 +479,10 @@ RectTransform:
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 2089905691925287270}
|
||||
m_Father: {fileID: 909271087739711220}
|
||||
m_RootOrder: 2
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
@@ -779,7 +560,6 @@ MonoBehaviour:
|
||||
atime6: 0
|
||||
atime7: 0
|
||||
m_Mode: 0
|
||||
m_ColorSpace: -1
|
||||
m_NumColorKeys: 2
|
||||
m_NumAlphaKeys: 2
|
||||
_gradientType: 0
|
||||
@@ -922,14 +702,12 @@ RectTransform:
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 2008335870574158510}
|
||||
- {fileID: 6674238761151365000}
|
||||
- {fileID: 1348356180262758599}
|
||||
- {fileID: 7336928442537586311}
|
||||
- {fileID: 7341333336313446738}
|
||||
m_Father: {fileID: 0}
|
||||
m_RootOrder: 0
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
@@ -1015,8 +793,7 @@ MonoBehaviour:
|
||||
item: {fileID: 5643565463360785033}
|
||||
gameIdField: {fileID: 8058295588038032232}
|
||||
leaderField: {fileID: 3311450659768657245}
|
||||
goButton: {fileID: 145214844678457394}
|
||||
dropButton: {fileID: 4081637582368108930}
|
||||
goButton: {fileID: 0}
|
||||
--- !u!114 &2208043217657811503
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -1037,75 +814,3 @@ MonoBehaviour:
|
||||
m_FlexibleWidth: 1
|
||||
m_FlexibleHeight: 1
|
||||
m_LayoutPriority: 1
|
||||
--- !u!1 &7814378479006347708
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 6454652926630703861}
|
||||
- component: {fileID: 374667974001408770}
|
||||
- component: {fileID: 5621315126805048060}
|
||||
m_Layer: 5
|
||||
m_Name: Icon
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &6454652926630703861
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7814378479006347708}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 1348356180262758599}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &374667974001408770
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7814378479006347708}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &5621315126805048060
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7814378479006347708}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 1344c3c82d62a2a41a3576d8abb8e3ea, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.RawImage
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 0, g: 0, b: 0, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Texture: {fileID: 2800000, guid: 8c998213d53883d4b8fdf7a3eb3c2909, type: 3}
|
||||
m_UVRect:
|
||||
serializedVersion: 2
|
||||
x: 0
|
||||
y: 0
|
||||
width: 1
|
||||
height: 1
|
||||
|
||||
-4
@@ -10,10 +10,8 @@ public class RunningGameItem : MonoBehaviour {
|
||||
public TextMeshProUGUI gameIdField;
|
||||
public TextMeshProUGUI leaderField;
|
||||
public Button goButton;
|
||||
public Button dropButton;
|
||||
|
||||
public Action<long> GoCallback;
|
||||
public Action<long> DropCallback;
|
||||
|
||||
private long gameId;
|
||||
|
||||
@@ -22,8 +20,6 @@ public class RunningGameItem : MonoBehaviour {
|
||||
|
||||
public void GoClicked() { GoCallback(gameId); }
|
||||
|
||||
public void DropClicked() { DropCallback?.Invoke(gameId); }
|
||||
|
||||
public void SetRunningGame(long gameId, AvailableLeader leader) {
|
||||
this.gameId = gameId;
|
||||
|
||||
|
||||
+49
-142
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using EagleGUIUtils;
|
||||
@@ -6,7 +6,6 @@ using Net.Eagle0.Eagle.Api;
|
||||
using Net.Eagle0.Eagle.Common;
|
||||
using Net.Eagle0.Eagle.Views;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace eagle {
|
||||
@@ -15,44 +14,16 @@ namespace eagle {
|
||||
|
||||
public class ImproveCommandSelector : CommandSelector {
|
||||
public HeroDropdownController heroDropdownController;
|
||||
public TMP_Dropdown typeDropdown;
|
||||
public Toggle lockToggle;
|
||||
|
||||
// Toggle buttons for each improvement type (assign to same ToggleGroup in Unity)
|
||||
public Toggle economyToggle;
|
||||
public Toggle agricultureToggle;
|
||||
public Toggle infrastructureToggle;
|
||||
public Toggle devastationToggle;
|
||||
public ToggleGroup improvementToggleGroup;
|
||||
|
||||
// Labels showing type name and value
|
||||
public TMP_Text economyLabel;
|
||||
public TMP_Text agricultureLabel;
|
||||
public TMP_Text infrastructureLabel;
|
||||
public TMP_Text devastationLabel;
|
||||
|
||||
List<HeroView> orderedHeroes;
|
||||
|
||||
private int SelectedTypeIndex => typeDropdown.value;
|
||||
private ImproveAvailableCommand ImproveAvailableCommand => _availableCommand.ImproveCommand;
|
||||
private ProvinceId ActingProvinceId => ImproveAvailableCommand.ActingProvinceId;
|
||||
|
||||
private ImprovementType SelectedType {
|
||||
get {
|
||||
if (economyToggle != null && economyToggle.isOn && economyToggle.interactable)
|
||||
return ImprovementType.Economy;
|
||||
if (agricultureToggle != null && agricultureToggle.isOn &&
|
||||
agricultureToggle.interactable)
|
||||
return ImprovementType.Agriculture;
|
||||
if (infrastructureToggle != null && infrastructureToggle.isOn &&
|
||||
infrastructureToggle.interactable)
|
||||
return ImprovementType.Infrastructure;
|
||||
if (devastationToggle != null && devastationToggle.isOn &&
|
||||
devastationToggle.interactable)
|
||||
return ImprovementType.Devastation;
|
||||
|
||||
// Fallback: return first available type
|
||||
return ImproveAvailableCommand.AvailableTypes.FirstOrDefault();
|
||||
}
|
||||
}
|
||||
private ImprovementType SelectedType =>
|
||||
ImproveAvailableCommand.AvailableTypes[SelectedTypeIndex];
|
||||
|
||||
private float OriginalImprovementValueForType(ImprovementType type) {
|
||||
var province = _model.Provinces[ActingProvinceId];
|
||||
@@ -86,142 +57,60 @@ namespace eagle {
|
||||
}
|
||||
}
|
||||
|
||||
private ImprovementType GetMinimumImprovementType() {
|
||||
ImprovementType minType = ImproveAvailableCommand.AvailableTypes[0];
|
||||
float minValue = OriginalImprovementValueForType(minType);
|
||||
private int MinimumImprovementIndex(ProvinceView province) {
|
||||
var minIndex = 0;
|
||||
float minValue =
|
||||
OriginalImprovementValueForType(ImproveAvailableCommand.AvailableTypes[0]);
|
||||
for (int i = 1; i < ImproveAvailableCommand.AvailableTypes.Count; i++) {
|
||||
var thisType = ImproveAvailableCommand.AvailableTypes[i];
|
||||
float thisVal = OriginalImprovementValueForType(thisType);
|
||||
float thisVal =
|
||||
OriginalImprovementValueForType(ImproveAvailableCommand.AvailableTypes[i]);
|
||||
if (thisVal < minValue) {
|
||||
minType = thisType;
|
||||
minIndex = i;
|
||||
minValue = thisVal;
|
||||
}
|
||||
}
|
||||
return minType;
|
||||
return minIndex;
|
||||
}
|
||||
|
||||
private ImprovementType GetDefaultImprovementType() {
|
||||
// Prefer devastation if available
|
||||
if (ImproveAvailableCommand.AvailableTypes.Contains(ImprovementType.Devastation)) {
|
||||
return ImprovementType.Devastation;
|
||||
}
|
||||
|
||||
// Otherwise pick the lowest stat
|
||||
return GetMinimumImprovementType();
|
||||
}
|
||||
|
||||
private Toggle GetToggleForType(ImprovementType type) {
|
||||
switch (type) {
|
||||
case ImprovementType.Economy: return economyToggle;
|
||||
case ImprovementType.Agriculture: return agricultureToggle;
|
||||
case ImprovementType.Infrastructure: return infrastructureToggle;
|
||||
case ImprovementType.Devastation: return devastationToggle;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
private TMP_Text GetLabelForType(ImprovementType type) {
|
||||
switch (type) {
|
||||
case ImprovementType.Economy: return economyLabel;
|
||||
case ImprovementType.Agriculture: return agricultureLabel;
|
||||
case ImprovementType.Infrastructure: return infrastructureLabel;
|
||||
case ImprovementType.Devastation: return devastationLabel;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void SelectType(ImprovementType type) {
|
||||
// Turn on the selected toggle - ToggleGroup handles turning off others
|
||||
var toggle = GetToggleForType(type);
|
||||
if (toggle != null && toggle.interactable) { toggle.isOn = true; }
|
||||
}
|
||||
|
||||
private const float DisabledAlpha = 0.35f;
|
||||
|
||||
private void ConfigureToggle(ImprovementType type, bool available) {
|
||||
var toggle = GetToggleForType(type);
|
||||
var label = GetLabelForType(type);
|
||||
|
||||
if (toggle == null) return;
|
||||
|
||||
// Always show the toggle, but disable if not available
|
||||
toggle.gameObject.SetActive(true);
|
||||
toggle.interactable = available;
|
||||
|
||||
// Turn off unavailable toggles so they don't appear selected
|
||||
if (!available) { toggle.isOn = false; }
|
||||
|
||||
// Only add to toggle group if available
|
||||
toggle.group = available ? improvementToggleGroup : null;
|
||||
|
||||
// Gray out unavailable toggles using CanvasGroup
|
||||
var canvasGroup = toggle.GetComponent<CanvasGroup>();
|
||||
if (canvasGroup == null) {
|
||||
canvasGroup = toggle.gameObject.AddComponent<CanvasGroup>();
|
||||
}
|
||||
canvasGroup.alpha = available ? 1f : DisabledAlpha;
|
||||
|
||||
// Always show label with current value
|
||||
if (label != null) { label.text = LabelStringForType(type); }
|
||||
}
|
||||
|
||||
private string LabelStringForType(ImprovementType type) {
|
||||
var originalStat =
|
||||
type == ImprovementType.Devastation
|
||||
? ProvinceStatUtils.RoundedDevastation(
|
||||
OriginalImprovementValueForType(type))
|
||||
: ProvinceStatUtils.RoundedStat(OriginalImprovementValueForType(type));
|
||||
if (type == ImprovementType.Devastation) {
|
||||
return GUIUtils.ColoredString(Color.red, originalStat.ToString());
|
||||
}
|
||||
var devastatedStat =
|
||||
ProvinceStatUtils.RoundedStat(EffectiveImprovementValueForType(type));
|
||||
if (devastatedStat == originalStat) {
|
||||
return $"{devastatedStat}";
|
||||
private void ChooseDefaultImprovement(ProvinceView province) {
|
||||
var devastationIndex =
|
||||
ImproveAvailableCommand.AvailableTypes.IndexOf(ImprovementType.Devastation);
|
||||
if (devastationIndex == -1) {
|
||||
typeDropdown.value = MinimumImprovementIndex(province);
|
||||
} else {
|
||||
return $"{GUIUtils.ColoredString(Color.red, devastatedStat.ToString())} / {originalStat}";
|
||||
typeDropdown.value = devastationIndex;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void SetUpUI() {
|
||||
var province = _model.Provinces[ImproveAvailableCommand.ActingProvinceId];
|
||||
var improveCommand = ImproveAvailableCommand;
|
||||
|
||||
typeDropdown.ClearOptions();
|
||||
|
||||
orderedHeroes = ImproveAvailableCommand.AvailableHeroIds.Select(id => _model.Heroes[id])
|
||||
.ToList();
|
||||
heroDropdownController.AvailableHeroes = orderedHeroes;
|
||||
|
||||
heroDropdownController.SelectedHeroId = improveCommand.RecommendedHeroId;
|
||||
|
||||
// Configure each toggle based on available types
|
||||
ConfigureToggle(
|
||||
ImprovementType.Economy,
|
||||
improveCommand.AvailableTypes.Contains(ImprovementType.Economy));
|
||||
ConfigureToggle(
|
||||
ImprovementType.Agriculture,
|
||||
improveCommand.AvailableTypes.Contains(ImprovementType.Agriculture));
|
||||
ConfigureToggle(
|
||||
ImprovementType.Infrastructure,
|
||||
improveCommand.AvailableTypes.Contains(ImprovementType.Infrastructure));
|
||||
ConfigureToggle(
|
||||
ImprovementType.Devastation,
|
||||
improveCommand.AvailableTypes.Contains(ImprovementType.Devastation));
|
||||
typeDropdown.AddOptions(
|
||||
improveCommand.AvailableTypes.Select(DropdownStringForType).ToList());
|
||||
|
||||
// Determine which type to select
|
||||
ImprovementType selectedType;
|
||||
if (improveCommand.LockedType != ImprovementType.None) {
|
||||
var lockedType = improveCommand.LockedType;
|
||||
if (improveCommand.AvailableTypes.Contains(lockedType)) {
|
||||
selectedType = lockedType;
|
||||
var lockedTypeIndex = improveCommand.AvailableTypes.IndexOf(lockedType);
|
||||
if (lockedTypeIndex > -1) {
|
||||
typeDropdown.value = lockedTypeIndex;
|
||||
lockToggle.isOn = true;
|
||||
} else {
|
||||
selectedType = GetDefaultImprovementType();
|
||||
ChooseDefaultImprovement(province);
|
||||
lockToggle.isOn = false;
|
||||
}
|
||||
} else {
|
||||
selectedType = GetDefaultImprovementType();
|
||||
ChooseDefaultImprovement(province);
|
||||
lockToggle.isOn = false;
|
||||
}
|
||||
|
||||
SelectType(selectedType);
|
||||
}
|
||||
|
||||
public override AvailableCommand.SealedValueOneofCase CommandType =>
|
||||
@@ -248,5 +137,23 @@ namespace eagle {
|
||||
LockType = lockToggle.isOn
|
||||
}
|
||||
};
|
||||
|
||||
private string DropdownStringForType(ImprovementType type) {
|
||||
var originalStat =
|
||||
type == ImprovementType.Devastation
|
||||
? ProvinceStatUtils.RoundedDevastation(
|
||||
OriginalImprovementValueForType(type))
|
||||
: ProvinceStatUtils.RoundedStat(OriginalImprovementValueForType(type));
|
||||
if (type == ImprovementType.Devastation) {
|
||||
return $"{type} ({GUIUtils.ColoredString(UnityEngine.Color.red, originalStat.ToString())})";
|
||||
}
|
||||
var devastatedStat =
|
||||
ProvinceStatUtils.RoundedStat(EffectiveImprovementValueForType(type));
|
||||
if (devastatedStat == originalStat) {
|
||||
return $"{type} ({devastatedStat})";
|
||||
} else {
|
||||
return $"{type} ({GUIUtils.ColoredString(UnityEngine.Color.red, devastatedStat.ToString())} / {originalStat})";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,10 +26,6 @@ namespace eagle {
|
||||
private TextMeshProUGUI _textComponent;
|
||||
private PersistentClientConnection _connection;
|
||||
private IGameStateProvider _gameStateProvider;
|
||||
private string _environmentName;
|
||||
|
||||
[Tooltip("Optional text component to display connected environment (prod/qa)")]
|
||||
public TextMeshProUGUI environmentIndicatorText;
|
||||
|
||||
[Tooltip("Optional button to force immediate reconnection when server is down")]
|
||||
public Button retryButton;
|
||||
@@ -68,15 +64,6 @@ namespace eagle {
|
||||
_gameStateProvider = provider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the environment name to display (e.g., "prod." or "qa.").
|
||||
/// Pass null to clear the indicator.
|
||||
/// </summary>
|
||||
public void SetEnvironment(string environmentName) {
|
||||
_environmentName = environmentName;
|
||||
UpdateEnvironmentIndicator();
|
||||
}
|
||||
|
||||
void Update() {
|
||||
if (_connection == null || _textComponent == null) { return; }
|
||||
|
||||
@@ -178,20 +165,5 @@ namespace eagle {
|
||||
private void OnRetryClicked() {
|
||||
if (_connection != null) { _connection.ForceReconnect(); }
|
||||
}
|
||||
|
||||
private void UpdateEnvironmentIndicator() {
|
||||
if (environmentIndicatorText == null) return;
|
||||
|
||||
if (string.IsNullOrEmpty(_environmentName)) {
|
||||
environmentIndicatorText.text = "";
|
||||
return;
|
||||
}
|
||||
|
||||
// Color prod green, qa yellow
|
||||
environmentIndicatorText.text =
|
||||
_environmentName switch { "prod." => $"<color=green>{_environmentName}</color>",
|
||||
"qa." => $"<color=yellow>{_environmentName}</color>",
|
||||
_ => _environmentName };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BIN
Binary file not shown.
-117
@@ -1,117 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1fb1f6deb68f9475281c9f7c427a7024
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 1
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 0
|
||||
wrapV: 0
|
||||
wrapW: 0
|
||||
nPOTScale: 1
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 0
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 0
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 0
|
||||
swizzle: 50462976
|
||||
cookieLightType: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 4
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
customData:
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spriteCustomMetadata:
|
||||
entries: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -243,8 +243,7 @@ namespace eagle {
|
||||
public void SetUpGame(
|
||||
long gameId,
|
||||
int? playerId,
|
||||
PersistentClientConnection persistentClientConnection,
|
||||
string environmentName = null) {
|
||||
PersistentClientConnection persistentClientConnection) {
|
||||
ModelUpdater = new GameModelUpdater(
|
||||
gameId,
|
||||
playerId,
|
||||
@@ -270,12 +269,9 @@ namespace eagle {
|
||||
|
||||
ModelUpdater.ErrorHandler = errorHandler;
|
||||
|
||||
// Set up game state provider and environment for connection status UI
|
||||
// Set up game state provider for connection status UI
|
||||
var statusUI = connectionStatusLabel.GetComponent<ConnectionStatusUI>();
|
||||
if (statusUI != null) {
|
||||
statusUI.SetGameStateProvider(ModelUpdater);
|
||||
statusUI.SetEnvironment(environmentName);
|
||||
}
|
||||
if (statusUI != null) { statusUI.SetGameStateProvider(ModelUpdater); }
|
||||
|
||||
// Fire-and-forget - subscription is awaited internally and failures are logged
|
||||
MainQueue.Q.EnqueueForNextUpdate(
|
||||
|
||||
@@ -348,10 +348,6 @@ namespace eagle {
|
||||
case GameUpdate.GameUpdateDetailsOneofCase.ShardokActionResultResponse:
|
||||
var specResponse = updateItem.ShardokActionResultResponse.ShardokGameResponses;
|
||||
|
||||
// Collect ended games to remove AFTER the UI callback, so the UI
|
||||
// can see the final battle results before the models are removed.
|
||||
var endedGames = new List<ShardokGameId>();
|
||||
|
||||
foreach (var oneResponse in specResponse) {
|
||||
if (!_currentModel.ShardokGameModels.TryGetValue(
|
||||
oneResponse.ShardokGameId,
|
||||
@@ -392,22 +388,14 @@ namespace eagle {
|
||||
_currentModel.ShardokGameModels[oneResponse.ShardokGameId] =
|
||||
shardokGameModel;
|
||||
} else {
|
||||
// Game ended - keep model for UI callback, mark for removal after
|
||||
_currentModel.ShardokGameModels[oneResponse.ShardokGameId] =
|
||||
shardokGameModel;
|
||||
endedGames.Add(oneResponse.ShardokGameId);
|
||||
// Game ended - remove from active models so UI knows battle is over
|
||||
_currentModel.ShardokGameModels.TryRemove(
|
||||
oneResponse.ShardokGameId,
|
||||
out _);
|
||||
_shardokResultCounts.TryRemove(oneResponse.ShardokGameId, out _);
|
||||
}
|
||||
}
|
||||
|
||||
// Invoke UI callback BEFORE removing ended games, so the UI can
|
||||
// see the final battle results (with GameStatus = Victory/Defeat)
|
||||
if (UpdateAction != null) UpdateAction.Invoke(_currentModel);
|
||||
|
||||
// Now remove ended games from active models
|
||||
foreach (var endedGameId in endedGames) {
|
||||
_currentModel.ShardokGameModels.TryRemove(endedGameId, out _);
|
||||
_shardokResultCounts.TryRemove(endedGameId, out _);
|
||||
}
|
||||
break;
|
||||
|
||||
case GameUpdate.GameUpdateDetailsOneofCase.StreamingTextResponse:
|
||||
@@ -514,22 +502,13 @@ namespace eagle {
|
||||
_connectionLogger.LogLine(
|
||||
$"[STATE_RESYNC] timestamp={timestamp} round={startingState.CurrentRoundId} factions={startingState.Factions.Count} heroes={startingState.Heroes.Count}");
|
||||
|
||||
// Clear all stale state before applying new starting state.
|
||||
// This handles rewind/reset scenarios where the server sends an earlier state.
|
||||
_currentModel.ShardokGameModels.Clear();
|
||||
_shardokResultCounts.Clear();
|
||||
_shardokNeedsResync.Clear();
|
||||
_currentModel.CommandToken = null;
|
||||
_currentModel.LastPostedToken = -1;
|
||||
_currentModel.AvailableCommandsByProvince.Clear();
|
||||
_commandSubmittedTime = null;
|
||||
|
||||
_currentModel.GsView = startingState;
|
||||
_currentModel.BattalionTypes =
|
||||
startingState.BattalionTypes.ToDictionary(bt => bt.TypeId, bt => bt);
|
||||
_currentModel.ChronicleEntries = startingState.ChronicleEntries.ToList();
|
||||
|
||||
// For any outstanding battles in the new state, create models and mark for resync.
|
||||
// For any outstanding battles not in ShardokGameModels, create models and mark for
|
||||
// resync. This ensures fresh clients get Shardok state for ongoing battles.
|
||||
bool needsResubscribe = false;
|
||||
foreach (var battle in _currentModel.ShardokBattles) {
|
||||
if (!_currentModel.ShardokGameModels.ContainsKey(battle.ShardokGameId)) {
|
||||
|
||||
+2
-2
@@ -36,10 +36,10 @@ namespace eagle.Notifications.ARNNotifications {
|
||||
$"{DisplayNames.ResolvedFactionString(lastFaction, currentModel)}'s hero {{HeroName}}";
|
||||
}
|
||||
|
||||
textTemplate = $"{executingFactionName} has executed {victimDescription}!\n\n";
|
||||
textTemplate = $"{executingFactionName} has executed {victimDescription}!";
|
||||
heroPlaceholders["HeroName"] = (hero.NameTextId, "the hero");
|
||||
} else {
|
||||
textTemplate = $"{executingFactionName} has executed {{ExecutedHero}}!\n\n";
|
||||
textTemplate = $"{executingFactionName} has executed {{ExecutedHero}}!";
|
||||
heroPlaceholders["ExecutedHero"] = (hero.NameTextId, "the prisoner");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e661753a1e43a4f2da81b76457cf55c8
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
LFS
BIN
Binary file not shown.
-117
@@ -1,117 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8d524cd6fa08d47c99b2462c2686b8d0
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 1
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 0
|
||||
wrapV: 0
|
||||
wrapW: 0
|
||||
nPOTScale: 1
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 0
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 0
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 0
|
||||
swizzle: 50462976
|
||||
cookieLightType: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 4
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
customData:
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spriteCustomMetadata:
|
||||
entries: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
LFS
BIN
Binary file not shown.
-117
@@ -1,117 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1e69ea88b87d34a48bbb45a1fbad81fc
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 1
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 0
|
||||
wrapV: 0
|
||||
wrapW: 0
|
||||
nPOTScale: 1
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 0
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 0
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 0
|
||||
swizzle: 50462976
|
||||
cookieLightType: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 4
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
customData:
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spriteCustomMetadata:
|
||||
entries: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Eagle/OAuth Buttons/web_neutral_sq_ctn@4x.png
LFS
BIN
Binary file not shown.
-117
@@ -1,117 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1850a7332b6e84e1a9b69f5f5b4cf482
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 1
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 0
|
||||
wrapV: 0
|
||||
wrapW: 0
|
||||
nPOTScale: 1
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 0
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 0
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 0
|
||||
swizzle: 50462976
|
||||
cookieLightType: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 4
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
customData:
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spriteCustomMetadata:
|
||||
entries: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+82
-247
@@ -45,17 +45,11 @@ namespace eagle {
|
||||
private readonly Logger _remoteEagleClientLogger = Logger.GetLogger("ConnectionLogger");
|
||||
private readonly Logger _timingsLogger = Logger.GetLogger("ConnectionLogger");
|
||||
|
||||
// Counter to diagnose duplicate logging - if logs show same seq# twice, two threads
|
||||
// are processing same message. If seq# increments but lines doubled, Logger issue.
|
||||
private int _logFlowSeq = 0;
|
||||
|
||||
/// <summary>Timestamped log for connection flow tracing.</summary>
|
||||
private void LogFlow(string message) {
|
||||
var seq = Interlocked.Increment(ref _logFlowSeq);
|
||||
var ts = DateTime.UtcNow.ToString("HH:mm:ss.fff");
|
||||
_remoteEagleClientLogger.LogLine($"[FLOW {ts}] #{seq} {message}");
|
||||
_remoteEagleClientLogger.LogLine($"[FLOW {ts}] {message}");
|
||||
}
|
||||
|
||||
private volatile bool _isConnecting = false;
|
||||
private Timer _idleCheckTimer = null;
|
||||
private Timer _heartbeatTimer = null;
|
||||
@@ -89,23 +83,7 @@ namespace eagle {
|
||||
_retryTimer?.Dispose();
|
||||
NextReconnectAttempt = null;
|
||||
LogConnectionEvent("force_reconnect", "User requested immediate reconnect");
|
||||
RunConnectAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fire-and-forget wrapper for Connect() that ensures exceptions are logged
|
||||
/// rather than silently swallowed by Task.Run().
|
||||
/// </summary>
|
||||
private void RunConnectAsync() {
|
||||
Task.Run(async () => {
|
||||
try {
|
||||
await Connect();
|
||||
} catch (Exception e) {
|
||||
// Log but don't rethrow - this is fire-and-forget
|
||||
Console.WriteLine($"[CONNECT] Exception in Connect: {e}");
|
||||
_remoteEagleClientLogger.LogLine($"Exception in Connect: {e}");
|
||||
}
|
||||
});
|
||||
Task.Run(() => Connect());
|
||||
}
|
||||
|
||||
private DateTime? GetDeadlineFromNow() {
|
||||
@@ -159,7 +137,7 @@ namespace eagle {
|
||||
_retryTimer = new Timer();
|
||||
_retryTimer.AutoReset = false;
|
||||
_retryTimer.Interval = backoffSeconds * 1000;
|
||||
_retryTimer.Elapsed += (sender, args) => RunConnectAsync();
|
||||
_retryTimer.Elapsed += (sender, args) => Task.Run(() => Connect());
|
||||
_retryTimer.Enabled = true;
|
||||
}
|
||||
|
||||
@@ -178,7 +156,6 @@ namespace eagle {
|
||||
private readonly Dictionary<EagleGameId, TaskCompletionSource<SubscriptionAck>>
|
||||
_pendingSubscriptionAcks = new();
|
||||
private const int SubscriptionAckTimeoutMs = 10000; // 10 seconds
|
||||
private const int WriteAsyncTimeoutMs = 10000; // 10 seconds for WriteAsync operations
|
||||
|
||||
public PersistentClientConnection(
|
||||
Eagle.EagleClient grpcClient,
|
||||
@@ -226,16 +203,12 @@ namespace eagle {
|
||||
|
||||
var shardokCount = shardokStatuses.Count();
|
||||
LogFlow($"SUBSCRIBE game={gameId} unfilteredCount={subscriber.LastUnfilteredResultCount} shardokGames={shardokCount}");
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"[SUBSCRIBE] Sending StreamGameRequest for game {gameId}, " +
|
||||
$"unfilteredCount={subscriber.LastUnfilteredResultCount}");
|
||||
|
||||
var sendSuccess = await DoWithStreamingCall(async (sc) => {
|
||||
await sc.RequestStream.WriteAsync(request);
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"[SUBSCRIBE] StreamGameRequest sent successfully for game {gameId}");
|
||||
return true;
|
||||
});
|
||||
await sc.RequestStream.WriteAsync(request).ConfigureAwait(false);
|
||||
LogFlow($"SUBSCRIBE sent for game={gameId}");
|
||||
return true;
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
if (!sendSuccess) {
|
||||
// Write failed - clean up and return
|
||||
@@ -249,7 +222,7 @@ namespace eagle {
|
||||
try {
|
||||
var ackTask = ackTcs.Task;
|
||||
var timeoutTask = Task.Delay(SubscriptionAckTimeoutMs, timeoutCts.Token);
|
||||
var completedTask = await Task.WhenAny(ackTask, timeoutTask);
|
||||
var completedTask = await Task.WhenAny(ackTask, timeoutTask).ConfigureAwait(false);
|
||||
|
||||
if (completedTask == timeoutTask) {
|
||||
// Timeout waiting for ack
|
||||
@@ -263,26 +236,15 @@ namespace eagle {
|
||||
// Cancel the timeout task since ack was received
|
||||
timeoutCts.Cancel();
|
||||
|
||||
var ack = await ackTask;
|
||||
var ack = await ackTask.ConfigureAwait(false);
|
||||
if (ack.Success) {
|
||||
LogFlow($"SUBSCRIBE_ACK game={gameId} success=true confirmedCount={ack.ConfirmedResultCount}");
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"[SUBSCRIBE] Subscription confirmed for game {gameId}, " +
|
||||
$"confirmedResultCount={ack.ConfirmedResultCount}");
|
||||
|
||||
// Note: Shardok resync flags are cleared in EagleGameModel.HandleOneGameUpdate
|
||||
// AFTER updates are actually received, not here. This ensures that if the
|
||||
// connection drops between acknowledgment and update delivery, the resync
|
||||
// will be requested again on the next reconnect.
|
||||
|
||||
return true;
|
||||
} else {
|
||||
LogFlow($"SUBSCRIBE_ACK game={gameId} success=false error={ack.ErrorMessage}");
|
||||
LogConnectionEvent(
|
||||
"subscribe_ack_failed",
|
||||
$"game={gameId}, error={ack.ErrorMessage}");
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"[SUBSCRIBE] Server rejected subscription for game {gameId}: {ack.ErrorMessage}");
|
||||
return false;
|
||||
}
|
||||
} catch (OperationCanceledException) {
|
||||
@@ -300,27 +262,29 @@ namespace eagle {
|
||||
}
|
||||
|
||||
public async Task Connect() {
|
||||
LogFlow($"Connect() called, state={_currentState}, failures={_consecutiveFailures}");
|
||||
|
||||
// Prevent concurrent connection attempts
|
||||
if (_isConnecting) {
|
||||
LogFlow($"Connect() SKIPPED already_connecting=true");
|
||||
LogFlow("Connect() skipped - already connecting");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check circuit breaker
|
||||
if (!_circuitBreaker.ShouldAttemptConnection()) {
|
||||
LogFlow($"Connect() BLOCKED circuit={_circuitBreaker.CurrentState}");
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"[CIRCUIT] Connection attempt blocked - circuit {_circuitBreaker.CurrentState}");
|
||||
LogConnectionEvent("connect_blocked", $"circuit={_circuitBreaker.CurrentState}");
|
||||
return;
|
||||
}
|
||||
|
||||
_isConnecting = true;
|
||||
try {
|
||||
LogFlow($"Connect() START failures={_consecutiveFailures}");
|
||||
_lastConnectAttempt = DateTime.UtcNow;
|
||||
_currentState = _consecutiveFailures > 0 ? ConnectionState.Reconnecting
|
||||
: ConnectionState.Connecting;
|
||||
LogFlow($"State -> {_currentState}");
|
||||
NextReconnectAttempt = null;
|
||||
LogFlow($"State -> {_currentState}");
|
||||
LogConnectionEvent("connect_attempt");
|
||||
|
||||
// Dispose existing streaming call before creating new one
|
||||
@@ -353,7 +317,7 @@ namespace eagle {
|
||||
|
||||
// Stream subscriptions OUTSIDE lock (can await)
|
||||
// Set state to SubscriptionPending while waiting for acks
|
||||
LogFlow($"Streaming {subscribersToStream.Count} subscriptions");
|
||||
LogFlow($"Stream created, {subscribersToStream.Count} subscribers to stream");
|
||||
if (subscribersToStream.Any()) {
|
||||
_currentState = ConnectionState.SubscriptionPending;
|
||||
LogFlow($"State -> {_currentState}");
|
||||
@@ -361,13 +325,16 @@ namespace eagle {
|
||||
|
||||
bool allSucceeded = true;
|
||||
foreach (var subscriber in subscribersToStream) {
|
||||
if (!await StreamOneGameAsync(subscriber)) { allSucceeded = false; }
|
||||
LogFlow($"Subscribing game {subscriber.GameId}...");
|
||||
if (!await StreamOneGameAsync(subscriber)) {
|
||||
LogFlow($"Subscription FAILED for game {subscriber.GameId}");
|
||||
allSucceeded = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!allSucceeded) {
|
||||
// Subscription failed - this is critical because the game won't receive
|
||||
// updates. Trigger reconnect rather than proceeding to Connected state.
|
||||
LogFlow("SUBSCRIBE_FAILED triggering reconnect");
|
||||
LogConnectionEvent(
|
||||
"subscribe_failed",
|
||||
"Subscription failed - triggering reconnect");
|
||||
@@ -391,7 +358,7 @@ namespace eagle {
|
||||
_lastSuccessfulConnect = DateTime.UtcNow;
|
||||
_consecutiveFailures = 0; // Reset backoff on successful connection
|
||||
_currentState = ConnectionState.Connected;
|
||||
LogFlow($"State -> {_currentState}");
|
||||
LogFlow($"State -> {_currentState} (all subscriptions succeeded)");
|
||||
_circuitBreaker.RecordSuccess();
|
||||
LogConnectionEvent("connect_success");
|
||||
|
||||
@@ -426,18 +393,6 @@ namespace eagle {
|
||||
_pendingCommands.Add(pendingCommands.Dequeue());
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up resources before reconnecting
|
||||
lock (this) {
|
||||
_streamingCall?.Dispose();
|
||||
_streamingCall = null;
|
||||
_threadCancellationTokenSource?.Cancel();
|
||||
}
|
||||
MarkAllShardokGamesForResync();
|
||||
CancelAllPendingSubscriptionAcks();
|
||||
|
||||
// Schedule reconnect - without this, the connection dies silently
|
||||
ScheduleReconnect($"ConnectFailed-{e.GetType().Name}");
|
||||
}
|
||||
} finally { _isConnecting = false; }
|
||||
}
|
||||
@@ -475,35 +430,26 @@ namespace eagle {
|
||||
lock (this) { _pendingCommands.Add(nextCommand); }
|
||||
}
|
||||
} else {
|
||||
// CurrentEagleToken is null - server hasn't sent us
|
||||
// commands yet, or we skipped them. Retry the pending
|
||||
// command anyway; if the server already processed it, it
|
||||
// will be ignored as duplicate.
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"No current token, retrying pending command with token {providedToken}");
|
||||
await PostRequest(nextCommand);
|
||||
"No matching command for token " + providedToken);
|
||||
}
|
||||
|
||||
break;
|
||||
case UniversalCommand.SealedValueOneofCase.ShardokCommand: {
|
||||
Int64 providedShardokToken =
|
||||
nextCommand.Command.ShardokCommand.ShardokToken;
|
||||
var currentShardokToken = subscriber.CurrentShardokToken(
|
||||
nextCommand.Command.ShardokCommand.GameId);
|
||||
if (currentShardokToken is Int64 shardokToken &&
|
||||
if (subscriber.CurrentShardokToken(
|
||||
nextCommand.Command.ShardokCommand.GameId)
|
||||
is Int64 shardokToken &&
|
||||
shardokToken == providedShardokToken) {
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
"Found a matching pending command with token " +
|
||||
shardokToken);
|
||||
await PostRequest(nextCommand);
|
||||
} else if (currentShardokToken == null) {
|
||||
// No current token - retry anyway
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"No current Shardok token, retrying pending command with token {providedShardokToken}");
|
||||
await PostRequest(nextCommand);
|
||||
} else {
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"Shardok token mismatch: pending={providedShardokToken} current={currentShardokToken}, dropping");
|
||||
"No matching command for token " +
|
||||
providedShardokToken);
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -513,27 +459,25 @@ namespace eagle {
|
||||
Int64 providedShardokToken =
|
||||
nextCommand.Command.ShardokPlacementCommands
|
||||
.ShardokToken;
|
||||
var currentShardokToken = subscriber.CurrentShardokToken(
|
||||
nextCommand.Command.ShardokPlacementCommands.GameId);
|
||||
if (currentShardokToken is Int64 shardokToken &&
|
||||
if (subscriber.CurrentShardokToken(
|
||||
nextCommand.Command.ShardokCommand.GameId)
|
||||
is Int64 shardokToken &&
|
||||
shardokToken == providedShardokToken) {
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
"Found a matching pending placement command with token " +
|
||||
"Found a matching pending command with token " +
|
||||
shardokToken);
|
||||
await PostRequest(nextCommand);
|
||||
} else if (currentShardokToken == null) {
|
||||
// No current token - retry anyway
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"No current Shardok token, retrying pending placement command with token {providedShardokToken}");
|
||||
await PostRequest(nextCommand);
|
||||
} else {
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"Shardok placement token mismatch: pending={providedShardokToken} current={currentShardokToken}, dropping");
|
||||
"No matching command for token " +
|
||||
providedShardokToken);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Note: PostRequest is called inside each case, not here
|
||||
// (removed duplicate PostRequest call that was causing double-posting)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -632,16 +576,18 @@ namespace eagle {
|
||||
|
||||
public async Task PostError(EagleGameId gameId, string errorMessage, string stackTrace) {
|
||||
await DoWithStreamingCall(async (streamingCall) => {
|
||||
await streamingCall.RequestStream.WriteAsync(new UpdateStreamRequest {
|
||||
ErrorRequest =
|
||||
new ErrorRequest {
|
||||
ErrorMessage = errorMessage,
|
||||
GameId = gameId,
|
||||
StackTrace = stackTrace
|
||||
}
|
||||
});
|
||||
await streamingCall.RequestStream
|
||||
.WriteAsync(new UpdateStreamRequest {
|
||||
ErrorRequest =
|
||||
new ErrorRequest {
|
||||
ErrorMessage = errorMessage,
|
||||
GameId = gameId,
|
||||
StackTrace = stackTrace
|
||||
}
|
||||
})
|
||||
.ConfigureAwait(false);
|
||||
return true;
|
||||
});
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public void Unsubscribe(IClientConnectionSubscriber subscriber) {
|
||||
@@ -652,25 +598,14 @@ namespace eagle {
|
||||
try {
|
||||
lock (this) { _pendingCommands.Add(request); }
|
||||
|
||||
var success = await DoWithStreamingCall(async (streamingCall) => {
|
||||
await streamingCall.RequestStream.WriteAsync(
|
||||
new UpdateStreamRequest { PostCommandRequest = request });
|
||||
await DoWithStreamingCall(async (streamingCall) => {
|
||||
await streamingCall.RequestStream
|
||||
.WriteAsync(new UpdateStreamRequest { PostCommandRequest = request })
|
||||
.ConfigureAwait(false);
|
||||
return true;
|
||||
});
|
||||
|
||||
// Only remove from pending if successfully sent.
|
||||
// If connection was dead, leave in queue for retry after reconnect.
|
||||
if (success) {
|
||||
lock (this) { _pendingCommands.Remove(request); }
|
||||
} else {
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"[POST] Command not sent (connection dead), keeping in pending queue for retry");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Console.WriteLine("Failed to post command: " + e);
|
||||
_remoteEagleClientLogger.LogLine($"[POST] Exception posting command: {e.Message}");
|
||||
// Leave in _pendingCommands for retry after reconnect
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
lock (this) { _pendingCommands.Remove(request); }
|
||||
} catch (Exception e) { Console.WriteLine("Failed to post command: " + e); }
|
||||
}
|
||||
|
||||
private async Task
|
||||
@@ -747,54 +682,34 @@ namespace eagle {
|
||||
|
||||
private delegate Task<bool> WithStreamingCallDelegate(
|
||||
AsyncDuplexStreamingCall<UpdateStreamRequest, UpdateStreamResponse> call);
|
||||
|
||||
/// <summary>
|
||||
/// Execute an action with the streaming call, with a timeout to prevent indefinite
|
||||
/// blocking. If the action takes longer than WriteAsyncTimeoutMs, we consider the
|
||||
/// connection dead and return false. This prevents thread pool exhaustion on dead
|
||||
/// connections.
|
||||
/// </summary>
|
||||
private async Task<bool> DoWithStreamingCall(WithStreamingCallDelegate action) {
|
||||
var sc = _streamingCall;
|
||||
if (sc == null) return false;
|
||||
|
||||
try {
|
||||
using var timeoutCts = new CancellationTokenSource(WriteAsyncTimeoutMs);
|
||||
var actionTask = action(sc);
|
||||
var timeoutTask = Task.Delay(WriteAsyncTimeoutMs, timeoutCts.Token);
|
||||
var actionTask = action(_streamingCall);
|
||||
var timeoutTask = Task.Delay(WriteTimeoutMs, _cancellationToken);
|
||||
|
||||
var completedTask =
|
||||
await Task.WhenAny(actionTask, timeoutTask).ConfigureAwait(false);
|
||||
|
||||
if (completedTask == timeoutTask) {
|
||||
// WriteAsync timed out - connection is likely 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] DoWithStreamingCall timed out - connection may be dead");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cancel the timeout task since action completed
|
||||
timeoutCts.Cancel();
|
||||
return await actionTask.ConfigureAwait(false);
|
||||
} catch (RpcException e) {
|
||||
} catch (OperationCanceledException) { return false; } catch (RpcException e) {
|
||||
if (e.StatusCode == StatusCode.Cancelled) {
|
||||
// This is expected when the connection is closed.
|
||||
return false;
|
||||
} else {
|
||||
throw;
|
||||
}
|
||||
} catch (OperationCanceledException) {
|
||||
// Timeout was triggered
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send a request on the streaming call with a timeout to prevent indefinite blocking.
|
||||
/// </summary>
|
||||
// Timeout for WriteAsync to prevent ThreadPool exhaustion from blocked writes
|
||||
private const int WriteTimeoutMs = 10000;
|
||||
|
||||
public async Task<bool> SendUpdateStreamRequestAsync(UpdateStreamRequest request) {
|
||||
AsyncDuplexStreamingCall<UpdateStreamRequest, UpdateStreamResponse> sc = null;
|
||||
lock (this) {
|
||||
@@ -803,31 +718,18 @@ namespace eagle {
|
||||
}
|
||||
|
||||
try {
|
||||
using var timeoutCts = new CancellationTokenSource(WriteAsyncTimeoutMs);
|
||||
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
|
||||
_cancellationToken,
|
||||
timeoutCts.Token);
|
||||
// Use timeout to prevent indefinite blocking on dead connections
|
||||
using var timeoutCts =
|
||||
CancellationTokenSource.CreateLinkedTokenSource(_cancellationToken);
|
||||
timeoutCts.CancelAfter(WriteTimeoutMs);
|
||||
|
||||
var writeTask = sc.RequestStream.WriteAsync(request, linkedCts.Token);
|
||||
var timeoutTask = Task.Delay(WriteAsyncTimeoutMs, timeoutCts.Token);
|
||||
|
||||
var completedTask =
|
||||
await Task.WhenAny(writeTask, timeoutTask).ConfigureAwait(false);
|
||||
|
||||
if (completedTask == timeoutTask) {
|
||||
// WriteAsync timed out - connection is likely dead
|
||||
LogConnectionEvent(
|
||||
"write_timeout",
|
||||
$"SendUpdateStreamRequestAsync timed out after {WriteAsyncTimeoutMs}ms");
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"[WRITE_TIMEOUT] SendUpdateStreamRequestAsync timed out after {WriteAsyncTimeoutMs}ms");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cancel the timeout task since write completed
|
||||
timeoutCts.Cancel();
|
||||
await writeTask.ConfigureAwait(false); // Observe any exception
|
||||
await sc.RequestStream.WriteAsync(request, timeoutCts.Token).ConfigureAwait(false);
|
||||
return true;
|
||||
} catch (OperationCanceledException) {
|
||||
// Timeout or cancellation - connection is likely dead
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
"[WRITE] WriteAsync timed out or cancelled - connection may be dead");
|
||||
return false;
|
||||
} catch (RpcException e) {
|
||||
if (e.StatusCode == StatusCode.Cancelled) {
|
||||
// This is expected when the connection is closed.
|
||||
@@ -835,9 +737,6 @@ namespace eagle {
|
||||
} else {
|
||||
throw;
|
||||
}
|
||||
} catch (OperationCanceledException) {
|
||||
// Timeout was triggered
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -850,7 +749,6 @@ namespace eagle {
|
||||
switch (gameUpdate.GameUpdateDetailsCase) {
|
||||
case GameUpdate.GameUpdateDetailsOneofCase.ErrorResponse:
|
||||
LogFlow($"UPDATE ErrorResponse game={gameUpdate.GameId}");
|
||||
_remoteEagleClientLogger.LogLine("Got an error response!");
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
lock (this) { _subscribers.Remove(gameUpdate.GameId); }
|
||||
});
|
||||
@@ -858,7 +756,6 @@ namespace eagle {
|
||||
|
||||
case GameUpdate.GameUpdateDetailsOneofCase.StreamingTextResponse:
|
||||
// Handle streaming text directly on gRPC thread - dictionary is thread-safe.
|
||||
// Listeners are notified later via ProcessPendingUpdates() on main thread.
|
||||
var str = gameUpdate.StreamingTextResponse;
|
||||
if (str != null) {
|
||||
ClientTextProvider.Provider.HandleNewStreamingText(
|
||||
@@ -867,8 +764,6 @@ namespace eagle {
|
||||
str.StartingByteCount,
|
||||
str.Completed);
|
||||
}
|
||||
// No MainQueue enqueue needed - ProcessPendingUpdates handles listener
|
||||
// notification
|
||||
break;
|
||||
|
||||
case GameUpdate.GameUpdateDetailsOneofCase.ActionResultResponse:
|
||||
@@ -881,11 +776,10 @@ namespace eagle {
|
||||
? arResp.AvailableCommands.CommandsByProvince?.Count ?? 0
|
||||
: 0;
|
||||
var status = arResp.ServerGameStatus?.Status.ToString() ?? "null";
|
||||
LogFlow($"UPDATE ActionResult game={gameUpdate.GameId} countAfter={arResp.UnfilteredResultCountAfter} " +
|
||||
$"actionResults={arCount} startingState={hasStartingState} hasCommands={hasCommands} " +
|
||||
LogFlow($"UPDATE ActionResult game={gameUpdate.GameId} actionResults={arCount} " +
|
||||
$"startingState={hasStartingState} hasCommands={hasCommands} " +
|
||||
$"token={cmdToken} cmdProvinces={cmdCount} status={status}");
|
||||
|
||||
// Update result counts IMMEDIATELY on the gRPC thread, before enqueueing.
|
||||
lock (this) {
|
||||
if (_subscribers.TryGetValue(gameUpdate.GameId, out var sub)) {
|
||||
sub.UpdateResultCounts(gameUpdate);
|
||||
@@ -901,28 +795,15 @@ namespace eagle {
|
||||
_subscribers.Remove(gameUpdate.GameId);
|
||||
}
|
||||
}
|
||||
|
||||
await TryPendingCommands();
|
||||
|
||||
var processTime = (DateTime.UtcNow - receivedTime).TotalMilliseconds;
|
||||
if (processTime > 100.0) {
|
||||
_timingsLogger.LogLine($"PROCESS {processTime} ms");
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case GameUpdate.GameUpdateDetailsOneofCase.ShardokActionResultResponse:
|
||||
var shardokResp = gameUpdate.ShardokActionResultResponse;
|
||||
var shardokGameCount = shardokResp.ShardokGameResponses?.Count ?? 0;
|
||||
// Log each Shardok game's sequence to detect missing results
|
||||
foreach (var sgr in shardokResp.ShardokGameResponses) {
|
||||
var resultCount = sgr.ActionResultViews?.Count ?? 0;
|
||||
LogFlow($"UPDATE ShardokResult game={gameUpdate.GameId} " +
|
||||
$"shardok={sgr.ShardokGameId} countAfter={sgr.NewResultViewCount} " +
|
||||
$"results={resultCount}");
|
||||
}
|
||||
var shResp = gameUpdate.ShardokActionResultResponse;
|
||||
var shGames = shResp.ShardokGameResponses?.Count ?? 0;
|
||||
LogFlow($"UPDATE ShardokResult game={gameUpdate.GameId} shardokGames={shGames}");
|
||||
|
||||
// Update result counts IMMEDIATELY on the gRPC thread, before enqueueing.
|
||||
lock (this) {
|
||||
if (_subscribers.TryGetValue(gameUpdate.GameId, out var sub2)) {
|
||||
sub2.UpdateResultCounts(gameUpdate);
|
||||
@@ -938,13 +819,7 @@ namespace eagle {
|
||||
_subscribers.Remove(gameUpdate.GameId);
|
||||
}
|
||||
}
|
||||
|
||||
await TryPendingCommands();
|
||||
|
||||
var processTime = (DateTime.UtcNow - receivedTime).TotalMilliseconds;
|
||||
if (processTime > 100.0) {
|
||||
_timingsLogger.LogLine($"PROCESS {processTime} ms");
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
@@ -1071,8 +946,6 @@ namespace eagle {
|
||||
var scNull = sc == null;
|
||||
var tokenCancelled = _currentThreadToken.IsCancellationRequested;
|
||||
LogFlow($"HandleStreamingCall ENDED normally: sc_null={scNull} token_cancelled={tokenCancelled}");
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
"How did we get here? This is not my beautiful wife!");
|
||||
} catch (RpcException e) {
|
||||
lock (this) {
|
||||
_lastDisconnect = DateTime.UtcNow;
|
||||
@@ -1137,19 +1010,6 @@ namespace eagle {
|
||||
CancelAllPendingSubscriptionAcks();
|
||||
|
||||
ScheduleReconnect("ObjectDisposed");
|
||||
} catch (Exception e) {
|
||||
// Catch-all for any unexpected exceptions. Without this, exceptions in an
|
||||
// async void method can crash the process or be silently lost, leaving the
|
||||
// connection dead with no recovery.
|
||||
_lastDisconnect = DateTime.UtcNow;
|
||||
LogConnectionEvent("disconnect", $"UnexpectedException: {e.GetType().Name}");
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"UNEXPECTED exception in HandleStreamingCall: {e}");
|
||||
|
||||
MarkAllShardokGamesForResync();
|
||||
CancelAllPendingSubscriptionAcks();
|
||||
|
||||
ScheduleReconnect($"UnexpectedException-{e.GetType().Name}");
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -1185,13 +1045,9 @@ namespace eagle {
|
||||
};
|
||||
_idleCheckTimer.Elapsed += (sender, args) => {
|
||||
try {
|
||||
// Log BEFORE callback to confirm timer is firing
|
||||
Console.WriteLine($"[IDLE_TIMER] Elapsed at {DateTime.UtcNow:HH:mm:ss.fff}");
|
||||
CheckForIdleTimeout();
|
||||
} catch (Exception e) {
|
||||
// Timer callbacks must not throw - it can stop the timer from firing again
|
||||
Console.WriteLine($"[IDLE_TIMER] Exception in callback: {e}");
|
||||
_remoteEagleClientLogger.LogLine($"Exception in idle check timer: {e}");
|
||||
LogFlow($"IDLE_TIMER_ERROR {e.GetType().Name}: {e.Message}");
|
||||
}
|
||||
};
|
||||
_idleCheckTimer.Enabled = true;
|
||||
@@ -1238,7 +1094,7 @@ namespace eagle {
|
||||
if (toCancel != null) {
|
||||
toCancel.Dispose();
|
||||
lock (this) { _streamingCall = null; }
|
||||
RunConnectAsync();
|
||||
Task.Run(() => Connect());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1249,22 +1105,15 @@ namespace eagle {
|
||||
new Timer { AutoReset = true, Interval = HeartbeatIntervalSeconds * 1000 };
|
||||
_heartbeatTimer.Elapsed += (sender, args) => {
|
||||
try {
|
||||
// Log BEFORE Task.Run to confirm timer is firing
|
||||
Console.WriteLine(
|
||||
$"[HEARTBEAT_TIMER] Elapsed at {DateTime.UtcNow:HH:mm:ss.fff}");
|
||||
Task.Run(async () => {
|
||||
try {
|
||||
await SendHeartbeat();
|
||||
await SendHeartbeat().ConfigureAwait(false);
|
||||
} catch (Exception e) {
|
||||
// Log but don't rethrow - exceptions in Task.Run are silently lost
|
||||
Console.WriteLine($"[HEARTBEAT] Exception in SendHeartbeat: {e}");
|
||||
_remoteEagleClientLogger.LogLine($"Exception in SendHeartbeat: {e}");
|
||||
LogFlow($"HEARTBEAT_ERROR {e.GetType().Name}: {e.Message}");
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
// Timer callbacks must not throw
|
||||
Console.WriteLine($"[HEARTBEAT_TIMER] Exception in callback: {e}");
|
||||
_remoteEagleClientLogger.LogLine($"Exception in heartbeat timer: {e}");
|
||||
LogFlow($"HEARTBEAT_TIMER_ERROR {e.GetType().Name}: {e.Message}");
|
||||
}
|
||||
};
|
||||
_heartbeatTimer.Enabled = true;
|
||||
@@ -1334,32 +1183,18 @@ namespace eagle {
|
||||
|
||||
private void HandleHeartbeatResponse(HeartbeatResponse response) {
|
||||
LogFlow($"HEARTBEAT_RESPONSE server_ts={response.ServerTimestamp}");
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"[HEARTBEAT] Got response, server_timestamp={response.ServerTimestamp}");
|
||||
|
||||
// Check for sync mismatches reported by server
|
||||
bool hasMismatch = false;
|
||||
foreach (var syncResult in response.GameSyncResults) {
|
||||
if (!syncResult.EagleInSync) {
|
||||
LogFlow($"SYNC_MISMATCH game={syncResult.GameId} server_count={syncResult.ServerUnfilteredResultCount}");
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"[SYNC_MISMATCH] Game {syncResult.GameId}: Eagle out of sync, " +
|
||||
$"server has {syncResult.ServerUnfilteredResultCount} results");
|
||||
LogConnectionEvent(
|
||||
"sync_mismatch_eagle",
|
||||
$"game={syncResult.GameId}, server_count={syncResult.ServerUnfilteredResultCount}");
|
||||
hasMismatch = true;
|
||||
}
|
||||
|
||||
foreach (var shardokResult in syncResult.ShardokSyncResults) {
|
||||
if (!shardokResult.InSync) {
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"[SYNC_MISMATCH] Game {syncResult.GameId}, Shardok {shardokResult.ShardokGameId}: " +
|
||||
$"out of sync, server has {shardokResult.ServerFilteredResultCount} results");
|
||||
LogConnectionEvent(
|
||||
"sync_mismatch_shardok",
|
||||
$"game={syncResult.GameId}, shardok={shardokResult.ShardokGameId}, " +
|
||||
$"server_count={shardokResult.ServerFilteredResultCount}");
|
||||
LogFlow($"SYNC_MISMATCH game={syncResult.GameId} shardok={shardokResult.ShardokGameId} server_count={shardokResult.ServerFilteredResultCount}");
|
||||
hasMismatch = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Auth;
|
||||
using Cysharp.Net.Http;
|
||||
using eagle;
|
||||
using Grpc.Core;
|
||||
@@ -58,25 +57,6 @@ public class EagleConnection : IDisposable {
|
||||
};
|
||||
}
|
||||
|
||||
private CallInvoker MakeJwtInvoker(string url) {
|
||||
var jwtInterceptor = new JwtAuthInterceptor();
|
||||
|
||||
_channel = GrpcChannel.ForAddress(
|
||||
"https://" + url,
|
||||
new GrpcChannelOptions {
|
||||
HttpHandler = CreateHttpHandler(),
|
||||
DisposeHttpClient = true,
|
||||
LoggerFactory = _loggerFactory,
|
||||
MaxReceiveMessageSize = null
|
||||
});
|
||||
|
||||
var invoker = _channel.Intercept(jwtInterceptor);
|
||||
return invoker;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create connection using Basic Auth (legacy).
|
||||
/// </summary>
|
||||
public EagleConnection(string playerName, string password, string url) {
|
||||
this.playerName = playerName;
|
||||
credentials = new Metadata { { "user", playerName } };
|
||||
@@ -88,26 +68,6 @@ public class EagleConnection : IDisposable {
|
||||
EagleGrpcClient = new Eagle.EagleClient(MakeInvoker(playerName, password, url));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create connection using JWT Bearer token auth.
|
||||
/// Tokens are read from TokenStorage on each request.
|
||||
/// </summary>
|
||||
public static EagleConnection CreateWithJwt(string url) {
|
||||
return new EagleConnection(url, useJwt: true);
|
||||
}
|
||||
|
||||
private EagleConnection(string url, bool useJwt) {
|
||||
// For JWT auth, get display name from TokenStorage
|
||||
playerName = TokenStorage.DisplayName ?? "Unknown";
|
||||
credentials = new Metadata { { "user", TokenStorage.UserId ?? "" } };
|
||||
authHeader = null; // Not used for JWT
|
||||
|
||||
_loggerFactory = LoggerFactory.Create(
|
||||
builder => { builder.AddProvider(new CustomFileLoggerProvider()); });
|
||||
|
||||
EagleGrpcClient = new Eagle.EagleClient(MakeJwtInvoker(url));
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
try {
|
||||
_channel?.Dispose();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -1,22 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using UnityEngine;
|
||||
|
||||
namespace common {
|
||||
/// <summary>
|
||||
/// Non-blocking logger that queues messages and writes them on a dedicated background thread.
|
||||
/// This ensures that file I/O issues (antivirus, cloud sync, slow disk) never block the caller.
|
||||
/// </summary>
|
||||
public sealed class Logger : IDisposable {
|
||||
private readonly StreamWriter _sw;
|
||||
private readonly ConcurrentQueue<string> _messageQueue = new ConcurrentQueue<string>();
|
||||
private readonly Thread _writerThread;
|
||||
private readonly AutoResetEvent _messageAvailable = new AutoResetEvent(false);
|
||||
private volatile bool _disposed;
|
||||
private readonly string _name;
|
||||
private readonly object _lock = new object();
|
||||
|
||||
private static Dictionary<String, Logger> _loggers = new Dictionary<String, Logger>();
|
||||
private static readonly object _loggersLock = new object();
|
||||
@@ -28,12 +18,11 @@ namespace common {
|
||||
}
|
||||
}
|
||||
|
||||
private static string CurrentTimeString() {
|
||||
private string CurrentTimeString() {
|
||||
return DateTime.UtcNow.ToString("yyyyMMdd_HHmmss.fff");
|
||||
}
|
||||
|
||||
private Logger(string name) {
|
||||
_name = name;
|
||||
var directoryPath =
|
||||
Path.Combine(Application.persistentDataPath, "eagle0", "Resources", name);
|
||||
|
||||
@@ -41,94 +30,19 @@ namespace common {
|
||||
|
||||
var logFilePath = Path.Combine(directoryPath, $"{name}_{CurrentTimeString()}.txt");
|
||||
_sw = new StreamWriter(logFilePath, append: true);
|
||||
|
||||
// Start dedicated writer thread
|
||||
_writerThread = new Thread(WriterLoop) {
|
||||
Name = $"Logger-{name}",
|
||||
IsBackground = true // Won't prevent app exit
|
||||
};
|
||||
_writerThread.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queue a log message. This method never blocks on file I/O.
|
||||
/// </summary>
|
||||
public void LogLine(string line) {
|
||||
if (_disposed) return;
|
||||
|
||||
// Format the message with timestamp immediately (captures accurate time)
|
||||
var formattedMessage = CurrentTimeString() + " " + line;
|
||||
_messageQueue.Enqueue(formattedMessage);
|
||||
_messageAvailable.Set(); // Signal the writer thread
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Background thread that processes the message queue and writes to file.
|
||||
/// File I/O blocking only affects this thread, not the callers of LogLine().
|
||||
/// </summary>
|
||||
private void WriterLoop() {
|
||||
while (!_disposed) {
|
||||
try {
|
||||
// Wait for messages with timeout so we can check _disposed periodically
|
||||
_messageAvailable.WaitOne(1000);
|
||||
|
||||
// Process all queued messages
|
||||
while (_messageQueue.TryDequeue(out var message)) {
|
||||
try {
|
||||
_sw.WriteLine(message);
|
||||
} catch (Exception ex) {
|
||||
// File write failed - log to Console as fallback
|
||||
Console.WriteLine($"[Logger-{_name}] Write failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// Flush after processing batch
|
||||
try {
|
||||
_sw.Flush();
|
||||
} catch (Exception ex) {
|
||||
Console.WriteLine($"[Logger-{_name}] Flush failed: {ex.Message}");
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
// Don't let any exception kill the writer thread
|
||||
Console.WriteLine($"[Logger-{_name}] WriterLoop error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// Drain remaining messages on shutdown
|
||||
while (_messageQueue.TryDequeue(out var message)) {
|
||||
try {
|
||||
_sw.WriteLine(message);
|
||||
} catch { /* ignore on shutdown */
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
lock (_lock) {
|
||||
_sw.WriteLine(CurrentTimeString() + " " + line);
|
||||
_sw.Flush();
|
||||
} catch { /* ignore on shutdown */
|
||||
}
|
||||
}
|
||||
|
||||
public void Close() { Dispose(); }
|
||||
public void Close() { _sw.Close(); }
|
||||
|
||||
public void Dispose() {
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
|
||||
// Signal writer thread to exit and wait for it
|
||||
_messageAvailable.Set();
|
||||
if (_writerThread != null && _writerThread.IsAlive) {
|
||||
_writerThread.Join(5000); // Wait up to 5 seconds for clean shutdown
|
||||
}
|
||||
|
||||
try {
|
||||
_sw?.Dispose();
|
||||
} catch { /* ignore */
|
||||
}
|
||||
|
||||
try {
|
||||
_messageAvailable?.Dispose();
|
||||
} catch { /* ignore */
|
||||
}
|
||||
if (_sw != null) { _sw.Dispose(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Cysharp.Net.Http;
|
||||
using eagle;
|
||||
using Google.Protobuf;
|
||||
using Net.Eagle0.Eagle.Api;
|
||||
@@ -46,10 +45,6 @@ namespace common {
|
||||
|
||||
private readonly HashSet<string> _failedPaths = new();
|
||||
private readonly object _failedPathsLock = new();
|
||||
|
||||
// Track paths currently being fetched to avoid duplicate requests
|
||||
private readonly HashSet<string> _inFlightPaths = new();
|
||||
private readonly object _inFlightLock = new();
|
||||
private Timer _periodicRetryTimer;
|
||||
|
||||
ResourceFetcher(string relativeLocalPath, HttpClient httpClient) {
|
||||
@@ -60,17 +55,9 @@ namespace common {
|
||||
relativeLocalPath);
|
||||
Directory.CreateDirectory(_localPath);
|
||||
|
||||
// Use HTTP/2 handler for better connection reuse and multiplexing.
|
||||
// YetAnotherHttpHandler doesn't follow redirects automatically,
|
||||
// which is what we want so we can retry each hop independently.
|
||||
var handler = new YetAnotherHttpHandler {
|
||||
Http2Only = true,
|
||||
// Keep connections alive to improve reliability on poor networks
|
||||
Http2KeepAliveInterval = TimeSpan.FromSeconds(15),
|
||||
Http2KeepAliveTimeout = TimeSpan.FromSeconds(5),
|
||||
Http2KeepAliveWhileIdle = true,
|
||||
ConnectTimeout = TimeSpan.FromSeconds(RequestTimeoutSeconds)
|
||||
};
|
||||
// Create a client that doesn't follow redirects automatically,
|
||||
// so we can retry each hop independently
|
||||
var handler = new HttpClientHandler { AllowAutoRedirect = false };
|
||||
_noRedirectClient = new HttpClient(
|
||||
handler) { Timeout = TimeSpan.FromSeconds(RequestTimeoutSeconds) };
|
||||
// Store auth header to add per-request only for eagle0.net
|
||||
@@ -116,45 +103,32 @@ namespace common {
|
||||
}
|
||||
|
||||
private async Task<bool> FetchRemote(string path) {
|
||||
// Check if this path is already being fetched
|
||||
lock (_inFlightLock) {
|
||||
if (_inFlightPaths.Contains(path)) {
|
||||
// Already being fetched; the existing fetch will handle _imageLoadQueue
|
||||
return false;
|
||||
}
|
||||
_inFlightPaths.Add(path);
|
||||
}
|
||||
Uri currentUri = new Uri(BaseUri, path);
|
||||
|
||||
try {
|
||||
Uri currentUri = new Uri(BaseUri, path);
|
||||
for (int hop = 0; hop < MaxRedirectHops; hop++) {
|
||||
var result = await FetchOneHopWithRetry(currentUri, path, hop);
|
||||
|
||||
for (int hop = 0; hop < MaxRedirectHops; hop++) {
|
||||
var result = await FetchOneHopWithRetry(currentUri, path, hop);
|
||||
|
||||
if (result.GotContent) {
|
||||
// Successfully received and saved content
|
||||
ClearFailed(path);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (result.RedirectUri != null) {
|
||||
// Follow the redirect
|
||||
currentUri = result.RedirectUri;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Failed and not a redirect
|
||||
MarkFailed(path);
|
||||
return false;
|
||||
if (result.GotContent) {
|
||||
// Successfully received and saved content
|
||||
ClearFailed(path);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Too many redirects
|
||||
Debug.Log($"ResourceFetcher: Too many redirects for {path}");
|
||||
if (result.RedirectUri != null) {
|
||||
// Follow the redirect
|
||||
currentUri = result.RedirectUri;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Failed and not a redirect
|
||||
MarkFailed(path);
|
||||
return false;
|
||||
} finally {
|
||||
lock (_inFlightLock) { _inFlightPaths.Remove(path); }
|
||||
}
|
||||
|
||||
// Too many redirects
|
||||
Debug.Log($"ResourceFetcher: Too many redirects for {path}");
|
||||
MarkFailed(path);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fetch a single hop with retries. Returns content, redirect, or failure.
|
||||
@@ -207,9 +181,9 @@ namespace common {
|
||||
if (attempt < MaxRetryAttempts - 1) {
|
||||
await Task.Delay(RetryDelaysMs[attempt]);
|
||||
}
|
||||
} catch (OperationCanceledException e) {
|
||||
} catch (TaskCanceledException e) {
|
||||
Debug.Log(
|
||||
$"ResourceFetcher: hop {hopIndex} canceled for {path} (attempt {attempt + 1}/{MaxRetryAttempts}): {e.Message}");
|
||||
$"ResourceFetcher: hop {hopIndex} timeout for {path} (attempt {attempt + 1}/{MaxRetryAttempts}): {e.Message}");
|
||||
if (attempt < MaxRetryAttempts - 1) {
|
||||
await Task.Delay(RetryDelaysMs[attempt]);
|
||||
}
|
||||
@@ -317,13 +291,10 @@ namespace common {
|
||||
public void Prefetch(IEnumerable<string> paths) {
|
||||
List<string> toFetch;
|
||||
lock (_failedPathsLock) {
|
||||
lock (_inFlightLock) {
|
||||
toFetch = paths.Where(path => !string.IsNullOrEmpty(path) &&
|
||||
!File.Exists(FullLocalPath(path)) &&
|
||||
!_failedPaths.Contains(path) &&
|
||||
!_inFlightPaths.Contains(path))
|
||||
.ToList();
|
||||
}
|
||||
toFetch = paths.Where(path => !string.IsNullOrEmpty(path) &&
|
||||
!File.Exists(FullLocalPath(path)) &&
|
||||
!_failedPaths.Contains(path))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
foreach (var path in toFetch) { FetchRemote(path); }
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"depth": 0,
|
||||
"source": "git",
|
||||
"dependencies": {},
|
||||
"hash": "5719afbc6b1d8cfa8729f24f0fe6a0e9dfa70f3a"
|
||||
"hash": ""
|
||||
},
|
||||
"com.unity.2d.sprite": {
|
||||
"version": "1.0.0",
|
||||
|
||||
@@ -253,8 +253,5 @@
|
||||
<Protobuf Include="..\..\..\..\..\..\..\..\..\src\main\protobuf\net\eagle0\eagle\api\command\util\expanded_combat_unit.proto" ProtoRoot="..\..\..\..\..\..\..\..\..\">
|
||||
<Link>src\main\protobuf\net\eagle0\eagle\api\command\util\expanded_combat_unit.proto</Link>
|
||||
</Protobuf>
|
||||
<Protobuf Include="..\..\..\..\..\..\..\..\..\src\main\protobuf\net\eagle0\eagle\api\auth.proto" ProtoRoot="..\..\..\..\..\..\..\..\..\" GrpcServices="Client">
|
||||
<Link>src\main\protobuf\net\eagle0\eagle\api\auth.proto</Link>
|
||||
</Protobuf>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -3,10 +3,6 @@ load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
|
||||
go_library(
|
||||
name = "admin_server_lib",
|
||||
srcs = ["admin_server.go"],
|
||||
embedsrcs = glob([
|
||||
"static/*",
|
||||
"templates/*.html",
|
||||
]),
|
||||
importpath = "github.com/nolen777/eagle0/src/main/go/net/eagle0/admin_server",
|
||||
visibility = ["//visibility:private"],
|
||||
deps = [
|
||||
@@ -21,10 +17,6 @@ go_binary(
|
||||
embed = [":admin_server_lib"],
|
||||
pure = "on",
|
||||
visibility = ["//visibility:public"],
|
||||
x_defs = {
|
||||
"main.buildTime": "{BUILD_TIMESTAMP}",
|
||||
"main.gitCommit": "{STABLE_GIT_COMMIT}",
|
||||
},
|
||||
)
|
||||
|
||||
# Linux x86_64 target for Docker deployment
|
||||
@@ -35,8 +27,4 @@ go_binary(
|
||||
goos = "linux",
|
||||
pure = "on",
|
||||
visibility = ["//visibility:public"],
|
||||
x_defs = {
|
||||
"main.buildTime": "{BUILD_TIMESTAMP}",
|
||||
"main.gitCommit": "{STABLE_GIT_COMMIT}",
|
||||
},
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,565 +0,0 @@
|
||||
/* Admin Server Styles - supplements Pico CSS */
|
||||
|
||||
:root {
|
||||
--pico-font-size: 16px;
|
||||
}
|
||||
|
||||
/* Navigation */
|
||||
nav {
|
||||
background: var(--pico-card-background-color);
|
||||
padding: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
border-bottom: 1px solid var(--pico-muted-border-color);
|
||||
}
|
||||
|
||||
nav ul {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
nav .brand {
|
||||
font-weight: bold;
|
||||
font-size: 1.25rem;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
/* Game cards */
|
||||
.game-card {
|
||||
background: var(--pico-card-background-color);
|
||||
border: 1px solid var(--pico-muted-border-color);
|
||||
border-radius: var(--pico-border-radius);
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.game-card h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.game-card .meta {
|
||||
color: var(--pico-muted-color);
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.game-card .players {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.game-card .actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Player badges */
|
||||
.player-badge {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: var(--pico-border-radius);
|
||||
font-size: 0.875rem;
|
||||
margin-right: 0.5rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.player-badge.human {
|
||||
background: var(--pico-primary-background);
|
||||
color: var(--pico-primary-inverse);
|
||||
}
|
||||
|
||||
.player-badge.ai {
|
||||
background: var(--pico-secondary-background);
|
||||
color: var(--pico-secondary-inverse);
|
||||
}
|
||||
|
||||
/* Status badges */
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: var(--pico-border-radius);
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.status-badge.running {
|
||||
background: #22c55e;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-badge.finished {
|
||||
background: var(--pico-muted-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* History table */
|
||||
.history-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.history-table th,
|
||||
.history-table td {
|
||||
text-align: left;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.history-table tr:hover {
|
||||
background: var(--pico-card-background-color);
|
||||
}
|
||||
|
||||
.action-type {
|
||||
font-family: monospace;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Loading indicator */
|
||||
.htmx-indicator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.htmx-request .htmx-indicator {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.htmx-request.htmx-indicator {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
color: var(--pico-muted-color);
|
||||
}
|
||||
|
||||
/* Page header */
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Breadcrumbs */
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--pico-muted-color);
|
||||
}
|
||||
|
||||
.breadcrumb a {
|
||||
color: var(--pico-muted-color);
|
||||
}
|
||||
|
||||
.breadcrumb .separator {
|
||||
color: var(--pico-muted-color);
|
||||
}
|
||||
|
||||
/* Clickable action rows */
|
||||
.action-row {
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.action-row:hover {
|
||||
background: var(--pico-primary-background) !important;
|
||||
color: var(--pico-primary-inverse);
|
||||
}
|
||||
|
||||
/* Action detail panel */
|
||||
.action-detail-row {
|
||||
background: var(--pico-card-background-color);
|
||||
}
|
||||
|
||||
.action-detail {
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--pico-muted-border-color);
|
||||
border-radius: var(--pico-border-radius);
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.action-detail-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.75rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid var(--pico-muted-border-color);
|
||||
}
|
||||
|
||||
.action-detail .close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
padding: 0 0.5rem;
|
||||
color: var(--pico-muted-color);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.action-detail .close-btn:hover {
|
||||
color: var(--pico-del-color);
|
||||
}
|
||||
|
||||
.action-json {
|
||||
background: var(--pico-code-background-color);
|
||||
padding: 1rem;
|
||||
border-radius: var(--pico-border-radius);
|
||||
overflow-x: auto;
|
||||
font-size: 0.875rem;
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
/* Settings page */
|
||||
.settings-search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.settings-search input {
|
||||
flex: 1;
|
||||
max-width: 400px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.settings-count {
|
||||
color: var(--pico-muted-color);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.settings-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.settings-table th,
|
||||
.settings-table td {
|
||||
text-align: left;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-bottom: 1px solid var(--pico-muted-border-color);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.settings-table th {
|
||||
background: var(--pico-card-background-color);
|
||||
font-weight: 600;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.setting-name {
|
||||
font-family: monospace;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.setting-type {
|
||||
font-size: 0.7rem;
|
||||
color: var(--pico-muted-color);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.setting-value form {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
margin: 0;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.setting-input {
|
||||
width: 100px;
|
||||
padding: 2px 6px !important;
|
||||
margin: 0 !important;
|
||||
font-size: 0.75rem;
|
||||
height: 22px !important;
|
||||
line-height: 1 !important;
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.setting-default {
|
||||
font-size: 0.8rem;
|
||||
color: var(--pico-muted-color);
|
||||
}
|
||||
|
||||
.setting-row.modified {
|
||||
background: rgba(255, 193, 7, 0.1);
|
||||
}
|
||||
|
||||
.setting-row.modified .setting-name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
padding: 2px 8px !important;
|
||||
font-size: 0.7rem;
|
||||
margin: 0 !important;
|
||||
height: 22px !important;
|
||||
line-height: 1 !important;
|
||||
}
|
||||
|
||||
.actions-cell {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.actions-cell button {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.settings-note {
|
||||
margin-top: 2rem;
|
||||
padding: 1rem;
|
||||
background: var(--pico-card-background-color);
|
||||
border-radius: var(--pico-border-radius);
|
||||
font-size: 0.875rem;
|
||||
color: var(--pico-muted-color);
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--pico-del-color);
|
||||
}
|
||||
|
||||
.success {
|
||||
color: var(--pico-ins-color);
|
||||
}
|
||||
|
||||
/* Players table */
|
||||
.players-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.players-table th,
|
||||
.players-table td {
|
||||
text-align: left;
|
||||
padding: 0.75rem;
|
||||
border-bottom: 1px solid var(--pico-muted-border-color);
|
||||
}
|
||||
|
||||
.players-table th {
|
||||
background: var(--pico-card-background-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.player-type {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: var(--pico-border-radius);
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.player-type.human {
|
||||
background: var(--pico-primary-background);
|
||||
color: var(--pico-primary-inverse);
|
||||
}
|
||||
|
||||
.player-type.ai {
|
||||
background: var(--pico-secondary-background);
|
||||
color: var(--pico-secondary-inverse);
|
||||
}
|
||||
|
||||
/* Button variants */
|
||||
.btn-primary {
|
||||
background: var(--pico-primary-background);
|
||||
color: var(--pico-primary-inverse);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--pico-secondary-background);
|
||||
color: var(--pico-secondary-inverse);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: var(--pico-del-color);
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #c0392b;
|
||||
}
|
||||
|
||||
/* Modal dialogs */
|
||||
dialog {
|
||||
border: 1px solid var(--pico-muted-border-color);
|
||||
border-radius: var(--pico-border-radius);
|
||||
padding: 1.5rem;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
dialog::backdrop {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
dialog h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
dialog .form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
dialog .form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
dialog .form-group input {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
dialog .modal-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
justify-content: flex-end;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
dialog .modal-actions button {
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
/* Rewind button and cell */
|
||||
.rewind-cell {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn-rewind {
|
||||
padding: 2px 8px !important;
|
||||
font-size: 0.7rem;
|
||||
margin: 0 !important;
|
||||
height: 22px !important;
|
||||
line-height: 1 !important;
|
||||
background: var(--pico-del-color);
|
||||
border-color: var(--pico-del-color);
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-rewind:hover {
|
||||
background: #c82333;
|
||||
border-color: #c82333;
|
||||
}
|
||||
|
||||
/* Alert messages */
|
||||
.alert {
|
||||
padding: 1rem;
|
||||
border-radius: var(--pico-border-radius);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.alert-success,
|
||||
.alert.success {
|
||||
background: rgba(34, 197, 94, 0.2);
|
||||
color: #16a34a;
|
||||
border: 1px solid #22c55e;
|
||||
}
|
||||
|
||||
.alert-error,
|
||||
.alert.error {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
color: #dc2626;
|
||||
border: 1px solid #ef4444;
|
||||
}
|
||||
|
||||
.alert a {
|
||||
color: inherit;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Table action buttons */
|
||||
.players-table .actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* JFR controls in nav */
|
||||
#jfr-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.jfr-status {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: var(--pico-border-radius);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.jfr-on {
|
||||
background: #22c55e;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.jfr-off {
|
||||
background: var(--pico-muted-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.jfr-loading {
|
||||
color: var(--pico-muted-color);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.jfr-error {
|
||||
color: var(--pico-del-color);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
#jfr-controls .btn-small {
|
||||
padding: 0.25rem 0.5rem !important;
|
||||
font-size: 0.75rem;
|
||||
height: auto !important;
|
||||
line-height: 1.2 !important;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#jfr-controls button.btn-small {
|
||||
background: var(--pico-primary-background);
|
||||
color: var(--pico-primary-inverse);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#jfr-controls button.btn-small.secondary {
|
||||
background: var(--pico-secondary-background);
|
||||
color: var(--pico-secondary-inverse);
|
||||
}
|
||||
|
||||
#jfr-controls a.btn-small {
|
||||
background: var(--pico-primary-background);
|
||||
color: var(--pico-primary-inverse);
|
||||
border-radius: var(--pico-border-radius);
|
||||
}
|
||||
|
||||
/* Result div */
|
||||
.rewind-result:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Build info in header */
|
||||
.build-info {
|
||||
font-size: 0.7rem;
|
||||
font-weight: normal;
|
||||
color: var(--pico-muted-color);
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
<tr class="action-detail-row">
|
||||
<td colspan="6">
|
||||
<div class="action-detail">
|
||||
<div class="action-detail-header">
|
||||
<strong>Action #{{.Index}}</strong> - {{.ActionType}} (Round {{.RoundId}})
|
||||
<button class="close-btn" onclick="this.closest('tr').remove()">×</button>
|
||||
</div>
|
||||
<pre class="action-json">{{.ActionJSON}}</pre>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -1,191 +0,0 @@
|
||||
{{define "content"}}
|
||||
<nav class="breadcrumb">
|
||||
<a href="/">Games</a>
|
||||
<span class="separator">/</span>
|
||||
<span>{{.Game.GameID}}</span>
|
||||
</nav>
|
||||
|
||||
<div class="page-header">
|
||||
<h1>Game {{.Game.GameID}}</h1>
|
||||
<span class="status-badge {{if eq .Game.RunStatus "Running"}}running{{else}}finished{{end}}">
|
||||
{{.Game.RunStatus}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<article>
|
||||
<header>
|
||||
<strong>Round {{.Game.CurrentRound}}</strong> • {{.Game.ActionCount}} total actions
|
||||
</header>
|
||||
</article>
|
||||
|
||||
<h2>Players</h2>
|
||||
<div id="player-management-result"></div>
|
||||
<table class="players-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Faction ID</th>
|
||||
<th>Faction Name</th>
|
||||
<th>Leader</th>
|
||||
<th>Type</th>
|
||||
<th>User</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Game.Players}}
|
||||
<tr>
|
||||
<td>{{.FactionID}}</td>
|
||||
<td>{{.FactionName}}</td>
|
||||
<td>{{.LeaderName}}</td>
|
||||
<td>
|
||||
<span class="player-type {{if .IsHuman}}human{{else}}ai{{end}}">
|
||||
{{if .IsHuman}}Human{{else}}AI{{end}}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{if .IsHuman}}{{.UserName}}{{else}}-{{end}}</td>
|
||||
<td class="actions">
|
||||
{{if .IsHuman}}
|
||||
<button class="btn-small btn-danger"
|
||||
onclick="confirmDropToAi('{{$.Game.GameID}}', {{.FactionID}}, '{{.UserName}}')">
|
||||
Drop to AI
|
||||
</button>
|
||||
<button class="btn-small"
|
||||
onclick="showReassignModal('{{$.Game.GameID}}', {{.FactionID}}, '{{.UserName}}')">
|
||||
Reassign
|
||||
</button>
|
||||
{{else}}
|
||||
<button class="btn-small btn-primary"
|
||||
onclick="showAssignUserModal('{{$.Game.GameID}}', {{.FactionID}})">
|
||||
Assign User
|
||||
</button>
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Modal for Assign User -->
|
||||
<dialog id="assign-user-modal">
|
||||
<form method="dialog">
|
||||
<h3>Assign User to AI Faction</h3>
|
||||
<div class="form-group">
|
||||
<label for="assign-username">Username:</label>
|
||||
<input type="text" id="assign-username" name="username" required>
|
||||
</div>
|
||||
<input type="hidden" id="assign-faction-id" name="faction_id">
|
||||
<input type="hidden" id="assign-game-id" name="game_id">
|
||||
<div class="modal-actions">
|
||||
<button type="button" class="btn-secondary" onclick="document.getElementById('assign-user-modal').close()">Cancel</button>
|
||||
<button type="button" class="btn-primary" onclick="submitAssignUser()">Assign</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<!-- Modal for Reassign -->
|
||||
<dialog id="reassign-modal">
|
||||
<form method="dialog">
|
||||
<h3>Reassign Faction</h3>
|
||||
<p id="reassign-current-user"></p>
|
||||
<div class="form-group">
|
||||
<label for="reassign-username">New Username:</label>
|
||||
<input type="text" id="reassign-username" name="username" required>
|
||||
</div>
|
||||
<input type="hidden" id="reassign-faction-id" name="faction_id">
|
||||
<input type="hidden" id="reassign-game-id" name="game_id">
|
||||
<div class="modal-actions">
|
||||
<button type="button" class="btn-secondary" onclick="document.getElementById('reassign-modal').close()">Cancel</button>
|
||||
<button type="button" class="btn-primary" onclick="submitReassign()">Reassign</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<script>
|
||||
function showAssignUserModal(gameId, factionId) {
|
||||
document.getElementById('assign-game-id').value = gameId;
|
||||
document.getElementById('assign-faction-id').value = factionId;
|
||||
document.getElementById('assign-username').value = '';
|
||||
document.getElementById('assign-user-modal').showModal();
|
||||
}
|
||||
|
||||
function submitAssignUser() {
|
||||
const gameId = document.getElementById('assign-game-id').value;
|
||||
const factionId = document.getElementById('assign-faction-id').value;
|
||||
const username = document.getElementById('assign-username').value;
|
||||
|
||||
if (!username.trim()) {
|
||||
alert('Username is required');
|
||||
return;
|
||||
}
|
||||
|
||||
htmx.ajax('POST', '/games/' + gameId + '/player-management/ai-to-human', {
|
||||
values: {faction_id: factionId, username: username},
|
||||
target: '#player-management-result',
|
||||
swap: 'innerHTML'
|
||||
});
|
||||
|
||||
document.getElementById('assign-user-modal').close();
|
||||
}
|
||||
|
||||
function confirmDropToAi(gameId, factionId, username) {
|
||||
if (confirm('Are you sure you want to convert "' + username + '" to AI control?')) {
|
||||
htmx.ajax('POST', '/games/' + gameId + '/player-management/human-to-ai', {
|
||||
values: {faction_id: factionId},
|
||||
target: '#player-management-result',
|
||||
swap: 'innerHTML'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function showReassignModal(gameId, factionId, currentUsername) {
|
||||
document.getElementById('reassign-game-id').value = gameId;
|
||||
document.getElementById('reassign-faction-id').value = factionId;
|
||||
document.getElementById('reassign-current-user').textContent = 'Currently assigned to: ' + currentUsername;
|
||||
document.getElementById('reassign-username').value = '';
|
||||
document.getElementById('reassign-modal').showModal();
|
||||
}
|
||||
|
||||
function submitReassign() {
|
||||
const gameId = document.getElementById('reassign-game-id').value;
|
||||
const factionId = document.getElementById('reassign-faction-id').value;
|
||||
const username = document.getElementById('reassign-username').value;
|
||||
|
||||
if (!username.trim()) {
|
||||
alert('Username is required');
|
||||
return;
|
||||
}
|
||||
|
||||
htmx.ajax('POST', '/games/' + gameId + '/player-management/reassign', {
|
||||
values: {faction_id: factionId, username: username},
|
||||
target: '#player-management-result',
|
||||
swap: 'innerHTML'
|
||||
});
|
||||
|
||||
document.getElementById('reassign-modal').close();
|
||||
}
|
||||
</script>
|
||||
|
||||
<h2>Action History</h2>
|
||||
<div id="rewind-result" class="rewind-result"></div>
|
||||
|
||||
<table class="history-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 60px">#</th>
|
||||
<th>Action Type</th>
|
||||
<th>Faction</th>
|
||||
<th style="width: 120px">Date</th>
|
||||
<th style="width: 80px">Round</th>
|
||||
<th style="width: 80px">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="history-body"
|
||||
hx-get="/games/{{.Game.GameID}}/history?start=0&limit=50"
|
||||
hx-trigger="load"
|
||||
hx-swap="innerHTML">
|
||||
<tr>
|
||||
<td colspan="6" class="htmx-indicator">Loading history...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{{end}}
|
||||
@@ -1,11 +0,0 @@
|
||||
<tr class="game-state-row">
|
||||
<td colspan="6">
|
||||
<div class="action-detail">
|
||||
<div class="action-detail-header">
|
||||
<strong>Game State after Action #{{.ActionIndex}}</strong>
|
||||
<button class="close-btn" onclick="this.closest('tr').remove()">×</button>
|
||||
</div>
|
||||
<pre class="action-json">{{.GameStateJSON}}</pre>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -1,37 +0,0 @@
|
||||
{{define "content"}}
|
||||
<div class="page-header">
|
||||
<h1>Running Games</h1>
|
||||
<span>{{len .Games}} game{{if ne (len .Games) 1}}s{{end}}</span>
|
||||
</div>
|
||||
|
||||
{{if .Games}}
|
||||
{{range .Games}}
|
||||
<article class="game-card">
|
||||
<h3>
|
||||
<a href="/games/{{.GameID}}">Game {{.GameID}}</a>
|
||||
<span class="status-badge {{if eq .RunStatus "Running"}}running{{else}}finished{{end}}">
|
||||
{{.RunStatus}}
|
||||
</span>
|
||||
</h3>
|
||||
<div class="meta">
|
||||
Round {{.CurrentRound}} • {{.ActionCount}} actions
|
||||
</div>
|
||||
<div class="players">
|
||||
{{range .Players}}
|
||||
<span class="player-badge {{if .IsHuman}}human{{else}}ai{{end}}">
|
||||
{{.FactionName}}{{if .IsHuman}} ({{.UserName}}){{end}}
|
||||
</span>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="actions">
|
||||
<a href="/games/{{.GameID}}" role="button" class="outline">View History</a>
|
||||
</div>
|
||||
</article>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<div class="empty-state">
|
||||
<p>No games currently running.</p>
|
||||
<p>Start a game from the Unity client to see it here.</p>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
@@ -1,58 +0,0 @@
|
||||
{{range .Entries}}
|
||||
<tr class="action-row" onclick="toggleActionDetail(this)">
|
||||
<td>{{.Index}}</td>
|
||||
<td class="action-type">{{.ActionType}}</td>
|
||||
<td>{{.FactionName}}</td>
|
||||
<td>{{.GameDate}}</td>
|
||||
<td>{{.RoundId}}</td>
|
||||
<td class="actions-cell">
|
||||
<button class="btn-small"
|
||||
hx-get="/games/{{$.GameID}}/state/{{.Index}}"
|
||||
hx-target="closest tr"
|
||||
hx-swap="afterend"
|
||||
onclick="event.stopPropagation()">
|
||||
State
|
||||
</button>
|
||||
<button class="btn-rewind"
|
||||
hx-post="/games/{{$.GameID}}/rewind"
|
||||
hx-vals='{"targetAction": "{{.Index}}"}'
|
||||
hx-confirm="Rewind game to action {{.Index}}? This will disconnect all clients and cannot be undone."
|
||||
hx-target="#rewind-result"
|
||||
hx-swap="innerHTML"
|
||||
onclick="event.stopPropagation()">
|
||||
Rewind
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="action-detail-row" style="display: none;">
|
||||
<td colspan="6">
|
||||
<div class="action-detail">
|
||||
<div class="action-detail-header">
|
||||
<strong>Action #{{.Index}}</strong> - {{.ActionType}} (Round {{.RoundId}})
|
||||
<button class="close-btn" onclick="event.stopPropagation(); this.closest('tr').style.display='none'">×</button>
|
||||
</div>
|
||||
<pre class="action-json">{{.ActionJSON}}</pre>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
{{if .HasMore}}
|
||||
<tr hx-get="/games/{{.GameID}}/history?start={{.NextStart}}&limit=50"
|
||||
hx-trigger="revealed"
|
||||
hx-swap="outerHTML">
|
||||
<td colspan="6" class="htmx-indicator">Loading more...</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
{{if and (not .Entries) (not .HasMore)}}
|
||||
<tr>
|
||||
<td colspan="6" class="empty-state">No actions recorded yet.</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
<script>
|
||||
function toggleActionDetail(row) {
|
||||
const detailRow = row.nextElementSibling;
|
||||
if (detailRow && detailRow.classList.contains('action-detail-row')) {
|
||||
detailRow.style.display = detailRow.style.display === 'none' ? '' : 'none';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,67 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{.Title}} - Eagle Admin</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
<ul>
|
||||
<li class="brand">Eagle Admin <span class="build-info">{{if .BuildInfo.GitCommit}}({{.BuildInfo.GitCommit}}{{if .BuildInfo.BuildTime}}, {{.BuildInfo.BuildTime}}{{end}}){{end}}</span></li>
|
||||
<li><a href="/">Games</a></li>
|
||||
<li><a href="/settings">Settings</a></li>
|
||||
<li><a href="/health">Health</a></li>
|
||||
<li id="jfr-controls" hx-get="/jfr/status" hx-trigger="load" hx-swap="innerHTML">
|
||||
<span class="jfr-loading">JFR: ...</span>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<main class="container">
|
||||
{{template "content" .}}
|
||||
</main>
|
||||
<script>
|
||||
// Handle JFR status response and update controls
|
||||
document.body.addEventListener('htmx:afterRequest', function(evt) {
|
||||
if (evt.detail.pathInfo.requestPath === '/jfr/status') {
|
||||
try {
|
||||
const data = JSON.parse(evt.detail.xhr.responseText);
|
||||
updateJfrControls(data.recording);
|
||||
} catch(e) {
|
||||
document.getElementById('jfr-controls').innerHTML =
|
||||
'<span class="jfr-error">JFR: Error</span>';
|
||||
}
|
||||
} else if (evt.detail.pathInfo.requestPath === '/jfr/start' ||
|
||||
evt.detail.pathInfo.requestPath === '/jfr/stop') {
|
||||
try {
|
||||
const data = JSON.parse(evt.detail.xhr.responseText);
|
||||
updateJfrControls(data.recording);
|
||||
} catch(e) {
|
||||
// Refresh status on error
|
||||
htmx.ajax('GET', '/jfr/status', '#jfr-controls');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function updateJfrControls(isRecording) {
|
||||
const container = document.getElementById('jfr-controls');
|
||||
if (isRecording) {
|
||||
container.innerHTML = `
|
||||
<span class="jfr-status jfr-on">JFR: ON</span>
|
||||
<button hx-post="/jfr/stop" hx-swap="none" class="btn-small secondary">Stop</button>
|
||||
<a href="/jfr/download" class="btn-small">Download</a>
|
||||
`;
|
||||
} else {
|
||||
container.innerHTML = `
|
||||
<span class="jfr-status jfr-off">JFR: OFF</span>
|
||||
<button hx-post="/jfr/start" hx-swap="none" class="btn-small">Start</button>
|
||||
`;
|
||||
}
|
||||
htmx.process(container);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,52 +0,0 @@
|
||||
{{define "content"}}
|
||||
<h1>Settings</h1>
|
||||
|
||||
<div class="settings-search">
|
||||
<input type="text"
|
||||
id="settings-filter"
|
||||
name="filter"
|
||||
placeholder="Search settings..."
|
||||
value="{{.Filter}}"
|
||||
hx-get="/settings/search"
|
||||
hx-trigger="input changed delay:300ms, keyup[key=='Enter']"
|
||||
hx-target="#settings-body"
|
||||
hx-swap="innerHTML">
|
||||
<span class="settings-count">{{len .Settings}} settings</span>
|
||||
</div>
|
||||
|
||||
<table class="settings-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Current Value</th>
|
||||
<th>Default</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="settings-body">
|
||||
{{range .Settings}}
|
||||
<tr class="setting-row{{if .IsModified}} modified{{end}}" id="setting-{{.Name}}">
|
||||
<td class="setting-name">{{.Name}}</td>
|
||||
<td class="setting-type">{{.SettingType}}</td>
|
||||
<td class="setting-value">
|
||||
<form hx-post="/settings/update" hx-target="#setting-{{.Name}}" hx-swap="outerHTML">
|
||||
<input type="hidden" name="name" value="{{.Name}}">
|
||||
<input type="text" name="value" value="{{.CurrentValue}}" class="setting-input">
|
||||
<button type="submit" class="btn-small">Save</button>
|
||||
</form>
|
||||
</td>
|
||||
<td class="setting-default">{{.DefaultValue}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
{{if not .Settings}}
|
||||
<tr>
|
||||
<td colspan="4" class="empty-state">No settings found.</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p class="settings-note">
|
||||
<strong>Note:</strong> Settings changes are in-memory only. A server restart will reset all values to defaults.
|
||||
</p>
|
||||
{{end}}
|
||||
@@ -1,19 +0,0 @@
|
||||
{{range .Settings}}
|
||||
<tr class="setting-row{{if .IsModified}} modified{{end}}" id="setting-{{.Name}}">
|
||||
<td class="setting-name">{{.Name}}</td>
|
||||
<td class="setting-type">{{.SettingType}}</td>
|
||||
<td class="setting-value">
|
||||
<form hx-post="/settings/update" hx-target="#setting-{{.Name}}" hx-swap="outerHTML">
|
||||
<input type="hidden" name="name" value="{{.Name}}">
|
||||
<input type="text" name="value" value="{{.CurrentValue}}" class="setting-input">
|
||||
<button type="submit" class="btn-small">Save</button>
|
||||
</form>
|
||||
</td>
|
||||
<td class="setting-default">{{.DefaultValue}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
{{if not .Settings}}
|
||||
<tr>
|
||||
<td colspan="4" class="empty-state">No settings match "{{.Filter}}"</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
+10
-43
@@ -15,9 +15,8 @@ func usage() {
|
||||
}
|
||||
|
||||
type Setting struct {
|
||||
Name string
|
||||
Type string
|
||||
DefaultValue string
|
||||
Name string
|
||||
Type string
|
||||
}
|
||||
|
||||
func parseSettings(buildFile string) ([]Setting, error) {
|
||||
@@ -33,38 +32,30 @@ func parseSettings(buildFile string) ([]Setting, error) {
|
||||
// Regex patterns to extract setting info
|
||||
settingNameRegex := regexp.MustCompile(`setting_name = "([^"]+)"`)
|
||||
settingTypeRegex := regexp.MustCompile(`setting_type = "([^"]+)"`)
|
||||
settingValueRegex := regexp.MustCompile(`setting_value = "([^"]+)"`)
|
||||
|
||||
var currentName, currentType, currentValue string
|
||||
|
||||
|
||||
var currentName, currentType string
|
||||
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
|
||||
|
||||
// Look for setting_name
|
||||
if matches := settingNameRegex.FindStringSubmatch(line); matches != nil {
|
||||
currentName = matches[1]
|
||||
}
|
||||
|
||||
|
||||
// Look for setting_type
|
||||
if matches := settingTypeRegex.FindStringSubmatch(line); matches != nil {
|
||||
currentType = matches[1]
|
||||
}
|
||||
|
||||
// Look for setting_value
|
||||
if matches := settingValueRegex.FindStringSubmatch(line); matches != nil {
|
||||
currentValue = matches[1]
|
||||
}
|
||||
|
||||
|
||||
// When we hit the end of a scala_setting_library block, save the setting
|
||||
if strings.Contains(line, ")") && currentName != "" && currentType != "" {
|
||||
settings = append(settings, Setting{
|
||||
Name: currentName,
|
||||
Type: currentType,
|
||||
DefaultValue: currentValue,
|
||||
Name: currentName,
|
||||
Type: currentType,
|
||||
})
|
||||
currentName = ""
|
||||
currentType = ""
|
||||
currentValue = ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,30 +92,6 @@ func generateSettingsLoader(settings []Setting) string {
|
||||
sb.WriteString(" case _ => throw NoSuchSettingException(key)\n")
|
||||
sb.WriteString(" }\n\n")
|
||||
|
||||
// Generate the getAllSettings method that returns current values and metadata
|
||||
sb.WriteString(" case class SettingInfo(\n")
|
||||
sb.WriteString(" name: String,\n")
|
||||
sb.WriteString(" settingType: String,\n")
|
||||
sb.WriteString(" currentValue: String,\n")
|
||||
sb.WriteString(" defaultValue: String\n")
|
||||
sb.WriteString(" )\n\n")
|
||||
|
||||
sb.WriteString(" def getAllSettings: Vector[SettingInfo] = Vector(\n")
|
||||
for i, setting := range settings {
|
||||
if setting.Type == "Int" {
|
||||
sb.WriteString(fmt.Sprintf(" SettingInfo(\"%s\", \"Int\", %s.intValue.toString, \"%s\")",
|
||||
setting.Name, setting.Name, setting.DefaultValue))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf(" SettingInfo(\"%s\", \"Double\", %s.doubleValue.toString, \"%s\")",
|
||||
setting.Name, setting.Name, setting.DefaultValue))
|
||||
}
|
||||
if i < len(settings)-1 {
|
||||
sb.WriteString(",")
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
sb.WriteString(" )\n\n")
|
||||
|
||||
// Add the rest of the class
|
||||
sb.WriteString(` case class SettingLine(
|
||||
key: String,
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
|
||||
|
||||
go_library(
|
||||
name = "jfr_server_lib",
|
||||
srcs = ["jfr_server.go"],
|
||||
importpath = "github.com/nolen777/eagle0/src/main/go/net/eagle0/jfr_server",
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
go_binary(
|
||||
name = "jfr_server",
|
||||
embed = [":jfr_server_lib"],
|
||||
pure = "on",
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
# Linux x86_64 target for Docker deployment
|
||||
go_binary(
|
||||
name = "jfr_server_linux_amd64",
|
||||
embed = [":jfr_server_lib"],
|
||||
goarch = "amd64",
|
||||
goos = "linux",
|
||||
pure = "on",
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
@@ -1,237 +0,0 @@
|
||||
// Package main provides a simple HTTP server for JFR (Java Flight Recorder) control.
|
||||
// It runs as a sidecar container with shared PID namespace to access the Eagle JVM.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const recordingName = "admin"
|
||||
|
||||
var (
|
||||
port = flag.Int("port", 8081, "HTTP server port")
|
||||
targetProc = flag.String("target", "eagle_server", "Target process name to find in jcmd output")
|
||||
)
|
||||
|
||||
// StatusResponse is the JSON response for /status endpoint
|
||||
type StatusResponse struct {
|
||||
Recording bool `json:"recording"`
|
||||
Details string `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
http.HandleFunc("/status", handleStatus)
|
||||
http.HandleFunc("/start", handleStart)
|
||||
http.HandleFunc("/stop", handleStop)
|
||||
http.HandleFunc("/dump", handleDump)
|
||||
http.HandleFunc("/health", handleHealth)
|
||||
|
||||
addr := fmt.Sprintf(":%d", *port)
|
||||
log.Printf("JFR sidecar server starting on %s", addr)
|
||||
log.Fatal(http.ListenAndServe(addr, nil))
|
||||
}
|
||||
|
||||
func handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("ok"))
|
||||
}
|
||||
|
||||
func handleStatus(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("JFR status requested from %s", r.RemoteAddr)
|
||||
|
||||
pid, err := findJvmPid()
|
||||
if err != nil {
|
||||
log.Printf("Error finding JVM PID: %v", err)
|
||||
http.Error(w, fmt.Sprintf("Failed to find JVM: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Check JFR recording status
|
||||
cmd := exec.Command("jcmd", strconv.Itoa(pid), "JFR.check")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
log.Printf("jcmd JFR.check failed: %s - %v", output, err)
|
||||
http.Error(w, fmt.Sprintf("jcmd failed: %s - %v", output, err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
outputStr := string(output)
|
||||
log.Printf("JFR.check output: %s", outputStr)
|
||||
|
||||
// Check if our named recording is active
|
||||
recording := strings.Contains(outputStr, fmt.Sprintf("name=%s", recordingName)) &&
|
||||
strings.Contains(outputStr, "running")
|
||||
|
||||
resp := StatusResponse{
|
||||
Recording: recording,
|
||||
Details: outputStr,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
|
||||
func handleStart(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("JFR start requested from %s", r.RemoteAddr)
|
||||
|
||||
pid, err := findJvmPid()
|
||||
if err != nil {
|
||||
log.Printf("Error finding JVM PID: %v", err)
|
||||
http.Error(w, fmt.Sprintf("Failed to find JVM: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Start JFR recording with profile settings
|
||||
cmd := exec.Command("jcmd", strconv.Itoa(pid), "JFR.start",
|
||||
fmt.Sprintf("name=%s", recordingName),
|
||||
"settings=profile",
|
||||
"maxage=1h",
|
||||
"maxsize=100m")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
log.Printf("jcmd JFR.start failed: %s - %v", output, err)
|
||||
http.Error(w, fmt.Sprintf("jcmd failed: %s - %v", output, err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("JFR.start output: %s", output)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(StatusResponse{Recording: true, Details: string(output)})
|
||||
}
|
||||
|
||||
func handleStop(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("JFR stop requested from %s", r.RemoteAddr)
|
||||
|
||||
pid, err := findJvmPid()
|
||||
if err != nil {
|
||||
log.Printf("Error finding JVM PID: %v", err)
|
||||
http.Error(w, fmt.Sprintf("Failed to find JVM: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Stop JFR recording
|
||||
cmd := exec.Command("jcmd", strconv.Itoa(pid), "JFR.stop", fmt.Sprintf("name=%s", recordingName))
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
log.Printf("jcmd JFR.stop failed: %s - %v", output, err)
|
||||
http.Error(w, fmt.Sprintf("jcmd failed: %s - %v", output, err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("JFR.stop output: %s", output)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(StatusResponse{Recording: false, Details: string(output)})
|
||||
}
|
||||
|
||||
func handleDump(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("JFR dump requested from %s", r.RemoteAddr)
|
||||
|
||||
// Find the target JVM PID
|
||||
pid, err := findJvmPid()
|
||||
if err != nil {
|
||||
log.Printf("Error finding JVM PID: %v", err)
|
||||
http.Error(w, fmt.Sprintf("Failed to find JVM: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
log.Printf("Found target JVM at PID %d", pid)
|
||||
|
||||
// Generate filename with timestamp
|
||||
timestamp := time.Now().Format("2006-01-02T15-04-05")
|
||||
filename := fmt.Sprintf("profile-%s.jfr", timestamp)
|
||||
tempPath := "/tmp/" + filename
|
||||
|
||||
// Run jcmd to dump JFR (use our named recording)
|
||||
dumpCmd := exec.Command("jcmd", strconv.Itoa(pid), "JFR.dump",
|
||||
fmt.Sprintf("name=%s", recordingName),
|
||||
"filename="+tempPath)
|
||||
output, err := dumpCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
log.Printf("jcmd JFR.dump failed: %s - %v", output, err)
|
||||
http.Error(w, fmt.Sprintf("jcmd failed: %s - %v", output, err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
log.Printf("JFR dump output: %s", output)
|
||||
|
||||
// Clean up temp file after we're done
|
||||
defer os.Remove(tempPath)
|
||||
|
||||
// Read the file
|
||||
data, err := os.ReadFile(tempPath)
|
||||
if err != nil {
|
||||
log.Printf("Failed to read JFR file: %v", err)
|
||||
http.Error(w, fmt.Sprintf("Failed to read JFR file: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Serving JFR file: %s (%d bytes)", filename, len(data))
|
||||
|
||||
// Serve as download
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename))
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(data)))
|
||||
w.Write(data)
|
||||
}
|
||||
|
||||
// findJvmPid finds the PID of the target JVM process using jcmd -l
|
||||
func findJvmPid() (int, error) {
|
||||
cmd := exec.Command("jcmd", "-l")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("jcmd -l failed: %v", err)
|
||||
}
|
||||
|
||||
// Parse jcmd output - format is "PID MainClass" per line
|
||||
// Example: "1 net.eagle0.eagle.Main"
|
||||
scanner := bufio.NewScanner(strings.NewReader(string(output)))
|
||||
pidRegex := regexp.MustCompile(`^(\d+)\s+(.*)$`)
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
matches := pidRegex.FindStringSubmatch(line)
|
||||
if len(matches) == 3 {
|
||||
pid, _ := strconv.Atoi(matches[1])
|
||||
mainClass := matches[2]
|
||||
|
||||
// Skip jcmd itself
|
||||
if strings.Contains(mainClass, "jcmd") || mainClass == "jdk.jcmd/sun.tools.jcmd.JCmd" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Look for our target process
|
||||
if strings.Contains(mainClass, *targetProc) || strings.Contains(mainClass, "eagle") {
|
||||
return pid, nil
|
||||
}
|
||||
|
||||
// If there's only one non-jcmd JVM, use it (likely PID 1 in container)
|
||||
if pid == 1 {
|
||||
return pid, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default to PID 1 if nothing else found (common in containers with shared PID namespace)
|
||||
return 1, nil
|
||||
}
|
||||
@@ -21,12 +21,15 @@ option java_outer_classname = "EagleInterface";
|
||||
option objc_class_prefix = "E0G";
|
||||
|
||||
service ShardokInternalInterface {
|
||||
// Server-side streaming for game updates
|
||||
// Server-side streaming for game updates - replaces polling via GetGameStatus
|
||||
rpc SubscribeToGame(GameSubscriptionRequest) returns (stream GameStatusResponse) {}
|
||||
|
||||
rpc PostCommand(PostCommandRequest) returns (GameStatusResponse) {}
|
||||
rpc PostPlacementCommands(PlacementCommandsRequest) returns (GameStatusResponse) {}
|
||||
|
||||
// Deprecated: Use SubscribeToGame for streaming updates instead
|
||||
rpc GetGameStatus(GameStatusRequest) returns (GameStatusResponse) {}
|
||||
|
||||
rpc GetHexMap(HexMapRequest) returns (HexMapResponse) {}
|
||||
rpc GetHexMapNames(HexMapNamesRequest) returns (HexMapNamesResponse) {}
|
||||
}
|
||||
@@ -55,6 +58,11 @@ message PlacementCommandsRequest {
|
||||
GameSetupInfo game_setup_info = 6;
|
||||
}
|
||||
|
||||
message GameStatusRequest {
|
||||
string game_id = 1;
|
||||
GameSetupInfo game_setup_info = 2;
|
||||
}
|
||||
|
||||
message GameStatusResponse {
|
||||
string game_id = 1;
|
||||
|
||||
|
||||
@@ -13,13 +13,11 @@ option objc_class_prefix = "E0A";
|
||||
|
||||
// Authentication service for OAuth login
|
||||
service Auth {
|
||||
// Get OAuth URL to open in system browser.
|
||||
// The server handles the OAuth callback - client polls CheckOAuthStatus.
|
||||
// Get OAuth URL to open in system browser
|
||||
rpc GetOAuthUrl(GetOAuthUrlRequest) returns (GetOAuthUrlResponse) {}
|
||||
|
||||
// Poll for OAuth completion. Call this after opening the URL from GetOAuthUrl.
|
||||
// The server receives the OAuth callback and exchanges the code for tokens.
|
||||
rpc CheckOAuthStatus(CheckOAuthStatusRequest) returns (CheckOAuthStatusResponse) {}
|
||||
// Exchange authorization code for JWT tokens
|
||||
rpc ExchangeCode(ExchangeCodeRequest) returns (ExchangeCodeResponse) {}
|
||||
|
||||
// Set or update display name (requires valid JWT)
|
||||
rpc SetDisplayName(SetDisplayNameRequest) returns (SetDisplayNameResponse) {}
|
||||
@@ -44,36 +42,28 @@ enum OAuthProvider {
|
||||
// Request to initiate OAuth flow
|
||||
message GetOAuthUrlRequest {
|
||||
OAuthProvider provider = 1;
|
||||
string redirect_uri = 2; // e.g., "eagle0://auth/callback"
|
||||
}
|
||||
|
||||
message GetOAuthUrlResponse {
|
||||
string auth_url = 1; // Full OAuth URL to open in system browser
|
||||
string state = 2; // State parameter - use this to poll CheckOAuthStatus
|
||||
string state = 2; // State parameter for CSRF protection
|
||||
}
|
||||
|
||||
// Poll for OAuth completion status
|
||||
message CheckOAuthStatusRequest {
|
||||
string state = 1; // State from GetOAuthUrlResponse
|
||||
// Exchange authorization code for JWT
|
||||
message ExchangeCodeRequest {
|
||||
OAuthProvider provider = 1;
|
||||
string authorization_code = 2;
|
||||
string state = 3; // Must match state from GetOAuthUrlResponse
|
||||
string redirect_uri = 4; // Must match exactly what was used in GetOAuthUrlRequest
|
||||
}
|
||||
|
||||
message CheckOAuthStatusResponse {
|
||||
OAuthStatus status = 1;
|
||||
// Only set when status is OAUTH_STATUS_SUCCESS:
|
||||
string access_token = 2;
|
||||
string refresh_token = 3;
|
||||
int64 expires_at = 4;
|
||||
UserInfo user = 5;
|
||||
bool is_new_user = 6;
|
||||
// Only set when status is OAUTH_STATUS_FAILED:
|
||||
string error_message = 7;
|
||||
}
|
||||
|
||||
enum OAuthStatus {
|
||||
OAUTH_STATUS_UNSPECIFIED = 0;
|
||||
OAUTH_STATUS_PENDING = 1; // User hasn't completed OAuth yet
|
||||
OAUTH_STATUS_SUCCESS = 2; // OAuth complete, tokens available
|
||||
OAUTH_STATUS_FAILED = 3; // OAuth failed (user denied, etc.)
|
||||
OAUTH_STATUS_EXPIRED = 4; // State token expired (typically 10 minutes)
|
||||
message ExchangeCodeResponse {
|
||||
string access_token = 1; // JWT for API access
|
||||
string refresh_token = 2; // Long-lived token for getting new access tokens
|
||||
int64 expires_at = 3; // Unix timestamp when access_token expires
|
||||
UserInfo user = 4;
|
||||
bool is_new_user = 5; // True if user needs to set display name
|
||||
}
|
||||
|
||||
// User information returned from auth endpoints
|
||||
|
||||
@@ -32,15 +32,6 @@ service Eagle {
|
||||
// Admin endpoints
|
||||
rpc GetRunningGames(GetRunningGamesRequest) returns (GetRunningGamesResponse) {}
|
||||
rpc GetGameHistory(GetGameHistoryRequest) returns (GetGameHistoryResponse) {}
|
||||
rpc GetActionDetail(GetActionDetailRequest) returns (GetActionDetailResponse) {}
|
||||
rpc GetGameStateAtAction(GetGameStateAtActionRequest) returns (GetGameStateAtActionResponse) {}
|
||||
rpc GetSettings(GetSettingsRequest) returns (GetSettingsResponse) {}
|
||||
|
||||
// Admin player management
|
||||
rpc ConvertAiToHuman(ConvertAiToHumanRequest) returns (ConvertAiToHumanResponse) {}
|
||||
rpc ConvertHumanToAi(ConvertHumanToAiRequest) returns (ConvertHumanToAiResponse) {}
|
||||
rpc ReassignFaction(ReassignFactionRequest) returns (ReassignFactionResponse) {}
|
||||
rpc RewindGame(RewindGameRequest) returns (RewindGameResponse) {}
|
||||
}
|
||||
|
||||
message PostCommandRequest {
|
||||
@@ -84,7 +75,6 @@ message UpdateStreamRequest {
|
||||
HexMapsRequest hex_maps_request = 8;
|
||||
PostCommandRequest post_command_request = 9;
|
||||
ErrorRequest error_request = 10;
|
||||
DropGameRequest drop_game_request = 11;
|
||||
}
|
||||
|
||||
message StreamGameRequest {
|
||||
@@ -131,7 +121,6 @@ message UpdateStreamResponse {
|
||||
HexMapResponse hex_map_response = 8;
|
||||
PostCommandResponse post_command_response = 9;
|
||||
SubscriptionAck subscription_ack = 10;
|
||||
DropGameResponse drop_game_response = 11;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -423,134 +412,4 @@ message GameHistoryEntry {
|
||||
.google.protobuf.Int32Value acting_faction_id = 4;
|
||||
.google.protobuf.Int32Value province_id = 5;
|
||||
string summary = 6; // human-readable summary of the action
|
||||
string action_json = 7; // full action result as JSON
|
||||
string faction_name = 8; // name of the acting faction, if any
|
||||
string game_date = 9; // in-game date (e.g. "March 453")
|
||||
}
|
||||
|
||||
message GetActionDetailRequest {
|
||||
int64 game_id = 1;
|
||||
int32 action_index = 2;
|
||||
}
|
||||
|
||||
message GetActionDetailResponse {
|
||||
int64 game_id = 1;
|
||||
int32 action_index = 2;
|
||||
string action_type = 3;
|
||||
int32 round_id = 4;
|
||||
string action_json = 5; // Full action result as JSON
|
||||
}
|
||||
|
||||
message GetGameStateAtActionRequest {
|
||||
int64 game_id = 1;
|
||||
int32 action_index = 2; // Get state after this action (-1 for initial state)
|
||||
}
|
||||
|
||||
message GetGameStateAtActionResponse {
|
||||
int64 game_id = 1;
|
||||
int32 action_index = 2;
|
||||
string game_state_json = 3; // Full game state as JSON
|
||||
}
|
||||
|
||||
message GetSettingsRequest {
|
||||
string filter = 1; // Optional name filter (substring match)
|
||||
}
|
||||
|
||||
message GetSettingsResponse {
|
||||
repeated Setting settings = 1;
|
||||
}
|
||||
|
||||
message Setting {
|
||||
string name = 1;
|
||||
string setting_type = 2; // "Int" or "Double"
|
||||
string current_value = 3; // Current value as string
|
||||
string default_value = 4; // Default from BUILD.bazel
|
||||
}
|
||||
|
||||
message DropGameRequest {
|
||||
int64 game_id = 1;
|
||||
}
|
||||
|
||||
enum DropGameResult {
|
||||
UNKNOWN_DROP_GAME_RESULT = 0;
|
||||
SUCCESS_DROP_GAME_RESULT = 1;
|
||||
GAME_NOT_FOUND_DROP_GAME_RESULT = 2;
|
||||
USER_NOT_IN_GAME_DROP_GAME_RESULT = 3;
|
||||
GAME_ALREADY_COMPLETED_DROP_GAME_RESULT = 4;
|
||||
}
|
||||
|
||||
message DropGameResponse {
|
||||
DropGameResult result = 1;
|
||||
int64 game_id = 2;
|
||||
}
|
||||
|
||||
// Admin player management messages
|
||||
message ConvertAiToHumanRequest {
|
||||
int64 game_id = 1;
|
||||
int32 faction_id = 2;
|
||||
string new_username = 3;
|
||||
}
|
||||
|
||||
message ConvertAiToHumanResponse {
|
||||
enum Result {
|
||||
UNKNOWN = 0;
|
||||
SUCCESS = 1;
|
||||
GAME_NOT_FOUND = 2;
|
||||
FACTION_NOT_FOUND = 3;
|
||||
FACTION_ALREADY_HUMAN = 4;
|
||||
USERNAME_ALREADY_IN_USE = 5;
|
||||
INVALID_USERNAME = 6;
|
||||
}
|
||||
Result result = 1;
|
||||
string error_message = 2;
|
||||
}
|
||||
|
||||
message ConvertHumanToAiRequest {
|
||||
int64 game_id = 1;
|
||||
int32 faction_id = 2;
|
||||
}
|
||||
|
||||
message ConvertHumanToAiResponse {
|
||||
enum Result {
|
||||
UNKNOWN = 0;
|
||||
SUCCESS = 1;
|
||||
GAME_NOT_FOUND = 2;
|
||||
FACTION_NOT_FOUND = 3;
|
||||
FACTION_ALREADY_AI = 4;
|
||||
}
|
||||
Result result = 1;
|
||||
string error_message = 2;
|
||||
}
|
||||
|
||||
message ReassignFactionRequest {
|
||||
int64 game_id = 1;
|
||||
int32 faction_id = 2;
|
||||
string new_username = 3;
|
||||
}
|
||||
|
||||
message ReassignFactionResponse {
|
||||
enum Result {
|
||||
UNKNOWN = 0;
|
||||
SUCCESS = 1;
|
||||
GAME_NOT_FOUND = 2;
|
||||
FACTION_NOT_FOUND = 3;
|
||||
FACTION_IS_AI = 4;
|
||||
USERNAME_ALREADY_IN_USE = 5;
|
||||
INVALID_USERNAME = 6;
|
||||
}
|
||||
Result result = 1;
|
||||
string previous_username = 2;
|
||||
string error_message = 3;
|
||||
}
|
||||
|
||||
message RewindGameRequest {
|
||||
int64 game_id = 1;
|
||||
int32 target_action_count = 2; // Rewind to state after this many actions
|
||||
}
|
||||
|
||||
message RewindGameResponse {
|
||||
bool success = 1;
|
||||
string error = 2;
|
||||
int32 new_action_count = 3;
|
||||
int32 resynced_clients = 4; // Number of clients that were resynced with the new state
|
||||
}
|
||||
|
||||
@@ -16,7 +16,4 @@ option objc_class_prefix = "E0G";
|
||||
message ChronicleEntry {
|
||||
string generated_text_id = 1;
|
||||
.net.eagle0.eagle.common.Date date = 2;
|
||||
// The style/chronicler used when generating this entry (e.g., "J.R.R. Tolkien", "Homer")
|
||||
// Used to label entries so the LLM knows which chronicler wrote each one
|
||||
string chronicler_style = 3;
|
||||
}
|
||||
|
||||
@@ -143,15 +143,10 @@ message ChronicleUpdateMessage {
|
||||
message PreviousChronicleEntry {
|
||||
.net.eagle0.eagle.common.Date date = 1;
|
||||
string generated_text_id = 2;
|
||||
// The chronicler style used for this entry (e.g., "J.R.R. Tolkien", "Homer")
|
||||
string chronicler_style = 3;
|
||||
}
|
||||
repeated PreviousChronicleEntry previous_entries = 2;
|
||||
|
||||
repeated EventForChronicle new_entries = 3;
|
||||
|
||||
// The chronicler style to use for the new entry
|
||||
string chronicler_style = 4;
|
||||
}
|
||||
|
||||
message DivineMessage {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -271,4 +271,4 @@ priceIndexShiftPerGold 0.0001 Double
|
||||
aiTruceAcceptanceBaseChance 100 Double
|
||||
aiAllianceAcceptanceBaseChance 100 Double
|
||||
primeStatMinForProfession 85 Int
|
||||
professionGainChance 0.07 Double
|
||||
professionGainChance 0.1 Double
|
||||
|
@@ -1,225 +1,224 @@
|
||||
lightningWisdomFactor 1 double
|
||||
lightningAgilityFactor 0.25 double
|
||||
lightningDamageFactor 250 double
|
||||
meteorBaseDamage 2500 double
|
||||
meteorSplashFactor 0.4 double
|
||||
meteorDirectFireBaseChance 90 double
|
||||
meteorSplashFireBaseChance 50 double
|
||||
meteorStructureDamage 30 double
|
||||
meteorFrozenIntegrityReduced 80 double
|
||||
meteorSnowIntegrityReduced 80 double
|
||||
raiseDeadBaseOdds 100 int16
|
||||
dismissBaseOdds 60 int16
|
||||
fearBaseOdds 75 int16
|
||||
meleeKnowledgeIncrement 10 int8
|
||||
scoutDirectKnowledgeIncrement 35 int8
|
||||
scoutAdjacentKnowledgeIncrement 20 int8
|
||||
damageReceivedMultiplierFromStun 1.2 double
|
||||
damageGivenMultiplierFromStun 0.5 double
|
||||
minimumCastleToPreventCharge 15 double
|
||||
holyWaveBaseChance 75 double
|
||||
holyWaveInspireBonus 25 double
|
||||
holyWaveVigorBuf 5 double
|
||||
maxInspireOverBase 10 double
|
||||
holyWaveFailedDebuf -10 double
|
||||
holyWaveDamagePercentage 0.7 double
|
||||
minMoraleInCastle 15 double
|
||||
fleeActionPointCost 5 int8
|
||||
firePropensityCity 10 int16
|
||||
firePropensityHill 0 int16
|
||||
firePropensityPlains 0 int16
|
||||
firePropensityForest 20 int16
|
||||
firePropensityWater -500 int16
|
||||
firePropensitySwamp -70 int16
|
||||
firePropensityMountain -10 int16
|
||||
firePropensityBridge 10 int16
|
||||
firePropensityCastle 0 int16
|
||||
firePropensityIce -500 int16
|
||||
firePropensitySun 10 int16
|
||||
firePropensityClouds 0 int16
|
||||
firePropensityRain -60 int16
|
||||
firePropensitySnow -60 int16
|
||||
firePropensityThunderstorm -85 int16
|
||||
firePropensityBlizzard -85 int16
|
||||
firePropensityWindSpeedMultiplier 1 double
|
||||
extinguishOwnTileBuf 25 int16
|
||||
fireOutNaturallyBaseOdds 30 int16
|
||||
extinguishBaseOdds 70 int16
|
||||
startFireBaseOdds 40 int16
|
||||
freezeWaterBaseOdds 60 int16
|
||||
freezeWaterBonusSun -10 int16
|
||||
freezeWaterBonusClouds 0 int16
|
||||
freezeWaterBonusRain 10 int16
|
||||
freezeWaterBonusThunderstorm 10 int16
|
||||
freezeWaterBonusSnow 50 int16
|
||||
freezeWaterBonusBlizzard 100 int16
|
||||
scoutBaseOdds 75 int16
|
||||
ambushBaseOdds 50 int16
|
||||
moraleShiftPerCasualty 0.1 double
|
||||
minimumMoraleToAct 10 double
|
||||
minimumVigorToAct 10 double
|
||||
minimumVigorForCombat 20 double
|
||||
minimumVigorForMeteor 30 double
|
||||
extraCostForZocMovement 1 int8
|
||||
holyWaveActionPointCost 5 int8
|
||||
fallInWaterDamagePerTroop 150 double
|
||||
fallInWaterBaseEscapeOdds 50 int16
|
||||
retreatActionPointCost 5 int8
|
||||
reinforceActionPointCost 5 int8
|
||||
braveWaterActionPointCost 8 int8
|
||||
braveWaterBaseOdds 50 int16
|
||||
braveWaterOddsRain -20 int16
|
||||
braveWaterOddsThunderstorm -50 int16
|
||||
braveWaterOddsSnow -35 int16
|
||||
braveWaterOddsBlizzard -70 int16
|
||||
braveWaterAgilityFactor 0.2 double
|
||||
braveWaterWisdomFactor 0.1 double
|
||||
braveWaterRangerBonus 20 double
|
||||
braveWaterArmamentFactor -0.5 double
|
||||
braveWaterMaxLoss 0.2 double
|
||||
lightningRange 3 int8
|
||||
meteorRange 3 int8
|
||||
duelDeclinedMoraleAdjustment -15 double
|
||||
duelWonMoraleBoost 15 double
|
||||
minimumKnowledgeForProfession 20 int8
|
||||
minimumKnowledgeForName 30 int8
|
||||
minimumKnowledgeForBattalionName 30 int8
|
||||
minimumKnowledgeForBattalionStats 50 int8
|
||||
minimumKnowledgeForHeroStats 75 int8
|
||||
duelBaseAcceptOdds 50 int16
|
||||
meteorImpactStartFireBaseOdds 90 int16
|
||||
meteorSplashStartFireBaseOdds 50 int16
|
||||
meteorImpactCastleMinDamage 0 double
|
||||
meteorImpactCastleMaxDamage 30 double
|
||||
meteorImpactBridgeMinDamage 25 double
|
||||
meteorImpactBridgeMaxDamage 100 double
|
||||
meteorSplashBridgeMinDamage 12.5 double
|
||||
meteorSplashBridgeMaxDamage 60 double
|
||||
burningBridgeDamage 25 double
|
||||
burningCastleDamage 15 double
|
||||
buildBridgeBaseOdds 30 int16
|
||||
buildBridgeForestBonus 30 int16
|
||||
meleeActionPointCost 5 int8
|
||||
archeryActionPointCost 5 int8
|
||||
lightningActionPointCost 5 int8
|
||||
meteorStartActionPointCost 5 int8
|
||||
meteorCancelActionPointCost 1 int8
|
||||
meteorTargetActionPointCost 1 int8
|
||||
meteorCastActionPointCost 1 int8
|
||||
extinguishActionPointCost 5 int8
|
||||
startFireActionPointCost 5 int8
|
||||
freezeWaterActionPointCost 5 int8
|
||||
buildBridgeActionPointCost 5 int8
|
||||
repairActionPointCost 5 int8
|
||||
reduceActionPointCost 5 int8
|
||||
scoutActionPointCost 5 int8
|
||||
raiseDeadActionPointCost 5 int8
|
||||
fearActionPointCost 5 int8
|
||||
challengeDuelActionPointCost 5 int8
|
||||
scoutRange 4 int8
|
||||
raiseDeadRange 2 int8
|
||||
fearRange 3 int8
|
||||
reduceRange 3 int8
|
||||
heroDamageFactor 12.5 double
|
||||
moraleAdjustmentForNotEnoughFood -10 double
|
||||
sizeMultiplierForNotEnoughFood 0.95 double
|
||||
freezeWaterIntegrity 80 double
|
||||
repairBuf 25 double
|
||||
averageReduceDebuf 10 double
|
||||
reduceForestBonus 50 double
|
||||
unitDamagePerReduceDebuf 800 double
|
||||
fearDebuf -25 double
|
||||
fearFailedBuf 10 double
|
||||
actionCostToVigorCost 0.5 double
|
||||
moraleMeanReversion 0.1 double
|
||||
meteorCastVigorCost 20 double
|
||||
meleeStrengthXp 1 int8
|
||||
archeryAgilityXp 1 int8
|
||||
ambushAgilityXp 1 int8
|
||||
archeryAmbushAgilityXp 2 int8
|
||||
meteorCastWisdomXp 5 int8
|
||||
lightningBoltWisdomXp 1 int8
|
||||
startFireWisdomXp 1 int8
|
||||
startFireAgilityXp 1 int8
|
||||
extinguishAgilityXp 1 int8
|
||||
extinguishWisdomXp 1 int8
|
||||
duelStrengthXp 10 int8
|
||||
duelAgilityXp 10 int8
|
||||
duelWinnerCharismaXp 20 int8
|
||||
minimumVigorToAcceptDuel 30 double
|
||||
chargeAgilityXp 1 int8
|
||||
iceIntegrityAdjustmentRiver -20 double
|
||||
iceIntegrityAdjustmentSun -20 double
|
||||
iceIntegrityAdjustmentClouds 0 double
|
||||
iceIntegrityAdjustmentRain 0 double
|
||||
iceIntegrityAdjustmentThunderstorm 0 double
|
||||
iceIntegrityAdjustmentSnow 10 double
|
||||
iceIntegrityAdjustmentBlizzard 25 double
|
||||
iceIntegrityAdjustmentFire -50 double
|
||||
initialWinterIceIntegrity 30 double
|
||||
armamentToVolleys 0.1 double
|
||||
defenderBaseKnowledgeGain 2 int8
|
||||
attackerBaseKnowledgeGain 1 int8
|
||||
adjacentHeroKnowledgeGain 4 int8
|
||||
maxRounds 30 int8
|
||||
holyWaveCharismaXp 5 int8
|
||||
snowIntegrityAdjustmentSun -20 double
|
||||
snowIntegrityAdjustmentClouds 0 double
|
||||
snowIntegrityAdjustmentRain 0 double
|
||||
snowIntegrityAdjustmentThunderstorm 0 double
|
||||
snowIntegrityAdjustmentSnow 10 double
|
||||
snowIntegrityAdjustmentBlizzard 20 double
|
||||
snowIntegrityAdjustmentFire -100 double
|
||||
freezeWaterWisdomXp 3 int8
|
||||
buildBridgeAgilityXp 2 int8
|
||||
buildBridgeStrengthXp 2 int8
|
||||
repairAgilityXp 1 int8
|
||||
repairStrengthXp 1 int8
|
||||
reduceAgilityXp 2 int8
|
||||
reduceStrengthXp 2 int8
|
||||
scoutAgilityXp 2 int8
|
||||
scoutWisdomXp 2 int8
|
||||
hideActionPointCost 5 int8
|
||||
fortifyStrengthXp 1 int8
|
||||
fortifyAgilityXp 1 int8
|
||||
hideAgilityXp 2 int8
|
||||
fortifyActionPointCost 5 int8
|
||||
fortifyDamageTakenMultiplier 0.9 double
|
||||
raiseDeadWisdomXp 1 int8
|
||||
raiseDeadCharismaXp 2 int8
|
||||
fearCharismaXp 2 int8
|
||||
undeadDecayRate 0.25 double
|
||||
deadReinforceRate 0.3 double
|
||||
raiseDeadStartingSize 100 int16
|
||||
undeadDecayFloor 100 int16
|
||||
winterMoraleAdjustment -20 double
|
||||
stealthForestModifier 10 int16
|
||||
stealthSwampModifier 10 int16
|
||||
stealthWaterModifier -20 int16
|
||||
lightningBoltDamageCap 0.2 double
|
||||
vipCapturedMoraleAdjustment -20 double
|
||||
fleeSuccessBaseChance 120 int16
|
||||
fleeSuccessPerAdjacentEnemy -20 int16
|
||||
fleeSuccessPerAdjacentFriendly 10 int16
|
||||
fleeSuccessAdjustmentForTwoAway 0.5 double
|
||||
fleeSuccessPerAdjacentImpassableTile -20 int16
|
||||
heroBraverySwing 25 double
|
||||
heroDamageAmbushAdjustment 50 double
|
||||
heroMortalityFactor 0.005 double
|
||||
baseDeadliness 0.0012 double
|
||||
aiUtilityRepeatCount 5 int8
|
||||
maxLookaheadTurnsClose 2 int8
|
||||
maxLookaheadTurnsFar 2 int8
|
||||
saveAll FALSE bool
|
||||
holyWaveVigorCost 10 double
|
||||
minLookaheadTurns 1 int8
|
||||
lookaheadTimeBudgetCloseInSeconds 3 double
|
||||
lookaheadTimeBudgetFarInSeconds 1.5 double
|
||||
lookaheadTimeBudgetSetup 0.5 double
|
||||
lookaheadTimeBudgetPerCommandCloseMs 100 double
|
||||
lookaheadTimeBudgetPerCommandFarMs 50 double
|
||||
lookaheadTimeBudgetPerCommandSetupMs 25 double
|
||||
aiMinimumFleeOddsThreshold 30 int16
|
||||
aiDesperateFleeThreshold 10 int16
|
||||
lookaheadTimeBudgetMaximumSeconds 3 double
|
||||
allAiBattleTimeBudgetMaximum 0.5 double
|
||||
lightningWisdomFactor 1 double
|
||||
lightningAgilityFactor 0.25 double
|
||||
lightningDamageFactor 250 double
|
||||
meteorBaseDamage 2500 double
|
||||
meteorSplashFactor 0.4 double
|
||||
meteorDirectFireBaseChance 90 double
|
||||
meteorSplashFireBaseChance 50 double
|
||||
meteorStructureDamage 30 double
|
||||
meteorFrozenIntegrityReduced 80 double
|
||||
meteorSnowIntegrityReduced 80 double
|
||||
raiseDeadBaseOdds 100 int16
|
||||
dismissBaseOdds 60 int16
|
||||
fearBaseOdds 75 int16
|
||||
meleeKnowledgeIncrement 10 int8
|
||||
scoutDirectKnowledgeIncrement 35 int8
|
||||
scoutAdjacentKnowledgeIncrement 20 int8
|
||||
damageReceivedMultiplierFromStun 1.2 double
|
||||
damageGivenMultiplierFromStun 0.5 double
|
||||
minimumCastleToPreventCharge 15 double
|
||||
holyWaveBaseChance 75 double
|
||||
holyWaveInspireBonus 25 double
|
||||
holyWaveVigorBuf 5 double
|
||||
maxInspireOverBase 10 double
|
||||
holyWaveFailedDebuf -10 double
|
||||
holyWaveDamagePercentage 0.7 double
|
||||
minMoraleInCastle 15 double
|
||||
fleeActionPointCost 5 int8
|
||||
firePropensityCity 10 int16
|
||||
firePropensityHill 0 int16
|
||||
firePropensityPlains 0 int16
|
||||
firePropensityForest 20 int16
|
||||
firePropensityWater -500 int16
|
||||
firePropensitySwamp -70 int16
|
||||
firePropensityMountain -10 int16
|
||||
firePropensityBridge 10 int16
|
||||
firePropensityCastle 0 int16
|
||||
firePropensityIce -500 int16
|
||||
firePropensitySun 10 int16
|
||||
firePropensityClouds 0 int16
|
||||
firePropensityRain -60 int16
|
||||
firePropensitySnow -60 int16
|
||||
firePropensityThunderstorm -85 int16
|
||||
firePropensityBlizzard -85 int16
|
||||
firePropensityWindSpeedMultiplier 1 double
|
||||
extinguishOwnTileBuf 25 int16
|
||||
fireOutNaturallyBaseOdds 30 int16
|
||||
extinguishBaseOdds 70 int16
|
||||
startFireBaseOdds 40 int16
|
||||
freezeWaterBaseOdds 60 int16
|
||||
freezeWaterBonusSun -10 int16
|
||||
freezeWaterBonusClouds 0 int16
|
||||
freezeWaterBonusRain 10 int16
|
||||
freezeWaterBonusThunderstorm 10 int16
|
||||
freezeWaterBonusSnow 50 int16
|
||||
freezeWaterBonusBlizzard 100 int16
|
||||
scoutBaseOdds 75 int16
|
||||
ambushBaseOdds 50 int16
|
||||
moraleShiftPerCasualty 0.1 double
|
||||
minimumMoraleToAct 10 double
|
||||
minimumVigorToAct 10 double
|
||||
minimumVigorForCombat 20 double
|
||||
minimumVigorForMeteor 30 double
|
||||
extraCostForZocMovement 1 int8
|
||||
holyWaveActionPointCost 5 int8
|
||||
fallInWaterDamagePerTroop 150 double
|
||||
fallInWaterBaseEscapeOdds 50 int16
|
||||
retreatActionPointCost 5 int8
|
||||
reinforceActionPointCost 5 int8
|
||||
braveWaterActionPointCost 8 int8
|
||||
braveWaterBaseOdds 50 int16
|
||||
braveWaterOddsRain -20 int16
|
||||
braveWaterOddsThunderstorm -50 int16
|
||||
braveWaterOddsSnow -35 int16
|
||||
braveWaterOddsBlizzard -70 int16
|
||||
braveWaterAgilityFactor 0.2 double
|
||||
braveWaterWisdomFactor 0.1 double
|
||||
braveWaterRangerBonus 20 double
|
||||
braveWaterArmamentFactor -0.5 double
|
||||
braveWaterMaxLoss 0.2 double
|
||||
lightningRange 3 int8
|
||||
meteorRange 3 int8
|
||||
duelDeclinedMoraleAdjustment -15 double
|
||||
duelWonMoraleBoost 15 double
|
||||
minimumKnowledgeForProfession 20 int8
|
||||
minimumKnowledgeForName 30 int8
|
||||
minimumKnowledgeForBattalionName 30 int8
|
||||
minimumKnowledgeForBattalionStats 50 int8
|
||||
minimumKnowledgeForHeroStats 75 int8
|
||||
duelBaseAcceptOdds 50 int16
|
||||
meteorImpactStartFireBaseOdds 90 int16
|
||||
meteorSplashStartFireBaseOdds 50 int16
|
||||
meteorImpactCastleMinDamage 0 double
|
||||
meteorImpactCastleMaxDamage 30 double
|
||||
meteorImpactBridgeMinDamage 25 double
|
||||
meteorImpactBridgeMaxDamage 100 double
|
||||
meteorSplashBridgeMinDamage 12.5 double
|
||||
meteorSplashBridgeMaxDamage 60 double
|
||||
burningBridgeDamage 25 double
|
||||
burningCastleDamage 15 double
|
||||
buildBridgeBaseOdds 30 int16
|
||||
buildBridgeForestBonus 30 int16
|
||||
meleeActionPointCost 5 int8
|
||||
archeryActionPointCost 5 int8
|
||||
lightningActionPointCost 5 int8
|
||||
meteorStartActionPointCost 5 int8
|
||||
meteorCancelActionPointCost 1 int8
|
||||
meteorTargetActionPointCost 1 int8
|
||||
meteorCastActionPointCost 1 int8
|
||||
extinguishActionPointCost 5 int8
|
||||
startFireActionPointCost 5 int8
|
||||
freezeWaterActionPointCost 5 int8
|
||||
buildBridgeActionPointCost 5 int8
|
||||
repairActionPointCost 5 int8
|
||||
reduceActionPointCost 5 int8
|
||||
scoutActionPointCost 5 int8
|
||||
raiseDeadActionPointCost 5 int8
|
||||
fearActionPointCost 5 int8
|
||||
challengeDuelActionPointCost 5 int8
|
||||
scoutRange 4 int8
|
||||
raiseDeadRange 2 int8
|
||||
fearRange 3 int8
|
||||
reduceRange 3 int8
|
||||
heroDamageFactor 12.5 double
|
||||
moraleAdjustmentForNotEnoughFood -10 double
|
||||
sizeMultiplierForNotEnoughFood 0.95 double
|
||||
freezeWaterIntegrity 80 double
|
||||
repairBuf 25 double
|
||||
averageReduceDebuf 10 double
|
||||
reduceForestBonus 50 double
|
||||
unitDamagePerReduceDebuf 800 double
|
||||
fearDebuf -25 double
|
||||
fearFailedBuf 10 double
|
||||
actionCostToVigorCost 0.5 double
|
||||
moraleMeanReversion 0.1 double
|
||||
meteorCastVigorCost 20 double
|
||||
meleeStrengthXp 1 int8
|
||||
archeryAgilityXp 1 int8
|
||||
ambushAgilityXp 1 int8
|
||||
archeryAmbushAgilityXp 2 int8
|
||||
meteorCastWisdomXp 5 int8
|
||||
lightningBoltWisdomXp 1 int8
|
||||
startFireWisdomXp 1 int8
|
||||
startFireAgilityXp 1 int8
|
||||
extinguishAgilityXp 1 int8
|
||||
extinguishWisdomXp 1 int8
|
||||
duelStrengthXp 10 int8
|
||||
duelAgilityXp 10 int8
|
||||
duelWinnerCharismaXp 20 int8
|
||||
minimumVigorToAcceptDuel 30 double
|
||||
chargeAgilityXp 1 int8
|
||||
iceIntegrityAdjustmentRiver -20 double
|
||||
iceIntegrityAdjustmentSun -20 double
|
||||
iceIntegrityAdjustmentClouds 0 double
|
||||
iceIntegrityAdjustmentRain 0 double
|
||||
iceIntegrityAdjustmentThunderstorm 0 double
|
||||
iceIntegrityAdjustmentSnow 10 double
|
||||
iceIntegrityAdjustmentBlizzard 25 double
|
||||
iceIntegrityAdjustmentFire -50 double
|
||||
initialWinterIceIntegrity 30 double
|
||||
armamentToVolleys 0.1 double
|
||||
defenderBaseKnowledgeGain 2 int8
|
||||
attackerBaseKnowledgeGain 1 int8
|
||||
adjacentHeroKnowledgeGain 4 int8
|
||||
maxRounds 30 int8
|
||||
holyWaveCharismaXp 5 int8
|
||||
snowIntegrityAdjustmentSun -20 double
|
||||
snowIntegrityAdjustmentClouds 0 double
|
||||
snowIntegrityAdjustmentRain 0 double
|
||||
snowIntegrityAdjustmentThunderstorm 0 double
|
||||
snowIntegrityAdjustmentSnow 10 double
|
||||
snowIntegrityAdjustmentBlizzard 20 double
|
||||
snowIntegrityAdjustmentFire -100 double
|
||||
freezeWaterWisdomXp 3 int8
|
||||
buildBridgeAgilityXp 2 int8
|
||||
buildBridgeStrengthXp 2 int8
|
||||
repairAgilityXp 1 int8
|
||||
repairStrengthXp 1 int8
|
||||
reduceAgilityXp 2 int8
|
||||
reduceStrengthXp 2 int8
|
||||
scoutAgilityXp 2 int8
|
||||
scoutWisdomXp 2 int8
|
||||
hideActionPointCost 5 int8
|
||||
fortifyStrengthXp 1 int8
|
||||
fortifyAgilityXp 1 int8
|
||||
hideAgilityXp 2 int8
|
||||
fortifyActionPointCost 5 int8
|
||||
fortifyDamageTakenMultiplier 0.9 double
|
||||
raiseDeadWisdomXp 1 int8
|
||||
raiseDeadCharismaXp 2 int8
|
||||
fearCharismaXp 2 int8
|
||||
undeadDecayRate 0.25 double
|
||||
deadReinforceRate 0.3 double
|
||||
raiseDeadStartingSize 100 int16
|
||||
undeadDecayFloor 100 int16
|
||||
winterMoraleAdjustment -20 double
|
||||
stealthForestModifier 10 int16
|
||||
stealthSwampModifier 10 int16
|
||||
stealthWaterModifier -20 int16
|
||||
lightningBoltDamageCap 0.2 double
|
||||
vipCapturedMoraleAdjustment -20 double
|
||||
fleeSuccessBaseChance 120 int16
|
||||
fleeSuccessPerAdjacentEnemy -20 int16
|
||||
fleeSuccessPerAdjacentFriendly 10 int16
|
||||
fleeSuccessAdjustmentForTwoAway 0.5 double
|
||||
fleeSuccessPerAdjacentImpassableTile -20 int16
|
||||
heroBraverySwing 25 double
|
||||
heroDamageAmbushAdjustment 50 double
|
||||
heroMortalityFactor 0.005 double
|
||||
baseDeadliness 0.0012 double
|
||||
aiUtilityRepeatCount 5 int8
|
||||
maxLookaheadTurnsClose 2 int8
|
||||
maxLookaheadTurnsFar 2 int8
|
||||
saveAll FALSE bool
|
||||
holyWaveVigorCost 10 double
|
||||
minLookaheadTurns 1 int8
|
||||
lookaheadTimeBudgetCloseInSeconds 3 double
|
||||
lookaheadTimeBudgetFarInSeconds 1.5 double
|
||||
lookaheadTimeBudgetSetup 0.5 double
|
||||
lookaheadTimeBudgetPerCommandCloseMs 100 double
|
||||
lookaheadTimeBudgetPerCommandFarMs 50 double
|
||||
lookaheadTimeBudgetPerCommandSetupMs 25 double
|
||||
aiMinimumFleeOddsThreshold 30 int16
|
||||
aiDesperateFleeThreshold 10 int16
|
||||
lookaheadTimeBudgetMaximumSeconds 5 double
|
||||
|
@@ -21,9 +21,7 @@ scala_binary(
|
||||
"//src/main/scala/net/eagle0/eagle/service:custom_battle_manager",
|
||||
"//src/main/scala/net/eagle0/eagle/service:exception_interceptor",
|
||||
"//src/main/scala/net/eagle0/eagle/service:games_manager",
|
||||
"//src/main/scala/net/eagle0/eagle/service:oauth_http_handler",
|
||||
"//src/main/scala/net/eagle0/eagle/service:server_setup_helpers",
|
||||
"//src/main/scala/net/eagle0/eagle/service/persistence:async_s3_persister",
|
||||
"//src/main/scala/net/eagle0/eagle/service/persistence:local_file_persister",
|
||||
"//src/main/scala/net/eagle0/eagle/service/persistence:save_directory",
|
||||
"//src/main/scala/net/eagle0/eagle/shardok_interface:battle_resolution",
|
||||
|
||||
@@ -8,9 +8,9 @@ import io.grpc.{Server, ServerBuilder}
|
||||
import net.eagle0.common.{FunctionalRandom, SeededRandom, SimpleTimedLogger}
|
||||
import net.eagle0.eagle.api.auth.auth.AuthGrpc
|
||||
import net.eagle0.eagle.api.eagle.EagleGrpc
|
||||
import net.eagle0.eagle.auth.{JwtService, JwtServiceImpl, OAuthService, OAuthServiceImpl, UserServiceImpl}
|
||||
import net.eagle0.eagle.auth.{JwtService, JwtServiceImpl, OAuthServiceImpl, UserServiceImpl}
|
||||
import net.eagle0.eagle.service.*
|
||||
import net.eagle0.eagle.service.persistence.{AsyncS3Persister, LocalFilePersister, SaveDirectory}
|
||||
import net.eagle0.eagle.service.persistence.{LocalFilePersister, SaveDirectory}
|
||||
|
||||
object Main {
|
||||
import ServerSetupHelpers.*
|
||||
@@ -25,12 +25,6 @@ object Main {
|
||||
|
||||
private val gptModelNameKey = Symbol("gptModelName")
|
||||
|
||||
private val serverBaseUrlKey = Symbol("serverBaseUrl")
|
||||
private val defaultServerBaseUrl = "https://prod.eagle0.net"
|
||||
|
||||
private val oauthHttpPortKey = Symbol("oauthHttpPort")
|
||||
private val defaultOauthHttpPort = "8080"
|
||||
|
||||
def parsedOptions(args: Array[String]): Map[Symbol, String] = {
|
||||
def nextOption(
|
||||
map: Map[Symbol, String],
|
||||
@@ -49,16 +43,6 @@ object Main {
|
||||
map ++ Map(gptModelNameKey -> value),
|
||||
tail
|
||||
)
|
||||
case "--server-base-url" +: value +: tail =>
|
||||
nextOption(
|
||||
map ++ Map(serverBaseUrlKey -> value),
|
||||
tail
|
||||
)
|
||||
case "--oauth-http-port" +: value +: tail =>
|
||||
nextOption(
|
||||
map ++ Map(oauthHttpPortKey -> value),
|
||||
tail
|
||||
)
|
||||
case option +: _ =>
|
||||
throw new IllegalArgumentException("Unknown option " + option)
|
||||
|
||||
@@ -70,9 +54,6 @@ object Main {
|
||||
def main(args: Array[String]): Unit = {
|
||||
SimpleTimedLogger.printLogger.logLine("Starting!")
|
||||
|
||||
// Register shutdown hook to flush pending S3 saves on graceful shutdown
|
||||
AsyncS3Persister.registerShutdownHook()
|
||||
|
||||
val options = parsedOptions(args)
|
||||
SimpleTimedLogger.printLogger.logLine(s"Options: $options")
|
||||
|
||||
@@ -93,24 +74,11 @@ object Main {
|
||||
val customBattleManager = newCustomBattleManager(shardokInterfaceAddress)
|
||||
customBattleManager.begin()
|
||||
|
||||
val serverBaseUrl =
|
||||
options.getOrElse(serverBaseUrlKey, defaultServerBaseUrl)
|
||||
|
||||
// Create OAuth service (shared between gRPC and HTTP)
|
||||
val oauthService = new OAuthServiceImpl(serverBaseUrl)
|
||||
|
||||
// Start OAuth HTTP server for callbacks
|
||||
val oauthHttpPort =
|
||||
options.getOrElse(oauthHttpPortKey, defaultOauthHttpPort).toInt
|
||||
val oauthHttpHandler = new OAuthHttpHandler(oauthService, oauthHttpPort)
|
||||
oauthHttpHandler.start()
|
||||
|
||||
val server = buildServer(
|
||||
gamesManager,
|
||||
customBattleManager,
|
||||
options.getOrElse(eagleGrpcPortKey, defaultEagleGrpcPort).toInt,
|
||||
SeededRandom(random.nextLong()),
|
||||
oauthService
|
||||
SeededRandom(random.nextLong())
|
||||
).start
|
||||
SimpleTimedLogger.printLogger.logLine(
|
||||
s"Eagle server is listening on port ${server.getPort}"
|
||||
@@ -123,8 +91,7 @@ object Main {
|
||||
gamesManager: GamesManager,
|
||||
customBattleManager: CustomBattleManager,
|
||||
grpcPort: Int,
|
||||
functionalRandom: FunctionalRandom,
|
||||
oauthService: OAuthService
|
||||
functionalRandom: FunctionalRandom
|
||||
): Server = {
|
||||
val serverBuilder = ServerBuilder.forPort(grpcPort)
|
||||
|
||||
@@ -132,6 +99,7 @@ object Main {
|
||||
val jwtService: Option[JwtService] = JwtServiceImpl.fromEnvironment()
|
||||
val authPersister = LocalFilePersister(SaveDirectory.saveDirectory)
|
||||
val userService = new UserServiceImpl(authPersister)
|
||||
val oauthService = new OAuthServiceImpl()
|
||||
|
||||
// Add Eagle game service
|
||||
serverBuilder.addService(
|
||||
|
||||
@@ -151,7 +151,7 @@ case class AIClient(
|
||||
(actingFactionId, gameState, availableCommands, functionalRandom) =>
|
||||
AttackDecisionCommandChooser.attackDecisionSelectedCommand(
|
||||
actingFactionId,
|
||||
GameStateConverter.fromProto(gameState),
|
||||
gameState,
|
||||
availableCommands,
|
||||
functionalRandom
|
||||
),
|
||||
|
||||
@@ -1,53 +1,27 @@
|
||||
package net.eagle0.eagle.ai
|
||||
|
||||
import net.eagle0.eagle.{FactionId, HeroId, ProvinceId}
|
||||
import net.eagle0.eagle.internal.game_state.GameState as GameStateProto
|
||||
import net.eagle0.eagle.internal.province.Province as ProvinceProto
|
||||
import net.eagle0.eagle.library.util.faction_utils.{FactionUtils, LegacyFactionUtils}
|
||||
import net.eagle0.eagle.library.util.hero.{HeroUtils, LegacyHeroUtils}
|
||||
import net.eagle0.eagle.model.state.faction.FactionT
|
||||
import net.eagle0.eagle.model.state.hero.HeroT
|
||||
import net.eagle0.eagle.model.state.province.ProvinceT
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.internal.province.Province
|
||||
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
|
||||
import net.eagle0.eagle.library.util.hero.LegacyHeroUtils
|
||||
|
||||
object AIClientUtils {
|
||||
def extraHeroCount(pid: ProvinceId, fid: FactionId, gs: GameStateProto): Int =
|
||||
def extraHeroCount(pid: ProvinceId, fid: FactionId, gs: GameState): Int =
|
||||
(gs.provinces(pid).rulingFactionHeroIds.size - desiredHeroCount(
|
||||
pid,
|
||||
fid,
|
||||
gs
|
||||
)).max(0)
|
||||
|
||||
def desiredHeroCount(pid: ProvinceId, fid: FactionId, gs: GameStateProto): Int =
|
||||
def desiredHeroCount(pid: ProvinceId, fid: FactionId, gs: GameState): Int =
|
||||
if LegacyFactionUtils.hostileNeighbors(pid, fid, gs).nonEmpty then 3
|
||||
else if gs.provinces(pid).support < 65 then 2
|
||||
else 1
|
||||
|
||||
def desiredCountForMarchTowardFocus(
|
||||
originProvince: ProvinceProto,
|
||||
destinationProvince: ProvinceProto,
|
||||
availableHeroIds: Vector[HeroId],
|
||||
factionLeaders: Vector[HeroId],
|
||||
favorLeaders: Boolean
|
||||
): Int =
|
||||
if favorLeaders &&
|
||||
originProvince.rulingFactionHeroIds.size == 2 &&
|
||||
originProvince.rulingFactionHeroIds
|
||||
.intersect(factionLeaders)
|
||||
.nonEmpty &&
|
||||
destinationProvince.rulingFactionHeroIds
|
||||
.intersect(factionLeaders)
|
||||
.isEmpty
|
||||
then 1
|
||||
else
|
||||
Vector(
|
||||
10,
|
||||
originProvince.rulingFactionHeroIds.size - 2,
|
||||
availableHeroIds.size
|
||||
).min
|
||||
|
||||
def desiredCountForMarchTowardFocus(
|
||||
originProvince: ProvinceT,
|
||||
destinationProvince: ProvinceT,
|
||||
originProvince: Province,
|
||||
destinationProvince: Province,
|
||||
availableHeroIds: Vector[HeroId],
|
||||
factionLeaders: Vector[HeroId],
|
||||
favorLeaders: Boolean
|
||||
@@ -69,11 +43,11 @@ object AIClientUtils {
|
||||
).min
|
||||
|
||||
def takenHeroIdsForMarchTowardFocus(
|
||||
originProvince: ProvinceProto,
|
||||
destinationProvince: ProvinceProto,
|
||||
originProvince: Province,
|
||||
destinationProvince: Province,
|
||||
availableHeroIds: Vector[HeroId],
|
||||
favorLeaders: Boolean,
|
||||
gs: GameStateProto
|
||||
gs: GameState
|
||||
): Vector[HeroId] =
|
||||
mostPowerfulHeroes(
|
||||
desiredCountForMarchTowardFocus(
|
||||
@@ -88,31 +62,9 @@ object AIClientUtils {
|
||||
favorLeaders
|
||||
)
|
||||
|
||||
def takenHeroIdsForMarchTowardFocus(
|
||||
originProvince: ProvinceT,
|
||||
destinationProvince: ProvinceT,
|
||||
availableHeroIds: Vector[HeroId],
|
||||
favorLeaders: Boolean,
|
||||
factions: Vector[FactionT],
|
||||
heroes: Map[HeroId, HeroT]
|
||||
): Vector[HeroId] =
|
||||
mostPowerfulHeroes(
|
||||
desiredCountForMarchTowardFocus(
|
||||
originProvince = originProvince,
|
||||
destinationProvince = destinationProvince,
|
||||
availableHeroIds = availableHeroIds,
|
||||
factionLeaders = factions.find(_.id == originProvince.rulingFactionId.get).get.leaderIds,
|
||||
favorLeaders = favorLeaders
|
||||
),
|
||||
availableHeroIds,
|
||||
favorLeaders,
|
||||
factions,
|
||||
heroes
|
||||
)
|
||||
|
||||
def mostPowerfulHeroes(
|
||||
desiredCount: Int,
|
||||
gs: GameStateProto,
|
||||
gs: GameState,
|
||||
availableHeroIds: Vector[HeroId],
|
||||
favorLeaders: Boolean
|
||||
): Vector[HeroId] = {
|
||||
@@ -132,28 +84,4 @@ object AIClientUtils {
|
||||
.map(_.id)
|
||||
.take(desiredCount)
|
||||
}
|
||||
|
||||
def mostPowerfulHeroes(
|
||||
desiredCount: Int,
|
||||
availableHeroIds: Vector[HeroId],
|
||||
favorLeaders: Boolean,
|
||||
factions: Vector[FactionT],
|
||||
heroes: Map[HeroId, HeroT]
|
||||
): Vector[HeroId] = {
|
||||
val availableHeroes = availableHeroIds
|
||||
.map(heroes)
|
||||
|
||||
if availableHeroes.size <= desiredCount then availableHeroes.map(_.id)
|
||||
else
|
||||
availableHeroes
|
||||
.sortBy(hero =>
|
||||
(
|
||||
if favorLeaders then !FactionUtils.isFactionLeader(hero.id, factions)
|
||||
else FactionUtils.isFactionLeader(hero.id, factions),
|
||||
-HeroUtils.power(hero)
|
||||
)
|
||||
)
|
||||
.map(_.id)
|
||||
.take(desiredCount)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,13 +51,8 @@ scala_library(
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils:legacy_faction_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/hero:hero_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/hero:legacy_hero_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/faction",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -148,12 +143,11 @@ scala_library(
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:battalion_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:command_selection",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:eagle_require",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:province_distances",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:legacy_battalion_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:legacy_province_distances",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:province_gold_surplus_calculator",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -205,7 +199,6 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/hero:legacy_hero_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/province:legacy_province_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/province",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -224,8 +217,6 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:command_selection",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:legacy_province_distances",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:province_distances",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@ import net.eagle0.eagle.{FactionId, ProvinceId}
|
||||
import net.eagle0.eagle.api.available_command.{MarchAvailableCommand, MarchCommandFromOneProvince}
|
||||
import net.eagle0.eagle.api.selected_command.MarchSelectedCommand
|
||||
import net.eagle0.eagle.common.combat_unit.CombatUnit
|
||||
import net.eagle0.eagle.library.util.{BattalionUtils, CommandSelection, ProvinceDistances}
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.library.util.{CommandSelection, LegacyBattalionUtils, LegacyProvinceDistances}
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.ProvinceGoldSurplusCalculator.provinceGoldSurplus
|
||||
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
|
||||
object MarchTowardProvinceCommandChooser {
|
||||
def marchTowardProvinceCommand(
|
||||
@@ -23,12 +23,12 @@ object MarchTowardProvinceCommandChooser {
|
||||
.map(dest => (opmc, gs.provinces(dest.provinceId)))
|
||||
.flatMap {
|
||||
case (opmc, p) =>
|
||||
ProvinceDistances
|
||||
LegacyProvinceDistances
|
||||
.distanceThroughFriendliesOption(
|
||||
destinationProvinceId,
|
||||
p.id,
|
||||
fid,
|
||||
gs.provinces
|
||||
gs
|
||||
)
|
||||
.map(distance => (opmc, p, distance))
|
||||
}
|
||||
@@ -51,8 +51,7 @@ object MarchTowardProvinceCommandChooser {
|
||||
destinationProvince = dest,
|
||||
availableHeroIds = opmc.availableHeroIds.toVector,
|
||||
favorLeaders = favorLeaders,
|
||||
factions = gs.factions.values.toVector,
|
||||
heroes = gs.heroes
|
||||
gs = gs
|
||||
)
|
||||
|
||||
Option.when(takenHeroIds.nonEmpty) {
|
||||
@@ -75,11 +74,11 @@ object MarchTowardProvinceCommandChooser {
|
||||
CombatUnit(factionId = fid, heroId = hid, battalionId = bid)
|
||||
}
|
||||
|
||||
val foodToTake = 2 * BattalionUtils.monthlyConsumedFood(
|
||||
val foodToTake = 2 * LegacyBattalionUtils.monthlyConsumedFood(
|
||||
takenBattalions,
|
||||
gs.battalionTypes
|
||||
gs.battalionTypes.toVector
|
||||
)
|
||||
val goldToTake = provinceGoldSurplus(originProvince)
|
||||
val goldToTake = provinceGoldSurplus(originProvince.id, gs)
|
||||
CommandSelection(
|
||||
actingFactionId = fid,
|
||||
actingProvinceId = actingProvinceId,
|
||||
|
||||
@@ -34,7 +34,6 @@ import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
|
||||
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
|
||||
import net.eagle0.eagle.library.util.LegacyProvinceDistances
|
||||
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
|
||||
import net.eagle0.eagle.model.proto_converters.province.ProvinceConverter
|
||||
import net.eagle0.eagle.views.battalion_view.BattalionView
|
||||
import net.eagle0.eagle.views.province_view.FullProvinceInfo
|
||||
|
||||
@@ -153,7 +152,7 @@ object MidGameAIClient {
|
||||
Math.min(actingProvince.food - foodToHoldBack, foodAvailable)
|
||||
val maxGoldToSend = Math.min(
|
||||
ProvinceGoldSurplusCalculator
|
||||
.provinceGoldSurplus(ProvinceConverter.fromProto(actingProvince)),
|
||||
.provinceGoldSurplus(actingProvinceId, gameState),
|
||||
goldAvailable
|
||||
)
|
||||
|
||||
@@ -941,7 +940,7 @@ object MidGameAIClient {
|
||||
fid = actingFactionId,
|
||||
opmc = opmc,
|
||||
destinationProvinceId = destinationProvinceId,
|
||||
gs = GameStateConverter.fromProto(gs),
|
||||
gs = gs,
|
||||
favorLeaders = false
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.library.util.{LegacyProvinceDistances, ProvinceDistances}
|
||||
import net.eagle0.eagle.library.util.CommandSelection
|
||||
import net.eagle0.eagle.library.util.ProvinceDistances.ProvinceAndDistance
|
||||
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
|
||||
|
||||
object MoveLeaderToBetterProvinceCommandChooser {
|
||||
private case class MCFOPWithDestinationAndDistance(
|
||||
@@ -96,7 +95,7 @@ object MoveLeaderToBetterProvinceCommandChooser {
|
||||
fid = actingFactionId,
|
||||
opmc = mcfopwd.mcfop,
|
||||
destinationProvinceId = mcfopwd.provinceAndDistance.provinceId,
|
||||
gs = GameStateConverter.fromProto(gs),
|
||||
gs = gs,
|
||||
favorLeaders = true
|
||||
)
|
||||
}
|
||||
|
||||
@@ -28,9 +28,6 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle:__subpackages__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:auth_scala_grpc",
|
||||
],
|
||||
deps = [
|
||||
":oauth_config",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:auth_scala_grpc",
|
||||
|
||||
@@ -24,30 +24,22 @@ case class ProviderUserInfo(
|
||||
username: String
|
||||
)
|
||||
|
||||
/** Result of OAuth flow */
|
||||
sealed trait OAuthResult
|
||||
case class OAuthSuccess(providerInfo: ProviderUserInfo, provider: OAuthProvider) extends OAuthResult
|
||||
case class OAuthFailure(error: String) extends OAuthResult
|
||||
case object OAuthPending extends OAuthResult
|
||||
case object OAuthExpired extends OAuthResult
|
||||
|
||||
/** Service for OAuth code exchange */
|
||||
trait OAuthService {
|
||||
|
||||
/** Generate OAuth authorization URL. Server handles callback internally. */
|
||||
def getAuthUrl(provider: OAuthProvider): (String, String) // (url, state)
|
||||
/** Generate OAuth authorization URL */
|
||||
def getAuthUrl(provider: OAuthProvider, redirectUri: String): (String, String) // (url, state)
|
||||
|
||||
/** Check status of OAuth flow for given state */
|
||||
def checkStatus(state: String): OAuthResult
|
||||
|
||||
/** Handle OAuth callback - exchange code for user info and store result */
|
||||
def handleCallback(code: String, state: String): Either[String, ProviderUserInfo]
|
||||
|
||||
/** Get the server's OAuth callback URL */
|
||||
def callbackUrl: String
|
||||
/** Exchange authorization code for user info */
|
||||
def exchangeCode(
|
||||
provider: OAuthProvider,
|
||||
code: String,
|
||||
state: String,
|
||||
redirectUri: String
|
||||
): Either[String, ProviderUserInfo]
|
||||
}
|
||||
|
||||
class OAuthServiceImpl(serverBaseUrl: String) extends OAuthService {
|
||||
class OAuthServiceImpl extends OAuthService {
|
||||
|
||||
private implicit val formats: DefaultFormats.type = DefaultFormats
|
||||
private val json = new Json(formats)
|
||||
@@ -57,29 +49,25 @@ class OAuthServiceImpl(serverBaseUrl: String) extends OAuthService {
|
||||
.connectTimeout(Duration.ofSeconds(10))
|
||||
.build()
|
||||
|
||||
// Pending OAuth sessions: state -> (provider, timestamp)
|
||||
private val pendingOAuth = TrieMap[String, (OAuthProvider, Long)]()
|
||||
|
||||
// Completed OAuth results: state -> result
|
||||
private val completedOAuth = TrieMap[String, OAuthResult]()
|
||||
// State parameter storage with timestamp (for CSRF protection)
|
||||
private val stateStore = TrieMap[String, Long]()
|
||||
|
||||
// State expiration (10 minutes)
|
||||
private val stateExpirationMs = 600000L
|
||||
|
||||
override def callbackUrl: String = s"$serverBaseUrl/oauth/callback"
|
||||
|
||||
override def getAuthUrl(provider: OAuthProvider): (String, String) = {
|
||||
override def getAuthUrl(provider: OAuthProvider, redirectUri: String): (String, String) = {
|
||||
val config = getConfig(provider)
|
||||
|
||||
val state = UUID.randomUUID().toString
|
||||
pendingOAuth.put(state, (provider, System.currentTimeMillis()))
|
||||
stateStore.put(state, System.currentTimeMillis())
|
||||
|
||||
// Clean old states
|
||||
cleanupExpiredStates()
|
||||
val cutoff = System.currentTimeMillis() - stateExpirationMs
|
||||
stateStore.filterInPlace((_, timestamp) => timestamp > cutoff)
|
||||
|
||||
val params = Map(
|
||||
"client_id" -> config.clientId,
|
||||
"redirect_uri" -> callbackUrl,
|
||||
"redirect_uri" -> redirectUri,
|
||||
"response_type" -> "code",
|
||||
"scope" -> config.scopes.mkString(" "),
|
||||
"state" -> state
|
||||
@@ -95,67 +83,30 @@ class OAuthServiceImpl(serverBaseUrl: String) extends OAuthService {
|
||||
(url, state)
|
||||
}
|
||||
|
||||
override def checkStatus(state: String): OAuthResult =
|
||||
// First check completed results
|
||||
completedOAuth.get(state) match {
|
||||
case Some(result) => result
|
||||
case None =>
|
||||
// Check if still pending
|
||||
pendingOAuth.get(state) match {
|
||||
case Some((_, timestamp)) if System.currentTimeMillis() - timestamp < stateExpirationMs =>
|
||||
OAuthPending
|
||||
case Some(_) =>
|
||||
// Expired
|
||||
pendingOAuth.remove(state)
|
||||
OAuthExpired
|
||||
case None =>
|
||||
// Unknown state - might be expired and cleaned up
|
||||
OAuthExpired
|
||||
}
|
||||
override def exchangeCode(
|
||||
provider: OAuthProvider,
|
||||
code: String,
|
||||
state: String,
|
||||
redirectUri: String
|
||||
): Either[String, ProviderUserInfo] = {
|
||||
// Verify state
|
||||
stateStore.remove(state) match {
|
||||
case Some(timestamp) if System.currentTimeMillis() - timestamp < stateExpirationMs =>
|
||||
// Valid state
|
||||
case _ =>
|
||||
return Left("Invalid or expired state parameter")
|
||||
}
|
||||
|
||||
override def handleCallback(code: String, state: String): Either[String, ProviderUserInfo] =
|
||||
// Get pending OAuth info
|
||||
pendingOAuth.remove(state) match {
|
||||
case Some((provider, timestamp)) if System.currentTimeMillis() - timestamp < stateExpirationMs =>
|
||||
val config = getConfig(provider)
|
||||
val config = getConfig(provider)
|
||||
|
||||
// Exchange code for access token
|
||||
val tokenResult = exchangeCodeForToken(config, code)
|
||||
tokenResult match {
|
||||
case Left(error) =>
|
||||
completedOAuth.put(state, OAuthFailure(error))
|
||||
Left(error)
|
||||
case Right(accessToken) =>
|
||||
// Fetch user info
|
||||
fetchUserInfo(provider, config, accessToken) match {
|
||||
case Left(error) =>
|
||||
completedOAuth.put(state, OAuthFailure(error))
|
||||
Left(error)
|
||||
case Right(userInfo) =>
|
||||
completedOAuth.put(state, OAuthSuccess(userInfo, provider))
|
||||
Right(userInfo)
|
||||
}
|
||||
}
|
||||
|
||||
case Some(_) =>
|
||||
completedOAuth.put(state, OAuthExpired)
|
||||
Left("OAuth session expired")
|
||||
|
||||
case None =>
|
||||
Left("Invalid or expired state parameter")
|
||||
// Exchange code for access token
|
||||
val tokenResult = exchangeCodeForToken(config, code, redirectUri)
|
||||
tokenResult match {
|
||||
case Left(error) => Left(error)
|
||||
case Right(accessToken) =>
|
||||
// Fetch user info
|
||||
fetchUserInfo(provider, config, accessToken)
|
||||
}
|
||||
|
||||
private def cleanupExpiredStates(): Unit = {
|
||||
val cutoff = System.currentTimeMillis() - stateExpirationMs
|
||||
pendingOAuth.filterInPlace((_, v) => v._2 > cutoff)
|
||||
completedOAuth.filterInPlace((state, _) =>
|
||||
pendingOAuth.contains(state) ||
|
||||
completedOAuth.get(state).exists {
|
||||
case OAuthSuccess(_, _) => true // Keep successes for a bit
|
||||
case _ => false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private def getConfig(provider: OAuthProvider): OAuthProviderConfig =
|
||||
@@ -167,7 +118,8 @@ class OAuthServiceImpl(serverBaseUrl: String) extends OAuthService {
|
||||
|
||||
private def exchangeCodeForToken(
|
||||
config: OAuthProviderConfig,
|
||||
code: String
|
||||
code: String,
|
||||
redirectUri: String
|
||||
): Either[String, String] =
|
||||
Try {
|
||||
val formData = Map(
|
||||
@@ -175,7 +127,7 @@ class OAuthServiceImpl(serverBaseUrl: String) extends OAuthService {
|
||||
"client_secret" -> config.clientSecret,
|
||||
"grant_type" -> "authorization_code",
|
||||
"code" -> code,
|
||||
"redirect_uri" -> callbackUrl
|
||||
"redirect_uri" -> redirectUri
|
||||
)
|
||||
|
||||
val formBody = formData.map {
|
||||
|
||||
@@ -51,13 +51,4 @@ trait Engine {
|
||||
selectedProvinceId: ProvinceId,
|
||||
selectedCommand: SelectedCommand
|
||||
): EngineAndResults
|
||||
|
||||
/**
|
||||
* Rewinds the game to a previous state by truncating history.
|
||||
* @param targetActionCount
|
||||
* The number of actions to keep (rewinds to state after this many actions)
|
||||
* @return
|
||||
* A new Engine at the target state
|
||||
*/
|
||||
def rewindTo(targetActionCount: Int): Engine
|
||||
}
|
||||
|
||||
@@ -273,19 +273,6 @@ case class EngineImpl(
|
||||
)
|
||||
}
|
||||
|
||||
override def rewindTo(targetActionCount: Int): EngineImpl = {
|
||||
internalRequire(
|
||||
targetActionCount >= 0 && targetActionCount <= history.count,
|
||||
s"Target action count $targetActionCount must be between 0 and ${history.count}"
|
||||
)
|
||||
|
||||
val truncatedHistory = history.truncateTo(targetActionCount)
|
||||
copy(
|
||||
currentState = GameStateConverter.fromProto(truncatedHistory.last.gameState).copy(gameId = gameId),
|
||||
history = truncatedHistory
|
||||
)
|
||||
}
|
||||
|
||||
def postCommand(
|
||||
factionId: FactionId,
|
||||
selectedProvinceId: ProvinceId,
|
||||
|
||||
@@ -40,14 +40,11 @@ trait GameHistory {
|
||||
|
||||
def withNewResults(newResults: Vector[ActionWithResultingState]): GameHistory
|
||||
|
||||
def truncateTo(targetActionCount: Int): GameHistory
|
||||
|
||||
def withNewResultsScala(newResults: Vector[ActionResultWithResultingState]): GameHistory =
|
||||
withNewResults(newResults.map { awrs =>
|
||||
ActionWithResultingState(
|
||||
actionResult = ActionResultProtoConverter.toProto(awrs.actionResult),
|
||||
gameState = GameStateConverter.toProto(awrs.resultingState),
|
||||
precomputedScalaState = Some(awrs.resultingState) // Preserve Scala state to avoid re-conversion
|
||||
gameState = GameStateConverter.toProto(awrs.resultingState)
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -851,7 +851,6 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library:game_history",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_applier",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_random_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/llm_prompt_generators:chronicle_update_prompt_generator",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/random_state_sequencer",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:empty_province_monthly_devastation_delta",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:faction_bias_minimum_adjustment_per_round",
|
||||
|
||||
+29
-53
@@ -75,14 +75,9 @@ case class CheckForFactionChangesAction(
|
||||
val newLeaders =
|
||||
faction.leaderIds.diff(killedHeroIds)
|
||||
|
||||
// Only change faction head if the current head was killed
|
||||
val newFactionHeadId =
|
||||
if newLeaders.contains(faction.factionHeadId) then faction.factionHeadId
|
||||
else newLeaders.headOption.getOrElse(0)
|
||||
|
||||
faction.copy(
|
||||
leaderIds = newLeaders,
|
||||
factionHeadId = newFactionHeadId
|
||||
factionHeadId = newLeaders.headOption.getOrElse(0)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -216,65 +211,46 @@ case class CheckForFactionChangesAction(
|
||||
case Nil => None
|
||||
case fs =>
|
||||
val changedFactionData = fs.map { revisedFaction =>
|
||||
val originalFaction = factions.find(_.id == revisedFaction.id).get
|
||||
val factionHeadChanged = originalFaction.factionHeadId != revisedFaction.factionHeadId
|
||||
|
||||
// Only include new head info if the head actually changed
|
||||
val (newFactionHeadHeroId, newFactionName, llmRequest, notification) =
|
||||
if factionHeadChanged then {
|
||||
val newHeadHero = heroes.find(_.id == revisedFaction.factionHeadId)
|
||||
val newName = newHeadHero.flatMap(_.ledFactionName)
|
||||
val llmRequestId = s"new_faction_head_${revisedFaction.id}_${revisedFaction.factionHeadId}"
|
||||
(
|
||||
Some(revisedFaction.factionHeadId),
|
||||
newName,
|
||||
Some(
|
||||
NewFactionHeadMessage(
|
||||
requestId = llmRequestId,
|
||||
eagleGameId = gameId,
|
||||
recipientFactionIds = Vector.empty,
|
||||
alwaysGenerate = false,
|
||||
newHeadHeroId = revisedFaction.factionHeadId,
|
||||
factionId = revisedFaction.id,
|
||||
previousFactionName = originalFaction.name,
|
||||
newFactionName = newName,
|
||||
previousHeadHeroId = originalFaction.factionHeadId
|
||||
)
|
||||
),
|
||||
Some(
|
||||
NotificationC(
|
||||
details = NotificationDetails.NewFactionHead(
|
||||
newHeadHeroId = revisedFaction.factionHeadId,
|
||||
factionId = revisedFaction.id,
|
||||
previousHeadHeroId = originalFaction.factionHeadId
|
||||
),
|
||||
affectedHeroIds = Vector(revisedFaction.factionHeadId, originalFaction.factionHeadId),
|
||||
llm = NotificationT.Llm.Id(llmRequestId),
|
||||
deferred = true
|
||||
)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
(None, None, None, None)
|
||||
}
|
||||
|
||||
val originalFaction = factions.find(_.id == revisedFaction.id).get
|
||||
val newHeadHero = heroes.find(_.id == revisedFaction.factionHeadId)
|
||||
val newFactionName = newHeadHero.flatMap(_.ledFactionName)
|
||||
val llmRequestId = s"new_faction_head_${revisedFaction.id}_${revisedFaction.factionHeadId}"
|
||||
(
|
||||
ChangedFactionC(
|
||||
factionId = revisedFaction.id,
|
||||
removedLeaderHeroIds = originalFaction.leaderIds.filter(killedHeroIds.contains),
|
||||
newFactionHeadHeroId = newFactionHeadHeroId,
|
||||
newFactionHeadHeroId = Some(revisedFaction.factionHeadId),
|
||||
newName = newFactionName
|
||||
),
|
||||
llmRequest,
|
||||
notification
|
||||
NewFactionHeadMessage(
|
||||
requestId = llmRequestId,
|
||||
eagleGameId = gameId,
|
||||
recipientFactionIds = Vector.empty,
|
||||
alwaysGenerate = false,
|
||||
newHeadHeroId = revisedFaction.factionHeadId,
|
||||
factionId = revisedFaction.id,
|
||||
previousFactionName = originalFaction.name,
|
||||
newFactionName = newFactionName,
|
||||
previousHeadHeroId = originalFaction.factionHeadId
|
||||
),
|
||||
NotificationC(
|
||||
details = NotificationDetails.NewFactionHead(
|
||||
newHeadHeroId = revisedFaction.factionHeadId,
|
||||
factionId = revisedFaction.id,
|
||||
previousHeadHeroId = originalFaction.factionHeadId
|
||||
),
|
||||
affectedHeroIds = Vector(revisedFaction.factionHeadId, originalFaction.factionHeadId),
|
||||
llm = NotificationT.Llm.Id(llmRequestId),
|
||||
deferred = true
|
||||
)
|
||||
)
|
||||
}.toVector
|
||||
Some(
|
||||
ActionResultC(
|
||||
actionResultType = FactionLeaderRemovedResultType,
|
||||
changedFactions = changedFactionData.map(_._1),
|
||||
newGeneratedTextRequests = changedFactionData.flatMap(_._2),
|
||||
newNotifications = changedFactionData.flatMap(_._3)
|
||||
newGeneratedTextRequests = changedFactionData.map(_._2),
|
||||
newNotifications = changedFactionData.map(_._3)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import net.eagle0.common.{FunctionalRandom, RandomState}
|
||||
import net.eagle0.eagle.{HeroId, ProvinceId, RoundId}
|
||||
import net.eagle0.eagle.library.actions.applier.ActionResultApplier
|
||||
import net.eagle0.eagle.library.actions.impl.common.ProtolessRandomSequentialResultsAction
|
||||
import net.eagle0.eagle.library.actions.llm_prompt_generators.ChronicleOptions
|
||||
import net.eagle0.eagle.library.actions.random_state_sequencer.RandomStateSequencer
|
||||
import net.eagle0.eagle.library.settings.{
|
||||
EmptyProvinceMonthlyDevastationDelta,
|
||||
@@ -86,18 +85,12 @@ case class NewRoundAction(
|
||||
initialSequencer.withActionResult(_ => NewYearAction(gameState).immediateExecute)
|
||||
else initialSequencer
|
||||
|
||||
// Select chronicler style using a hash that doesn't cycle predictably
|
||||
val chroniclerStyle = ChronicleOptions.styles(
|
||||
(gameState.gameId, newDate.year, newDate.month.value).hashCode.abs % ChronicleOptions.styles.size
|
||||
)
|
||||
|
||||
// Add the main new round result
|
||||
val afterNewRoundSequencer = afterNewYearSequencer.withActionResult { currentState =>
|
||||
newRoundResult(
|
||||
gs = currentState,
|
||||
newRoundId = newRoundId,
|
||||
newDate = newDate,
|
||||
chroniclerStyle = chroniclerStyle
|
||||
newDate = newDate
|
||||
)
|
||||
}
|
||||
|
||||
@@ -109,8 +102,7 @@ case class NewRoundAction(
|
||||
|
||||
private def chronicleLlmRequests(
|
||||
newDate: Date,
|
||||
gs: GameState,
|
||||
chroniclerStyle: String
|
||||
gs: GameState
|
||||
): Vector[LlmRequestT] =
|
||||
if isChronicleMonth(newDate) then
|
||||
Vector(
|
||||
@@ -121,8 +113,7 @@ case class NewRoundAction(
|
||||
previous_entries = gs.chronicleEntries.map { entry =>
|
||||
ChronicleUpdatePreviousEntry(
|
||||
date = entry.date,
|
||||
generatedTextId = entry.generatedTextId,
|
||||
chroniclerStyle = entry.chroniclerStyle
|
||||
generatedTextId = entry.generatedTextId
|
||||
)
|
||||
},
|
||||
new_entries = ChronicleEventGenerator.eventTextEntries(
|
||||
@@ -130,8 +121,7 @@ case class NewRoundAction(
|
||||
since = gs.chronicleEntries.lastOption
|
||||
.map(_.date)
|
||||
.getOrElse(Date(year = 0, month = Date.Month.January))
|
||||
),
|
||||
chronicler_style = chroniclerStyle
|
||||
)
|
||||
)
|
||||
)
|
||||
else Vector()
|
||||
@@ -142,8 +132,7 @@ case class NewRoundAction(
|
||||
private def newRoundResult(
|
||||
gs: GameState,
|
||||
newRoundId: RoundId,
|
||||
newDate: Date,
|
||||
chroniclerStyle: String
|
||||
newDate: Date
|
||||
): ActionResultT = {
|
||||
case class Changes(
|
||||
changedProvince: ChangedProvinceC,
|
||||
@@ -265,7 +254,7 @@ case class NewRoundAction(
|
||||
)
|
||||
}.toVector
|
||||
|
||||
val llmRequests = chronicleLlmRequests(newDate, gs, chroniclerStyle)
|
||||
val llmRequests = chronicleLlmRequests(newDate, gs)
|
||||
|
||||
ActionResultC(
|
||||
actionResultType = NewRoundActionResultType,
|
||||
@@ -279,8 +268,7 @@ case class NewRoundAction(
|
||||
newChronicleEntry = llmRequests.headOption.map { request =>
|
||||
ChronicleEntry(
|
||||
date = newDate,
|
||||
generatedTextId = request.requestId,
|
||||
chroniclerStyle = Some(chroniclerStyle)
|
||||
generatedTextId = request.requestId
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
+3
-16
@@ -1,22 +1,9 @@
|
||||
package net.eagle0.eagle.library.actions.impl.common
|
||||
|
||||
import net.eagle0.eagle.internal.action_result.ActionResult
|
||||
import net.eagle0.eagle.internal.game_state.GameState as GameStateProto
|
||||
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
|
||||
case class ActionWithResultingState(
|
||||
actionResult: ActionResult,
|
||||
gameState: GameStateProto,
|
||||
private val precomputedScalaState: Option[GameState] = None
|
||||
) {
|
||||
|
||||
/**
|
||||
* Scala GameState, either precomputed (when we already had it) or lazily converted from proto.
|
||||
*
|
||||
* When results come from Scala-based actions via withNewResultsScala, the Scala state is preserved to avoid
|
||||
* unnecessary proto->Scala conversion. When loaded from disk/S3, conversion happens lazily on first access.
|
||||
*/
|
||||
lazy val scalaGameState: GameState =
|
||||
precomputedScalaState.getOrElse(GameStateConverter.fromProto(gameState))
|
||||
}
|
||||
gameState: GameState
|
||||
)
|
||||
|
||||
@@ -25,14 +25,11 @@ scala_library(
|
||||
],
|
||||
exports = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
+1
-1
@@ -1,8 +1,8 @@
|
||||
package net.eagle0.eagle.library.actions.llm_prompt_generators
|
||||
|
||||
import net.eagle0.eagle.client_text.{ClientTextStore, TextGenerationResult}
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.internal.generated_text_request.AllianceOfferMessage
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
|
||||
case class AllianceOfferMessagePromptGenerator(
|
||||
allianceOfferMessage: AllianceOfferMessage,
|
||||
|
||||
+1
-1
@@ -10,8 +10,8 @@ import net.eagle0.eagle.common.diplomacy_offer_status.DiplomacyOfferStatus.{
|
||||
DIPLOMACY_OFFER_STATUS_UNKNOWN,
|
||||
DIPLOMACY_OFFER_STATUS_UNRESOLVED
|
||||
}
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.internal.generated_text_request.AllianceOfferResolutionMessage
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
|
||||
case class AllianceResolutionMessagePromptGenerator(
|
||||
allianceOfferResolutionMessage: AllianceOfferResolutionMessage,
|
||||
|
||||
@@ -9,11 +9,11 @@ scala_library(
|
||||
":generator_utilities",
|
||||
":hero_description_generator",
|
||||
":llm_prompt_generator",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:client_text_store",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:text_generation_result",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -26,11 +26,11 @@ scala_library(
|
||||
":generator_utilities",
|
||||
":hero_description_generator",
|
||||
":llm_prompt_generator",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:client_text_store",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:text_generation_result",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -38,17 +38,11 @@ scala_library(
|
||||
name = "battalion_descriptions",
|
||||
srcs = ["BattalionDescriptions.scala"],
|
||||
visibility = ["//visibility:public"],
|
||||
exports = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:battalion_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:battalion_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library:eagle_internal_exception",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type_id",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -61,11 +55,11 @@ scala_library(
|
||||
":generator_utilities",
|
||||
":hero_description_generator",
|
||||
":llm_prompt_generator",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:client_text_store",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:text_generation_result",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -78,12 +72,12 @@ scala_library(
|
||||
":generator_utilities",
|
||||
":hero_description_generator",
|
||||
":llm_prompt_generator",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:client_text_store",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:text_generation_result",
|
||||
"//src/main/scala/net/eagle0/eagle/library:eagle_internal_exception",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -97,11 +91,11 @@ scala_library(
|
||||
":hero_description_generator",
|
||||
":hero_info_utilities",
|
||||
":llm_prompt_generator",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:client_text_store",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:text_generation_result",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -113,12 +107,12 @@ scala_library(
|
||||
":generator_utilities",
|
||||
":hero_description_generator",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:event_for_chronicle_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:client_text_store",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:text_generation_result",
|
||||
"//src/main/scala/net/eagle0/eagle/library:eagle_internal_exception",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -132,14 +126,14 @@ scala_library(
|
||||
":llm_prompt_generator",
|
||||
":map_description",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:event_for_chronicle_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
|
||||
"//src/main/scala/net/eagle0/common:simple_timed_logger",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:client_text_store",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:text_generation_result",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:chronicle_word_count",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero/backstory_version",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils:legacy_faction_utils",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -152,12 +146,10 @@ scala_library(
|
||||
":generator_utilities",
|
||||
":hero_description_generator",
|
||||
":llm_prompt_generator",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:client_text_store",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:text_generation_result",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/quest/concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/unaffiliated_hero",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -170,10 +162,10 @@ scala_library(
|
||||
":generator_utilities",
|
||||
":hero_description_generator",
|
||||
":llm_prompt_generator",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:client_text_store",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:text_generation_result",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -181,8 +173,8 @@ scala_library(
|
||||
name = "faction_info_utilities",
|
||||
srcs = ["FactionInfoUtilities.scala"],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -191,11 +183,11 @@ scala_library(
|
||||
srcs = ["GeneratorUtilities.scala"],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:client_text_store",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:text_generation_result",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -208,10 +200,10 @@ scala_library(
|
||||
":generator_utilities",
|
||||
":hero_description_generator",
|
||||
":llm_prompt_generator",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:client_text_store",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:text_generation_result",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -219,19 +211,15 @@ scala_library(
|
||||
name = "hero_description_generator",
|
||||
srcs = ["HeroDescriptionGenerator.scala"],
|
||||
visibility = ["//visibility:public"],
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero/backstory_version",
|
||||
],
|
||||
deps = [
|
||||
":faction_info_utilities",
|
||||
":generator_utilities",
|
||||
":hero_info_utilities",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:client_text_store",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:text_generation_result",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero/backstory_version",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -245,11 +233,11 @@ scala_library(
|
||||
":hero_description_generator",
|
||||
":hero_info_utilities",
|
||||
":llm_prompt_generator",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:client_text_store",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:text_generation_result",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -258,8 +246,9 @@ scala_library(
|
||||
srcs = ["HeroInfoUtilities.scala"],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -274,13 +263,13 @@ scala_library(
|
||||
":hero_description_generator",
|
||||
":llm_prompt_generator",
|
||||
":quest_ended_generator_utilities",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:client_text_store",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:text_generation_result",
|
||||
"//src/main/scala/net/eagle0/eagle/library:eagle_internal_exception",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/hero:profession",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -292,11 +281,11 @@ scala_library(
|
||||
":generator_utilities",
|
||||
":hero_description_generator",
|
||||
":llm_prompt_generator",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:client_text_store",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:text_generation_result",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -309,11 +298,11 @@ scala_library(
|
||||
":generator_utilities",
|
||||
":hero_description_generator",
|
||||
":llm_prompt_generator",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:client_text_store",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:text_generation_result",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -326,11 +315,11 @@ scala_library(
|
||||
":generator_utilities",
|
||||
":hero_description_generator",
|
||||
":llm_prompt_generator",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:client_text_store",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:text_generation_result",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -368,11 +357,11 @@ scala_library(
|
||||
":hero_description_generator",
|
||||
":hero_info_utilities",
|
||||
":llm_prompt_generator",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:client_text_store",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:text_generation_result",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -385,11 +374,11 @@ scala_library(
|
||||
":generator_utilities",
|
||||
":hero_description_generator",
|
||||
":llm_prompt_generator",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:client_text_store",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:text_generation_result",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -403,11 +392,11 @@ scala_library(
|
||||
":hero_description_generator",
|
||||
":hero_info_utilities",
|
||||
":llm_prompt_generator",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:client_text_store",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:text_generation_result",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -421,11 +410,11 @@ scala_library(
|
||||
":hero_description_generator",
|
||||
":hero_info_utilities",
|
||||
":llm_prompt_generator",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:client_text_store",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:text_generation_result",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -439,11 +428,11 @@ scala_library(
|
||||
":hero_description_generator",
|
||||
":hero_info_utilities",
|
||||
":llm_prompt_generator",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:client_text_store",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:text_generation_result",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -457,11 +446,11 @@ scala_library(
|
||||
":hero_description_generator",
|
||||
":hero_info_utilities",
|
||||
":llm_prompt_generator",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:client_text_store",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:text_generation_result",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -473,12 +462,12 @@ scala_library(
|
||||
":generator_utilities",
|
||||
":hero_description_generator",
|
||||
":llm_prompt_generator",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:client_text_store",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:text_generation_result",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/hero:profession",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -490,13 +479,12 @@ scala_library(
|
||||
":faction_info_utilities",
|
||||
":hero_description_generator",
|
||||
":hero_info_utilities",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:client_text_store",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:text_generation_result",
|
||||
"//src/main/scala/net/eagle0/eagle/library:eagle_internal_exception",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:battalion_type_id_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -510,11 +498,11 @@ scala_library(
|
||||
":hero_description_generator",
|
||||
":llm_prompt_generator",
|
||||
":quest_ended_generator_utilities",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:client_text_store",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:text_generation_result",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -528,11 +516,11 @@ scala_library(
|
||||
":hero_description_generator",
|
||||
":llm_prompt_generator",
|
||||
":quest_ended_generator_utilities",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:client_text_store",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:text_generation_result",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -545,11 +533,11 @@ scala_library(
|
||||
":generator_utilities",
|
||||
":hero_description_generator",
|
||||
":llm_prompt_generator",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:client_text_store",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:text_generation_result",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -562,11 +550,11 @@ scala_library(
|
||||
":generator_utilities",
|
||||
":hero_description_generator",
|
||||
":llm_prompt_generator",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:client_text_store",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:text_generation_result",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -578,11 +566,11 @@ scala_library(
|
||||
":generator_utilities",
|
||||
":hero_description_generator",
|
||||
":llm_prompt_generator",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:client_text_store",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:text_generation_result",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -597,12 +585,12 @@ scala_library(
|
||||
":hero_description_generator",
|
||||
":hero_info_utilities",
|
||||
":llm_prompt_generator",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:client_text_store",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:text_generation_result",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:beast_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -615,11 +603,11 @@ scala_library(
|
||||
":generator_utilities",
|
||||
":hero_description_generator",
|
||||
":llm_prompt_generator",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:client_text_store",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:text_generation_result",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -632,11 +620,11 @@ scala_library(
|
||||
":generator_utilities",
|
||||
":hero_description_generator",
|
||||
":llm_prompt_generator",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:client_text_store",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:text_generation_result",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -649,10 +637,10 @@ scala_library(
|
||||
":generator_utilities",
|
||||
":hero_description_generator",
|
||||
":llm_prompt_generator",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:client_text_store",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:text_generation_result",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
+9
-23
@@ -1,31 +1,20 @@
|
||||
package net.eagle0.eagle.library.actions.llm_prompt_generators
|
||||
|
||||
import net.eagle0.eagle.common.battalion_type.BattalionTypeId as ProtoBattalionTypeId
|
||||
import net.eagle0.eagle.common.battalion_type.BattalionTypeId
|
||||
import net.eagle0.eagle.internal.battalion.Battalion
|
||||
import net.eagle0.eagle.library.EagleInternalException
|
||||
import net.eagle0.eagle.model.state.battalion.BattalionT
|
||||
import net.eagle0.eagle.model.state.BattalionTypeId
|
||||
import net.eagle0.eagle.views.battalion_view.BattalionView
|
||||
|
||||
object BattalionDescriptions {
|
||||
def battalionTypeName(ofType: ProtoBattalionTypeId): String = ofType match {
|
||||
case ProtoBattalionTypeId.LIGHT_INFANTRY => "light infantry"
|
||||
case ProtoBattalionTypeId.HEAVY_INFANTRY => "heavy infantry"
|
||||
case ProtoBattalionTypeId.LIGHT_CAVALRY => "dragoons"
|
||||
case ProtoBattalionTypeId.HEAVY_CAVALRY => "knights"
|
||||
case ProtoBattalionTypeId.LONGBOWMEN => "longbowmen"
|
||||
case ProtoBattalionTypeId.UNDEAD => "zombies"
|
||||
case ProtoBattalionTypeId.Unrecognized(_) =>
|
||||
throw new EagleInternalException("Unknown battalion type")
|
||||
}
|
||||
|
||||
def battalionTypeName(ofType: BattalionTypeId): String = ofType match {
|
||||
case BattalionTypeId.LightInfantry => "light infantry"
|
||||
case BattalionTypeId.HeavyInfantry => "heavy infantry"
|
||||
case BattalionTypeId.LightCavalry => "dragoons"
|
||||
case BattalionTypeId.HeavyCavalry => "knights"
|
||||
case BattalionTypeId.Longbowmen => "longbowmen"
|
||||
case BattalionTypeId.Undead => "zombies"
|
||||
case BattalionTypeId.LIGHT_INFANTRY => "light infantry"
|
||||
case BattalionTypeId.HEAVY_INFANTRY => "heavy infantry"
|
||||
case BattalionTypeId.LIGHT_CAVALRY => "dragoons"
|
||||
case BattalionTypeId.HEAVY_CAVALRY => "knights"
|
||||
case BattalionTypeId.LONGBOWMEN => "longbowmen"
|
||||
case BattalionTypeId.UNDEAD => "zombies"
|
||||
case BattalionTypeId.Unrecognized(_) =>
|
||||
throw new EagleInternalException("Unknown battalion type")
|
||||
}
|
||||
|
||||
def descriptionForBattalion(bv: BattalionView) =
|
||||
@@ -33,7 +22,4 @@ object BattalionDescriptions {
|
||||
|
||||
def descriptionForBattalion(batt: Battalion) =
|
||||
s"a battalion of ${batt.size} ${battalionTypeName(batt.`type`)} known as \"${batt.name}\""
|
||||
|
||||
def descriptionForBattalion(batt: BattalionT): String =
|
||||
s"a battalion of ${batt.size} ${battalionTypeName(batt.typeId)} known as \"${batt.name}\""
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,8 +1,8 @@
|
||||
package net.eagle0.eagle.library.actions.llm_prompt_generators
|
||||
|
||||
import net.eagle0.eagle.client_text.{ClientTextStore, TextGenerationResult}
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.internal.generated_text_request.BreakAllianceMessage
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
|
||||
case class BreakAllianceMessagePromptGenerator(
|
||||
breakAllianceMessage: BreakAllianceMessage,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user