mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-29 00:35:41 +00:00
Compare commits
87
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96566f9322 | ||
|
|
0b5146f5b1 | ||
|
|
8cd2548f07 | ||
|
|
0551453536 | ||
|
|
ce357c612e | ||
|
|
f9e69b6f75 | ||
|
|
f43e914720 | ||
|
|
6c50c0da24 | ||
|
|
d265b76607 | ||
|
|
09a51e4280 | ||
|
|
5593effe69 | ||
|
|
44c268de93 | ||
|
|
0a40acb84d | ||
|
|
9603b497d2 | ||
|
|
0551dd0f13 | ||
|
|
45c4cf783d | ||
|
|
72c52e0b0d | ||
|
|
dffd569ed7 | ||
|
|
a1ffae91a8 | ||
|
|
45a32af435 | ||
|
|
1df8ec68e8 | ||
|
|
53d6e6f63d | ||
|
|
bc84cf6871 | ||
|
|
865a34d00a | ||
|
|
1a751cea6d | ||
|
|
5a8a343bcc | ||
|
|
5979dc7372 | ||
|
|
87474888f9 | ||
|
|
1b3697a40c | ||
|
|
a45b5dadd8 | ||
|
|
9f910bf849 | ||
|
|
c5466e38a8 | ||
|
|
958104b238 | ||
|
|
95e1d80e78 | ||
|
|
0dce9f47b0 | ||
|
|
6e788f4388 | ||
|
|
90d0918233 | ||
|
|
e6038927f1 | ||
|
|
acad796662 | ||
|
|
83c4ac7d38 | ||
|
|
7db07dc371 | ||
|
|
f1b843873a | ||
|
|
e8aefbb6ee | ||
|
|
1c51cc080f | ||
|
|
3946f2eb2d | ||
|
|
49bbdb1d2c | ||
|
|
bbdc30a4af | ||
|
|
19f5cf9e89 | ||
|
|
9033571110 | ||
|
|
e9e557f8f6 | ||
|
|
b32d252df3 | ||
|
|
d4723db2d1 | ||
|
|
7ce3cca731 | ||
|
|
24d21d402d | ||
|
|
e9fb1c5a87 | ||
|
|
b8b7d3a980 | ||
|
|
e503a8af9d | ||
|
|
9c4f46b6ca | ||
|
|
da453bb353 | ||
|
|
618cd18f44 | ||
|
|
429725c4e1 | ||
|
|
54bdefd75c | ||
|
|
98baf7ec66 | ||
|
|
fe4332c107 | ||
|
|
dfa18cef70 | ||
|
|
7845a54b5e | ||
|
|
3106fd9a40 | ||
|
|
19a14174c5 | ||
|
|
1f460a2777 | ||
|
|
12244fb1d4 | ||
|
|
b87910dcf5 | ||
|
|
214790c5e8 | ||
|
|
ffd4ff29d3 | ||
|
|
63e0334ef8 | ||
|
|
4aae50d72c | ||
|
|
0bd6e5b5d2 | ||
|
|
0560f15d1c | ||
|
|
428b91f337 | ||
|
|
d489857692 | ||
|
|
93e6771ded | ||
|
|
430b16bc86 | ||
|
|
8fb518ccad | ||
|
|
bfd4fcebbf | ||
|
|
7c312eb2ef | ||
|
|
209fab050b | ||
|
|
0bc0cbc738 | ||
|
|
48b561a999 |
@@ -37,3 +37,4 @@ scripts/refresh_name_layers/refresh_name_layers.zip
|
||||
.metals
|
||||
api_keys.txt
|
||||
|
||||
src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/Packages/com.unity.dedicated-server/
|
||||
|
||||
@@ -32,8 +32,9 @@ repos:
|
||||
- id: gazelle
|
||||
name: gazelle
|
||||
language: system
|
||||
entry: bazel run //:gazelle
|
||||
entry: ./scripts/pre-commit-gazelle.sh
|
||||
files: '(\.go|\.proto|BUILD\.bazel|BUILD|WORKSPACE|WORKSPACE\.bazel|\.bzl)$'
|
||||
pass_filenames: false
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: update-action-result-types
|
||||
|
||||
@@ -85,6 +85,16 @@ bazel run gazelle # Update Go build files
|
||||
./scripts/updateActionResultTypes.sh # Update protocol buffer mappings
|
||||
```
|
||||
|
||||
### Pre-Commit Checklist
|
||||
|
||||
**MANDATORY: Before running `git commit`, verify:**
|
||||
|
||||
1. **If you modified any BUILD.bazel file:** Run `bazel run gazelle` and stage any changes it makes
|
||||
2. **If you modified C++ or C# files:** Run `clang-format -i` on the modified files
|
||||
3. **If you modified Scala files:** scalafmt will run automatically via pre-commit hook
|
||||
|
||||
The pre-commit hook runs gazelle but only checks if it succeeds - it does NOT verify the BUILD files are in canonical format. The `gazelle_test` will fail if deps are not alphabetically sorted. **Always run gazelle manually after BUILD file changes.**
|
||||
|
||||
### Code Formatting
|
||||
|
||||
```bash
|
||||
@@ -244,6 +254,32 @@ done
|
||||
- **Always test performance changes** - what seems like an optimization may sometimes have unexpected overhead or
|
||||
behavior changes.
|
||||
|
||||
## Troubleshooting Scala Build Errors
|
||||
|
||||
### MissingType Errors
|
||||
|
||||
When you see errors like:
|
||||
```
|
||||
dotty.tools.dotc.core.MissingType: Cannot resolve reference to type net.eagle0.eagle.internal.game_state.type.GameState
|
||||
```
|
||||
|
||||
**This is NOT a Scala compiler crash.** This is a missing dependency in BUILD.bazel.
|
||||
|
||||
**How to fix:**
|
||||
1. Identify the missing type from the error message (e.g., `game_state.GameState`)
|
||||
2. Find the Bazel target that provides this type (e.g., `//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto`)
|
||||
3. Add it to the `deps` of the failing target
|
||||
4. If the type appears in a public method signature, also add it to `exports` so downstream targets can see it
|
||||
|
||||
**Common pattern:** When adding a method to a class that takes or returns a proto type, the proto dependency often needs to be added to both `deps` AND `exports`.
|
||||
|
||||
### Bazel Clean
|
||||
|
||||
**NEVER run `bazel clean` without asking first.** It rarely fixes actual issues and wastes significant rebuild time. The issues that seem like they need `bazel clean` are usually:
|
||||
- Missing imports in Scala code
|
||||
- Missing dependencies in BUILD.bazel
|
||||
- Missing exports for types used in public signatures
|
||||
|
||||
## Game Content
|
||||
|
||||
**Maps:** `.e0mj` files in `/src/main/resources/net/eagle0/shardok/maps/`
|
||||
|
||||
@@ -76,6 +76,7 @@ use_repo(
|
||||
"com_github_aws_aws_sdk_go_v2_config",
|
||||
"com_github_aws_aws_sdk_go_v2_credentials",
|
||||
"com_github_aws_aws_sdk_go_v2_service_s3",
|
||||
"org_golang_google_grpc",
|
||||
"org_golang_google_protobuf",
|
||||
)
|
||||
|
||||
|
||||
+1
-2
@@ -1,2 +1 @@
|
||||
|
||||
UNITY_VERSION='6000.2.7f2'
|
||||
UNITY_VERSION='6000.3.0f1'
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,215 @@
|
||||
# Deproto Migration Plan
|
||||
|
||||
## Vision
|
||||
|
||||
**Protocol buffers should only be used at the edges** — for network serialization (gRPC) and disk persistence. Inside the Eagle game engine, all logic should operate on native Scala models.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ GRPC BOUNDARY │
|
||||
│ EagleServiceImpl.scala ←→ Proto Messages ←→ Unity Client │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
GameStateConverter
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ SCALA ENGINE │
|
||||
│ │
|
||||
│ GameStateC ───→ Actions ───→ ActionResultT ───→ New GameStateC │
|
||||
│ ↑ │ │
|
||||
│ │ (Pure Scala models) │ │
|
||||
│ └───────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ HeroC, FactionC, ProvinceC, BattalionC, ArmyC, etc. │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
GameStateConverter
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ PERSISTENCE BOUNDARY │
|
||||
│ GameHistory.scala ←→ Proto Messages ←→ File/Database │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Current State
|
||||
|
||||
### Completed Phases
|
||||
|
||||
| Phase | Status | Summary |
|
||||
|-------|--------|---------|
|
||||
| Phase 1: GameStateC | **Complete** | Scala `GameState` model with 22 fields |
|
||||
| Phase 2: EngineImpl | **Complete** | Holds Scala `GameState` internally |
|
||||
| Phase 3: GameHistory | **Complete** | `stateAfter` returns Scala GameState |
|
||||
| Phase 4: ActionResultT | **Complete** | All 59 actions return `ActionResultT` |
|
||||
| Phase 5: Action Base Classes | **Complete** | All `RandomSequentialResultsAction` and `DeterministicSingleResultAction` converted to T-type base classes |
|
||||
| Phase 5b: Base Class Cleanup | **Complete** | `RandomSequentialResultsAction` and `DeterministicSingleResultAction` deleted |
|
||||
|
||||
### Current Architecture
|
||||
|
||||
**ActionResultT Production (100% Complete):**
|
||||
- All actions produce `ActionResultT`
|
||||
- Conversion to `ActionResultProto` happens via `ActionResultProtoConverter.toProto()`
|
||||
- No direct `ActionResultProto` construction outside the converter
|
||||
|
||||
**ActionResultProto Consumption (Next Target):**
|
||||
- `ActionResultProtoApplierImpl` - applies proto results to proto GameState
|
||||
- `RoundPhaseAdvancer` - calls converter, passes protos to applier
|
||||
- `InMemoryHistory` / `PersistedHistory` - stores proto results
|
||||
- Service layer (`GameController`, `GamesManager`, etc.) - uses proto for client communication
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Migrate to ActionResultT Consumers
|
||||
|
||||
### Objective
|
||||
|
||||
Eliminate internal consumption of `ActionResultProto`. Everything inside the engine should work with `ActionResultT`.
|
||||
|
||||
### Current Flow (Proto-Heavy)
|
||||
```
|
||||
Action.execute()
|
||||
→ ActionResultT
|
||||
→ ActionResultProtoConverter.toProto()
|
||||
→ ActionResultProto
|
||||
→ ActionResultProtoApplierImpl.applyActionResults()
|
||||
→ GameStateProto
|
||||
→ GameStateConverter.fromProto()
|
||||
→ GameStateC
|
||||
```
|
||||
|
||||
### Target Flow (T-Types Throughout)
|
||||
```
|
||||
Action.execute()
|
||||
→ ActionResultT
|
||||
→ ActionResultTApplier.applyActionResults()
|
||||
→ GameStateC
|
||||
|
||||
(Proto conversion only at boundaries)
|
||||
```
|
||||
|
||||
### Key Files to Convert
|
||||
|
||||
**Tier 1 - Core Applier:**
|
||||
```
|
||||
src/main/scala/net/eagle0/eagle/library/actions/applier/ActionResultProtoApplierImpl.scala
|
||||
```
|
||||
Create `ActionResultApplier` that applies `ActionResultT` directly to Scala `GameState`.
|
||||
|
||||
**Tier 2 - RoundPhaseAdvancer:**
|
||||
```
|
||||
src/main/scala/net/eagle0/eagle/library/RoundPhaseAdvancer.scala
|
||||
```
|
||||
Currently has ~20 calls to `ActionResultProtoConverter.toProto()`. After Tier 1, these become unnecessary.
|
||||
|
||||
**Tier 3 - Sequencers:**
|
||||
```
|
||||
src/main/scala/net/eagle0/eagle/library/actions/impl/common/RandomStateTSequencer.scala
|
||||
src/main/scala/net/eagle0/eagle/library/actions/impl/common/RandomStateProtoSequencer.scala
|
||||
```
|
||||
Modify `RandomStateTSequencer` to thread Scala `GameState` throughout (currently converts to proto internally). Then evaluate whether `RandomStateProtoSequencer` is still needed at all.
|
||||
|
||||
**Tier 4 - History APIs:**
|
||||
```
|
||||
src/main/scala/net/eagle0/eagle/service/InMemoryHistory.scala
|
||||
src/main/scala/net/eagle0/eagle/service/PersistedHistory.scala
|
||||
```
|
||||
Change APIs to vend Scala `GameState` and `ActionResultT` instead of proto versions. `PersistedHistory` converts to proto internally for disk persistence; `InMemoryHistory` doesn't need proto at all.
|
||||
|
||||
### ActionResultProto Consumer Inventory
|
||||
|
||||
| File | Usage | Target |
|
||||
|------|-------|--------|
|
||||
| `ActionResultTApplierImpl.scala` | Converts T→Proto, delegates to proto applier | Replace with `ActionResultApplier` |
|
||||
| `RoundPhaseAdvancer.scala` | ~20 converter calls | Eliminate after applier conversion |
|
||||
| `RandomStateTSequencer.scala` | Converts T→Proto internally | Thread Scala GameState throughout |
|
||||
| `RandomStateProtoSequencer.scala` | Returns `Vector[ActionResultProto]` | Evaluate if still needed |
|
||||
| `VigorXPApplier.scala` | Wraps proto results | Convert to work with T |
|
||||
| `ResolveBattleAction.scala` | 2 converter calls | Convert after dependencies |
|
||||
| `PerformForcedTurnBackAction.scala` | 1 converter call | Convert after dependencies |
|
||||
| `EndFreeForAllDecisionPhaseAction.scala` | ~~fromProtoState~~ | **Complete** - now takes Scala GameState |
|
||||
| `EndBattleRequestPhaseAction.scala` | ~~fromProtoState~~ | **Complete** - now takes Scala GameState |
|
||||
| `EndDefenseDecisionPhaseAction.scala` | ~~fromProtoState~~ | **Complete** - now takes Scala GameState |
|
||||
| `EndPleaseRecruitMePhaseAction.scala` | `fromProtoState` | **BLOCKED** - Scala 3.7.2 compiler crash (lambdaLift phase) |
|
||||
| `InMemoryHistory.scala` | Stores proto results | Vend Scala types, remove proto entirely |
|
||||
| `PersistedHistory.scala` | Stores proto results | Vend Scala types, convert internally for disk |
|
||||
| `GameController.scala` | Uses proto for client communication | Keep proto (gRPC boundary) |
|
||||
|
||||
### Estimated Effort
|
||||
|
||||
| Component | Lines | Complexity |
|
||||
|-----------|-------|------------|
|
||||
| `ActionResultApplier` | ~900 | High (port of proto applier) |
|
||||
| `RandomStateTSequencer` refactor | ~150 | Medium |
|
||||
| `RoundPhaseAdvancer` updates | ~100 | Medium |
|
||||
| History API updates | ~100 | Low |
|
||||
| Action/utility updates | ~200 | Low |
|
||||
| **Total** | **~1450** | |
|
||||
|
||||
### Validation
|
||||
- [ ] `ActionResultApplier` created and tested
|
||||
- [ ] `RandomStateTSequencer` threads Scala GameState throughout
|
||||
- [ ] `RoundPhaseAdvancer` uses T-types internally
|
||||
- [ ] History APIs vend Scala types
|
||||
- [ ] No `ActionResultProtoConverter.toProto()` calls except at persistence/gRPC boundaries
|
||||
- [ ] All tests pass
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: Clean Up Legacy Utilities
|
||||
|
||||
### Objective
|
||||
Remove remaining direct proto imports from utility classes.
|
||||
|
||||
### Files to Modify
|
||||
|
||||
| File | Status |
|
||||
|------|--------|
|
||||
| `LegacyFactionUtils.scala` | Replace proto imports with `FactionT` |
|
||||
| `LegacyUnaffiliatedHeroUtils.scala` | Replace proto imports with Scala models |
|
||||
| `BattalionTypeLoader.scala` | Keep proto for file loading, convert immediately after |
|
||||
| `BeastUtils.scala` | **Complete** - now uses Scala `BeastInfo` only |
|
||||
|
||||
---
|
||||
|
||||
## Phase 8: Verify Boundaries
|
||||
|
||||
### Objective
|
||||
Confirm protos are used correctly at boundaries — and ONLY there.
|
||||
|
||||
### Expected Proto Usage (Keep)
|
||||
- `EagleServiceImpl.scala` - gRPC boundary
|
||||
- `InMemoryHistory.scala` / `PersistedHistory.scala` - Persistence boundary
|
||||
- `*Converter.scala` - Explicit conversion utilities
|
||||
- `*Loader.scala` - File loading utilities
|
||||
|
||||
### Expected No Proto Usage (Verify)
|
||||
- `/library/actions/impl/` - Pure Scala models
|
||||
- `/library/util/` - Pure Scala models (except loaders)
|
||||
- `/model/state/` - Pure Scala models
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Persistence Format**: Currently game state is persisted as proto. Should we keep proto for persistence (good for schema evolution) or switch to a different format?
|
||||
|
||||
2. **Shardok Integration**: `ResolveBattleAction` communicates with Shardok. Should the Shardok interface use protos (external service) or Scala models?
|
||||
|
||||
3. **View Generation**: `GameStateViewDiffer` works with view protos for client updates. Should views also have Scala models, or is proto acceptable for client-facing projections?
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
### Code Quality
|
||||
- [ ] Zero proto imports in `/library/actions/` (except boundaries)
|
||||
- [ ] Zero proto imports in `/library/` utilities (except loaders)
|
||||
- [ ] `GameStateT` used throughout engine internals
|
||||
- [ ] Proto usage limited to: `EagleServiceImpl`, loaders, converters, persistence
|
||||
|
||||
### Architecture
|
||||
- [ ] Clear separation: Scala models (internal) vs Proto (boundaries)
|
||||
- [ ] Converters as the only bridge between domains
|
||||
- [ ] No "proto creep" into business logic
|
||||
Binary file not shown.
@@ -9,6 +9,7 @@ require (
|
||||
github.com/aws/aws-sdk-go-v2/config v1.28.10
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.51
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.72.2
|
||||
google.golang.org/grpc v1.68.0
|
||||
google.golang.org/protobuf v1.36.3
|
||||
)
|
||||
|
||||
|
||||
@@ -40,6 +40,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=
|
||||
google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=
|
||||
google.golang.org/protobuf v1.26.0-rc.1 h1:7QnIQpGRHE5RnLKnESfDoxm2dTapTZua5a0kS0A+VXQ=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
curl -L "https://docs.google.com/spreadsheets/d/1pv-WMXReccddPwev_YG9IXEGznuGHrYjNNEZ0Rb-ZhM/export?gid=0&format=tsv" > src/main/resources/net/eagle0/shardok/settings.tsv
|
||||
curl -L "https://docs.google.com/spreadsheets/d/1p6I5nUMcoAPHIcqikVgbBCFVnqN9dpOEVClbS_wOI7M/export?gid=0&format=tsv" > src/main/resources/net/eagle0/eagle/settings.tsv
|
||||
curl -L "https://docs.google.com/spreadsheets/d/1pv-WMXReccddPwev_YG9IXEGznuGHrYjNNEZ0Rb-ZhM/export?gid=0&format=tsv" | tr -d '\r' > src/main/resources/net/eagle0/shardok/settings.tsv
|
||||
curl -L "https://docs.google.com/spreadsheets/d/1p6I5nUMcoAPHIcqikVgbBCFVnqN9dpOEVClbS_wOI7M/export?gid=0&format=tsv" | tr -d '\r' > src/main/resources/net/eagle0/eagle/settings.tsv
|
||||
|
||||
bazel run //src/main/go/net/eagle0/build/settings_generator:settings_generator -- \
|
||||
${PWD}/src/main/resources/net/eagle0/eagle/settings.tsv \
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
curl -L "https://docs.google.com/spreadsheets/d/1DHEsiv4cY4gE6AX3sVH82K__mpBD1aznIYCQwQxA_F0/export?gid=0&format=tsv" > /tmp/names.tsv
|
||||
curl -L "https://docs.google.com/spreadsheets/d/1DHEsiv4cY4gE6AX3sVH82K__mpBD1aznIYCQwQxA_F0/export?gid=0&format=tsv" | tr -d '\r' > /tmp/names.tsv
|
||||
bazel run //src/main/scala/net/eagle0/util:name_list_checker -- /tmp/names.tsv > src/main/resources/net/eagle0/names.tsv
|
||||
bazel run //src/main/scala/net/eagle0/util:name_list_json_maker -- /tmp/names.tsv > src/main/resources/net/eagle0/names.json
|
||||
curl -L "https://docs.google.com/spreadsheets/d/1NhvG73HKyVE36yGpkV2oJiSIXoNqQOYTr5ArLnucYL0/export?gid=0&format=tsv" > src/main/resources/net/eagle0/shardok/battalionTypes.tsv
|
||||
curl -L "https://docs.google.com/spreadsheets/d/1pNWiyxIks2wJ1v7jRLFD24zrKHG2AfhC-nkWmQKQGN4/export?gid=0&format=tsv" > src/main/resources/net/eagle0/eagle/heroes.tsv
|
||||
curl -L "https://docs.google.com/spreadsheets/d/1RUguq5eAQprsZwOOqiCc-1dg4Urc_6iJ6awZsFU4MeI/export?gid=0&format=tsv" > src/main/resources/net/eagle0/eagle/beasts.tsv
|
||||
curl -L "https://docs.google.com/spreadsheets/d/1NhvG73HKyVE36yGpkV2oJiSIXoNqQOYTr5ArLnucYL0/export?gid=0&format=tsv" | tr -d '\r' > src/main/resources/net/eagle0/shardok/battalionTypes.tsv
|
||||
curl -L "https://docs.google.com/spreadsheets/d/1pNWiyxIks2wJ1v7jRLFD24zrKHG2AfhC-nkWmQKQGN4/export?gid=0&format=tsv" | tr -d '\r' > src/main/resources/net/eagle0/eagle/heroes.tsv
|
||||
curl -L "https://docs.google.com/spreadsheets/d/1RUguq5eAQprsZwOOqiCc-1dg4Urc_6iJ6awZsFU4MeI/export?gid=0&format=tsv" | tr -d '\r' > src/main/resources/net/eagle0/eagle/beasts.tsv
|
||||
#curl -L "https://docs.google.com/spreadsheets/d/1Z-60cJ_N1IasvqpVb5awKEkIYznEeR2IZSdli47oW88/export?gid=0&format=tsv" > src/main/resources/net/eagle0/eagle/province_map.tsv
|
||||
|
||||
${PWD}/scripts/dlSettings.sh
|
||||
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
# Pre-commit hook wrapper for gazelle that fails if files are modified.
|
||||
# This ensures BUILD files are in canonical format before committing.
|
||||
|
||||
set -e
|
||||
|
||||
# Run gazelle
|
||||
bazel run //:gazelle 2>/dev/null
|
||||
|
||||
# Check if any BUILD files were modified
|
||||
if ! git diff --quiet -- '*.bazel' '**/BUILD' 'WORKSPACE*'; then
|
||||
echo ""
|
||||
echo "ERROR: gazelle modified BUILD files. Please stage the changes and retry:"
|
||||
echo ""
|
||||
git diff --name-only -- '*.bazel' '**/BUILD' 'WORKSPACE*'
|
||||
echo ""
|
||||
echo "Run: git add -u && git commit"
|
||||
exit 1
|
||||
fi
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "AbstractMCTSAI.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <fstream>
|
||||
#include <future>
|
||||
#include <iomanip>
|
||||
@@ -84,7 +85,7 @@ auto AbstractMCTSAI::Search(
|
||||
LogSearchResults(rootNode.get(), bestChild, result);
|
||||
}
|
||||
|
||||
// Dump tree if requested
|
||||
// Dump tree if explicitly requested via config
|
||||
if (!config_.debugDumpPath.empty()) { DumpTreeToFile(rootNode.get(), config_.debugDumpPath); }
|
||||
|
||||
return result;
|
||||
@@ -129,6 +130,37 @@ auto AbstractMCTSAI::BuildMCTSTree(
|
||||
// Initialize action counter
|
||||
root->totalActions = rootActions.size();
|
||||
|
||||
// CRITICAL: Do at least one expansion before entering the time-bounded loop.
|
||||
// This ensures we always have at least one child to return, even if the deadline
|
||||
// has already passed (e.g., due to debugger pause, system load, etc.)
|
||||
{
|
||||
auto* selected = MCTSSelection(root.get());
|
||||
const bool selectedIsRoot = (selected == root.get());
|
||||
const size_t childrenBeforeExpansion = root->children.size();
|
||||
|
||||
if (selected) {
|
||||
auto* expanded = MCTSExpansion(selected, engine);
|
||||
const double reward =
|
||||
MCTSSimulation(engine, *expanded->gameState, playerId_, expanded->playerFlips);
|
||||
MCTSBackpropagation(expanded, reward, config_.backpropagationPolicy);
|
||||
}
|
||||
|
||||
// Verify we actually have at least one child after the initial expansion
|
||||
if (root->children.empty()) {
|
||||
throw MCTSInternalError(
|
||||
"MCTS BuildMCTSTree: Initial expansion failed to produce any children. "
|
||||
"totalActions=" +
|
||||
std::to_string(root->totalActions) +
|
||||
", selected=" + (selected ? "non-null" : "null") +
|
||||
", selectedIsRoot=" + (selectedIsRoot ? "true" : "false") +
|
||||
", childrenBefore=" + std::to_string(childrenBeforeExpansion) +
|
||||
", childrenAfter=" + std::to_string(root->children.size()) +
|
||||
", root->CanExpand()=" + (root->CanExpand() ? "true" : "false") +
|
||||
", root->nextUntriedActionIndex=" +
|
||||
std::to_string(root->nextUntriedActionIndex));
|
||||
}
|
||||
}
|
||||
|
||||
std::atomic<int> iterations{0};
|
||||
|
||||
if (config_.useMultithreading && config_.numThreads > 1) {
|
||||
@@ -205,10 +237,19 @@ auto AbstractMCTSAI::BuildMCTSTree(
|
||||
auto AbstractMCTSAI::MCTSSelection(MCTSNode* root) const -> MCTSNode* {
|
||||
MCTSNode* current = root;
|
||||
|
||||
while (!current->isTerminal && current->depth < config_.maxTreeDepth) {
|
||||
while (current->depth < config_.maxTreeDepth) {
|
||||
// Check expansion FIRST - allows expanding "terminal" nodes that still have
|
||||
// untried actions (e.g., final round where we need to pick an action)
|
||||
if (current->CanExpand()) {
|
||||
return current; // Node has untried actions/outcomes
|
||||
} else if (!current->children.empty()) {
|
||||
}
|
||||
|
||||
// Only after expansion check: stop if terminal and fully expanded
|
||||
if (current->isTerminal) {
|
||||
break; // Terminal and no more actions to try
|
||||
}
|
||||
|
||||
if (!current->children.empty()) {
|
||||
// Choose child based on node type
|
||||
if (current->IsChanceNode()) {
|
||||
// Chance nodes: select outcome proportional to probability
|
||||
@@ -228,7 +269,9 @@ auto AbstractMCTSAI::MCTSSelection(MCTSNode* root) const -> MCTSNode* {
|
||||
|
||||
auto AbstractMCTSAI::MCTSExpansion(MCTSNode* node, const MCTSGameEngine& engine) const
|
||||
-> MCTSNode* {
|
||||
if (!node->CanExpand() || node->isTerminal) {
|
||||
// Only skip if we truly can't expand. Allow expansion even if "terminal" as long as
|
||||
// there are untried actions (e.g., final round where we need to pick an action).
|
||||
if (!node->CanExpand()) {
|
||||
return node; // Nothing to expand
|
||||
}
|
||||
|
||||
@@ -317,8 +360,11 @@ auto AbstractMCTSAI::MCTSExpansion(MCTSNode* node, const MCTSGameEngine& engine)
|
||||
// Normalize by total probability of expanded outcomes
|
||||
if (totalProbability > 0.0) {
|
||||
node->immediateScore = expectedImmediate / totalProbability;
|
||||
// Also update lookaheadScore if this is a fresh expansion
|
||||
if (node->children.size() == 1) { node->lookaheadScore = node->immediateScore; }
|
||||
// CRITICAL: Always update lookaheadScore to the expected value.
|
||||
// Without this, chance nodes keep their initial lookaheadScore from the parent
|
||||
// state (before the action), while regular actions use the child state (after).
|
||||
// This gives chance nodes an unfair initial UCB advantage.
|
||||
node->lookaheadScore = node->immediateScore;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -385,8 +431,14 @@ auto AbstractMCTSAI::MCTSExpansion(MCTSNode* node, const MCTSGameEngine& engine)
|
||||
chanceNode->parent = node;
|
||||
node->children.push_back(std::move(chanceNode));
|
||||
|
||||
// Return the chance node for further expansion
|
||||
return node->children.back().get();
|
||||
// CRITICAL: Immediately expand the first outcome and return that instead.
|
||||
// If we returned the chance node itself, MCTSSimulation would run on the parent state
|
||||
// (since chance nodes have parent's gameState), which is wrong. We need to simulate
|
||||
// from an actual outcome state.
|
||||
//
|
||||
// Note: This recursion is bounded because outcome children are decision nodes,
|
||||
// not chance nodes, so the recursion goes exactly one level deep.
|
||||
return MCTSExpansion(node->children.back().get(), engine);
|
||||
}
|
||||
|
||||
// Regular (non-chance) action: create decision node directly
|
||||
|
||||
@@ -16,28 +16,39 @@
|
||||
namespace shardok {
|
||||
namespace mcts {
|
||||
|
||||
// Information about binary chance outcomes (success/failure)
|
||||
struct BinaryOutcomeInfo {
|
||||
double successProbability; // Probability of success (0.0 to 1.0)
|
||||
// Information about chance outcomes (supports both binary and multi-outcome)
|
||||
struct ChanceOutcomeInfo {
|
||||
std::vector<double> probabilities; // Probability of each outcome (must sum to 1.0)
|
||||
std::vector<double> rolls; // Roll values for each outcome
|
||||
|
||||
// Returns extreme roll values that guarantee success/failure against any threshold.
|
||||
//
|
||||
// NOTE: These are not truly "representative" rolls - they guarantee outcomes rather
|
||||
// than simulating typical rolls. Some commands have variance beyond success/failure
|
||||
// (e.g., BUILD_BRIDGE quality depends on roll margin). This simplification ignores
|
||||
// that variance. If outcome quality matters for AI decisions, we may need to revisit
|
||||
// this approach with actual representative rolls based on the command's threshold.
|
||||
[[nodiscard]] static std::vector<double> getRepresentativeRolls() {
|
||||
// Factory for binary success/failure outcomes (e.g., START_FIRE)
|
||||
[[nodiscard]] static ChanceOutcomeInfo binary(double successProbability) {
|
||||
// -100: triggers open-ended low sequence, succeeds against any threshold
|
||||
// 150: triggers open-ended high sequence, fails against any threshold
|
||||
return {-100.0, 150.0};
|
||||
return {{successProbability, 1.0 - successProbability}, {-100.0, 150.0}};
|
||||
}
|
||||
|
||||
[[nodiscard]] std::vector<double> getProbabilities() const {
|
||||
return {successProbability, 1.0 - successProbability};
|
||||
// Factory for multi-outcome with fixed seeds (e.g., END_TURN)
|
||||
// Uses uniformly distributed roll values to sample different random outcomes
|
||||
[[nodiscard]] static ChanceOutcomeInfo multiOutcome(int numOutcomes) {
|
||||
std::vector<double> probs(numOutcomes, 1.0 / numOutcomes);
|
||||
std::vector<double> rollValues;
|
||||
rollValues.reserve(numOutcomes);
|
||||
// Spread rolls across the percentile range: 10, 30, 50, 70, 90 for 5 outcomes
|
||||
for (int i = 0; i < numOutcomes; ++i) {
|
||||
rollValues.push_back(10.0 + (80.0 * i) / (numOutcomes - 1));
|
||||
}
|
||||
return {probs, rollValues};
|
||||
}
|
||||
|
||||
[[nodiscard]] const std::vector<double>& getRepresentativeRolls() const { return rolls; }
|
||||
|
||||
[[nodiscard]] const std::vector<double>& getProbabilities() const { return probabilities; }
|
||||
};
|
||||
|
||||
// Backward compatibility alias
|
||||
using BinaryOutcomeInfo = ChanceOutcomeInfo;
|
||||
|
||||
// Abstract interface for game engines
|
||||
class MCTSGameEngine {
|
||||
public:
|
||||
|
||||
@@ -32,6 +32,7 @@ cc_library(
|
||||
"//src/main/cpp/net/eagle0/shardok/library:engine",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:game_state_w",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/settings:game_settings",
|
||||
"//src/main/protobuf/net/eagle0/shardok/common:command_type_cc_proto",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -67,8 +67,22 @@ bool ShardokAction::equals(const MCTSAction& other) const {
|
||||
}
|
||||
|
||||
bool ShardokAction::requiresChanceNode() const {
|
||||
// Actions with probabilistic outcomes require chance nodes
|
||||
return hasOdds_;
|
||||
// Actions with probabilistic outcomes require chance nodes:
|
||||
// 1. Binary success/failure actions (hasOdds_): START_FIRE, FEAR, etc.
|
||||
// 2. END_TURN: random effects (fire spread, weather changes)
|
||||
// 3. Combat actions: roll affects damage dealt (MELEE, ARCHERY, CHARGE, DUEL)
|
||||
if (hasOdds_) { return true; }
|
||||
|
||||
using namespace net::eagle0::shardok::common;
|
||||
switch (type_) {
|
||||
case END_TURN_COMMAND:
|
||||
case MELEE_COMMAND:
|
||||
case ARCHERY_COMMAND:
|
||||
case CHARGE_COMMAND:
|
||||
case CHALLENGE_DUEL_COMMAND:
|
||||
case REDUCE_COMMAND: return true;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace shardok::mcts
|
||||
@@ -140,7 +140,7 @@ std::unique_ptr<MCTSGameState> ShardokGameEngine::applyAction(
|
||||
|
||||
engine->PostCommand(currentPlayer, shardokAction->getIndex(), randomGen);
|
||||
|
||||
// Create and return the new state (don't cache the mutated engine)
|
||||
// Create and return the new state
|
||||
auto newState = std::make_unique<ShardokGameState>(
|
||||
engine->GetCurrentGameState(),
|
||||
scoreCalculator_,
|
||||
@@ -152,6 +152,11 @@ std::unique_ptr<MCTSGameState> ShardokGameEngine::applyAction(
|
||||
*alCache_,
|
||||
criticalTileCoords_);
|
||||
|
||||
// Cache the engine on the new state so score() can use it for END_TURN normalization
|
||||
// The engine's command list may be stale after the action was applied, but that's OK -
|
||||
// we'll refresh it when we call GetAvailableCommandsForAIPlayer() in score()
|
||||
newState->setCachedEngine(engine);
|
||||
|
||||
// Don't pre-compute hash - let it be computed lazily on first use
|
||||
// Many states (especially in simulation) never need their hash computed
|
||||
return newState;
|
||||
@@ -538,7 +543,7 @@ void ShardokGameEngine::resetCacheStatistics() {
|
||||
timeInLegalActionsComputation_.store(0, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
BinaryOutcomeInfo ShardokGameEngine::getBinaryOutcomeInfo(
|
||||
ChanceOutcomeInfo ShardokGameEngine::getBinaryOutcomeInfo(
|
||||
const MCTSGameState& state,
|
||||
const MCTSAction& action) const {
|
||||
const auto* shardokState = dynamic_cast<const ShardokGameState*>(&state);
|
||||
@@ -548,6 +553,33 @@ BinaryOutcomeInfo ShardokGameEngine::getBinaryOutcomeInfo(
|
||||
throw ShardokInternalErrorException("Invalid state or action type in getBinaryOutcomeInfo");
|
||||
}
|
||||
|
||||
// Check for multi-outcome commands (roll affects outcome quality, not just success/failure)
|
||||
// These use multiOutcome() with fixed seeds to sample the range of possible results
|
||||
using namespace net::eagle0::shardok::common;
|
||||
const auto commandType = static_cast<CommandType>(shardokAction->getType());
|
||||
|
||||
switch (commandType) {
|
||||
case END_TURN_COMMAND:
|
||||
// END_TURN has random effects (fire spread, weather changes)
|
||||
return ChanceOutcomeInfo::multiOutcome(5);
|
||||
|
||||
case MELEE_COMMAND:
|
||||
case ARCHERY_COMMAND:
|
||||
case CHARGE_COMMAND:
|
||||
case REDUCE_COMMAND:
|
||||
// Combat/siege commands: OpenEndedPercentile roll affects damage dealt
|
||||
// Use 5 outcomes to sample the roll distribution
|
||||
return ChanceOutcomeInfo::multiOutcome(5);
|
||||
|
||||
case CHALLENGE_DUEL_COMMAND:
|
||||
// Duels have multiple combat rounds with rolls, so outcomes vary significantly
|
||||
return ChanceOutcomeInfo::multiOutcome(5);
|
||||
|
||||
default:
|
||||
// Continue to binary outcome handling below
|
||||
break;
|
||||
}
|
||||
|
||||
const auto currentPlayer = static_cast<PlayerId>(state.currentPlayerId());
|
||||
|
||||
// Get or create the engine for this state
|
||||
@@ -577,7 +609,7 @@ BinaryOutcomeInfo ShardokGameEngine::getBinaryOutcomeInfo(
|
||||
|
||||
const auto& descriptor = descriptors->at(actionIndex);
|
||||
|
||||
// Get success probability
|
||||
// Get success probability for binary outcome actions
|
||||
if (!descriptor->HasOdds()) {
|
||||
throw ShardokInternalErrorException("Action does not have odds in getBinaryOutcomeInfo");
|
||||
}
|
||||
@@ -585,7 +617,7 @@ BinaryOutcomeInfo ShardokGameEngine::getBinaryOutcomeInfo(
|
||||
const auto successChancePercentile = descriptor->GetOddsPercentile();
|
||||
const double successProbability = static_cast<double>(successChancePercentile) / 100.0;
|
||||
|
||||
return BinaryOutcomeInfo{successProbability};
|
||||
return ChanceOutcomeInfo::binary(successProbability);
|
||||
}
|
||||
|
||||
void ShardokGameEngine::clearLegalActionsCache() { legalActionsCache_.clear(); }
|
||||
|
||||
@@ -64,7 +64,7 @@ double ShardokGameState::score(MCTSPlayerId playerId) const {
|
||||
const bool scoreFromDefenderPerspective =
|
||||
foundDefender ? requestedPlayerIsDefender : isDefender_;
|
||||
|
||||
// Call score calculator with correct perspective for the requested player
|
||||
// Score the current state directly
|
||||
return scoreCalculator_
|
||||
->GuessedStateScore(scoreFromDefenderPerspective, state_, strategy_, castleCoords_);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
#include "FireUtils.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/TileModifier.hpp"
|
||||
|
||||
namespace shardok {
|
||||
@@ -71,10 +73,13 @@ auto GetFireDamage(
|
||||
const double openEndedPercentileRoll1,
|
||||
const double openEndedPercentileRoll2) -> CombatDamage {
|
||||
double randomSwing = 1.0 + (2 * openEndedPercentileRoll1 - 100.0) * RANDOMNESS_FACTOR / 100.0;
|
||||
const double basicDamage = BASE_FIRE_DAMAGE_PER_TROOP * randomSwing * troops;
|
||||
// Clamp damage to minimum 0 to prevent negative damage from extreme open-ended rolls.
|
||||
// Open-ended percentile rolls can theoretically go as low as -475 (with max roll depth).
|
||||
const double basicDamage = std::max(0.0, BASE_FIRE_DAMAGE_PER_TROOP * randomSwing * troops);
|
||||
|
||||
randomSwing = 1.0 + (2 * openEndedPercentileRoll2 - 100.0) * RANDOMNESS_FACTOR / 100.0;
|
||||
const double penetratingDamage = BASE_PENETRATING_FIRE_DAMAGE_PER_TROOP * randomSwing * troops;
|
||||
const double penetratingDamage =
|
||||
std::max(0.0, BASE_PENETRATING_FIRE_DAMAGE_PER_TROOP * randomSwing * troops);
|
||||
|
||||
return CombatDamage::Builder()
|
||||
.SetFire(basicDamage)
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokException.hpp"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
auto MutatingMaybeSetMorale(Battalion *battalion, double newVal, const BattalionTypeSPtr &type)
|
||||
@@ -147,6 +149,12 @@ int32_t MutatingInternalTakeDamage(
|
||||
int32_t newNumber;
|
||||
const int32_t casualties = (int32_t)(takenDamage * baseDeadliness);
|
||||
|
||||
if (casualties < 0) {
|
||||
throw ShardokInternalErrorException(
|
||||
"Negative casualties in MutatingInternalTakeDamage: " + std::to_string(casualties) +
|
||||
" (takenDamage=" + std::to_string(takenDamage) + ")");
|
||||
}
|
||||
|
||||
if (battalion->size() > casualties) {
|
||||
newNumber = battalion->size() - casualties;
|
||||
} else {
|
||||
|
||||
@@ -19,7 +19,13 @@ using net::eagle0::shardok::api::UnitView;
|
||||
using net::eagle0::shardok::common::CommandType;
|
||||
using std::vector;
|
||||
|
||||
auto UnknownUnit(UnitId uid, PlayerId pid, BattalionTypeId bt, int size, bool hidden) -> UnitView;
|
||||
auto UnknownUnit(
|
||||
UnitId uid,
|
||||
PlayerId pid,
|
||||
BattalionTypeId bt,
|
||||
int size,
|
||||
bool hidden,
|
||||
int8_t startingPositionIndex) -> UnitView;
|
||||
|
||||
[[nodiscard]] auto UnitFilteredForPlayer(
|
||||
const SettingsGetter &settings,
|
||||
@@ -36,7 +42,8 @@ auto UnknownUnit(UnitId uid, PlayerId pid, BattalionTypeId bt, int size, bool hi
|
||||
unit->player_id(),
|
||||
unit->battalion().type(),
|
||||
unit->battalion().size(),
|
||||
true);
|
||||
true,
|
||||
unit->starting_position_index());
|
||||
}
|
||||
if (IsUnplaced(unit->location()) && !visibleToAsker) {
|
||||
return UnknownUnit(
|
||||
@@ -44,7 +51,8 @@ auto UnknownUnit(UnitId uid, PlayerId pid, BattalionTypeId bt, int size, bool hi
|
||||
unit->player_id(),
|
||||
unit->battalion().type(),
|
||||
unit->battalion().size(),
|
||||
false);
|
||||
false,
|
||||
unit->starting_position_index());
|
||||
}
|
||||
|
||||
UnitView filtered{};
|
||||
@@ -142,7 +150,8 @@ auto UnknownUnit(
|
||||
const PlayerId pid,
|
||||
BattalionTypeId bt,
|
||||
const int size,
|
||||
const bool hidden) -> UnitView {
|
||||
const bool hidden,
|
||||
const int8_t startingPositionIndex) -> UnitView {
|
||||
UnitView uv;
|
||||
uv.set_unit_id(uid);
|
||||
uv.set_player_id(pid);
|
||||
@@ -154,6 +163,11 @@ auto UnknownUnit(
|
||||
uv.mutable_battalion()->set_size(size);
|
||||
uv.set_hidden(hidden);
|
||||
|
||||
// starting_position_index is public info - defenders know attacker spawn directions
|
||||
if (startingPositionIndex != -1) {
|
||||
uv.mutable_starting_position_index()->set_value(startingPositionIndex);
|
||||
}
|
||||
|
||||
uv.set_my_knowledge(0);
|
||||
|
||||
return uv;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+11
-2
@@ -174,7 +174,8 @@ public class CustomBattleHandler : MonoBehaviour, IClientConnectionSubscriber {
|
||||
});
|
||||
}
|
||||
|
||||
private void Register() { _persistentClientConnection.Subscribe(this); }
|
||||
// Fire-and-forget - subscription is awaited internally and failures are logged
|
||||
private void Register() { _ = _persistentClientConnection.Subscribe(this); }
|
||||
|
||||
public GameId? CurrentShardokToken(string shardokGameId) { return _shardokModel.History.Count; }
|
||||
|
||||
@@ -187,9 +188,14 @@ public class CustomBattleHandler : MonoBehaviour, IClientConnectionSubscriber {
|
||||
case GameUpdate.GameUpdateDetailsOneofCase.ShardokActionResultResponse:
|
||||
foreach (var resp in gameUpdate.ShardokActionResultResponse
|
||||
.ShardokGameResponses) {
|
||||
_shardokModel.HandleUpdates(
|
||||
var updatesOk = _shardokModel.HandleUpdates(
|
||||
resp.ActionResultViews,
|
||||
resp.NewResultViewCount);
|
||||
if (!updatesOk) {
|
||||
// In custom battle mode, just clear and continue
|
||||
_shardokModel.History.Clear();
|
||||
continue;
|
||||
}
|
||||
_shardokModel.HandleAvailableCommands(resp.AvailableCommands);
|
||||
}
|
||||
|
||||
@@ -443,4 +449,7 @@ public class CustomBattleHandler : MonoBehaviour, IClientConnectionSubscriber {
|
||||
} };
|
||||
|
||||
public List<IClientConnectionSubscriber.StreamingTextStatus> StreamingTextStatuses => new();
|
||||
|
||||
// CustomBattleHandler only handles Shardok updates, not Eagle, so no count to update
|
||||
public void UpdateResultCounts(GameUpdate update) {}
|
||||
}
|
||||
|
||||
+12
@@ -86,5 +86,17 @@ namespace eagle {
|
||||
}
|
||||
|
||||
public void ToggleClicked(bool value) { SetCostLabel(); }
|
||||
|
||||
public override void AddTargetedHero(HeroId heroId) {
|
||||
// Find the index of the clicked hero in the divinable heroes list
|
||||
var divinableArray = DivineCommand.DivinableHeroes.ToArray();
|
||||
for (int i = 0; i < divinableArray.Length; i++) {
|
||||
if (divinableArray[i].Hero.Id == heroId) {
|
||||
_selectedHeroIndex = i;
|
||||
SetUpUI();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+22
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Net.Eagle0.Eagle.Api;
|
||||
using Net.Eagle0.Eagle.Api.Command.Util;
|
||||
@@ -9,6 +10,7 @@ using UnityEngine.UI;
|
||||
|
||||
namespace eagle {
|
||||
using ProvinceId = Int32;
|
||||
using HeroId = Int32;
|
||||
|
||||
public class ManagePrisonersCommandSelector : CommandSelector {
|
||||
// Unity accessors
|
||||
@@ -36,6 +38,26 @@ namespace eagle {
|
||||
|
||||
public override AvailableCommand.SealedValueOneofCase CommandType =>
|
||||
AvailableCommand.SealedValueOneofCase.ManagePrisonerCommand;
|
||||
|
||||
public override List<HeroId> TargetedHeroIds => new() { SelectedHero.Id };
|
||||
|
||||
public override bool HeroIsTargetable(HeroId heroId) {
|
||||
if (_availableCommand == null || _availableCommand.ManagePrisonerCommand == null) {
|
||||
return false;
|
||||
}
|
||||
return ManagePrisonersCommand.Prisoners.Any(p => p.Prisoner.Hero.Id == heroId);
|
||||
}
|
||||
|
||||
public override void AddTargetedHero(HeroId heroId) {
|
||||
// Find the prisoner with this heroId and select it
|
||||
for (int i = 0; i < ManagePrisonersCommand.Prisoners.Count; i++) {
|
||||
if (ManagePrisonersCommand.Prisoners[i].Prisoner.Hero.Id == heroId) {
|
||||
_selectedHeroIndex = i;
|
||||
DisplaySelectedHero();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
public override string HeaderString => "Manage Prisoners";
|
||||
public override string CommitButtonString =>
|
||||
$"Commit {DisplayNames.PrisonerManagementTypeName(SelectedOption)}";
|
||||
|
||||
+18
-2
@@ -30,10 +30,26 @@ namespace eagle {
|
||||
public override string HeaderString => "Recruit Heroes";
|
||||
public override string CommitButtonString => "Commit Recruit";
|
||||
|
||||
public override bool HeroIsTargetable(HeroId heroId) =>
|
||||
RecruitHeroesCommand.AvailableHeroes.Any(euh => euh.Hero.Id == heroId);
|
||||
public override bool HeroIsTargetable(HeroId heroId) {
|
||||
if (_availableCommand == null || _availableCommand.RecruitHeroesCommand == null) {
|
||||
return false;
|
||||
}
|
||||
return RecruitHeroesCommand.AvailableHeroes.Any(euh => euh.Hero.Id == heroId);
|
||||
}
|
||||
public override List<HeroId> TargetedHeroIds => new List<HeroId> { SelectedHero.Hero.Id };
|
||||
|
||||
public override void AddTargetedHero(HeroId heroId) {
|
||||
// Find the hero in available heroes and select it
|
||||
var availableHeroes = RecruitHeroesCommand.AvailableHeroes.ToList();
|
||||
for (int i = 0; i < availableHeroes.Count; i++) {
|
||||
if (availableHeroes[i].Hero.Id == heroId) {
|
||||
_selectedHeroIndex = i;
|
||||
DisplayHero();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DisplayHero() {
|
||||
heroDetails.SetHero(SelectedHero.Hero, _model);
|
||||
statusText.text = DisplayNames.UnaffiliatedHeroStatus(SelectedHero.Type);
|
||||
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
using System;
|
||||
using common;
|
||||
|
||||
namespace eagle {
|
||||
/// <summary>
|
||||
/// Circuit breaker pattern for connection failures.
|
||||
/// Prevents cascading failures by blocking connection attempts during server outages.
|
||||
/// </summary>
|
||||
public class ConnectionCircuitBreaker {
|
||||
public enum State {
|
||||
Closed, // Normal operation, allowing connections
|
||||
Open, // Too many failures, blocking connections
|
||||
HalfOpen // Testing if service recovered
|
||||
}
|
||||
|
||||
private const int FailureThreshold = 5; // Open after 5 failures
|
||||
private const double OpenTimeoutSeconds = 60.0; // Wait 60s before test
|
||||
private const double SuccessResetThreshold = 3; // Close after 3 successes
|
||||
|
||||
private State _state = State.Closed;
|
||||
private int _failureCount = 0;
|
||||
private int _successCount = 0;
|
||||
private DateTime? _openedAt = null;
|
||||
|
||||
private readonly Logger _logger = Logger.GetLogger("ConnectionLogger");
|
||||
|
||||
public State CurrentState {
|
||||
get {
|
||||
lock (this) { return _state; }
|
||||
}
|
||||
}
|
||||
|
||||
public DateTime? NextTestAttempt {
|
||||
get {
|
||||
lock (this) {
|
||||
if (_state == State.Open && _openedAt.HasValue) {
|
||||
return _openedAt.Value.AddSeconds(OpenTimeoutSeconds);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a connection attempt should be allowed.
|
||||
/// </summary>
|
||||
public bool ShouldAttemptConnection() {
|
||||
lock (this) {
|
||||
switch (_state) {
|
||||
case State.Closed: return true; // Normal operation
|
||||
|
||||
case State.HalfOpen: return true; // Allow test attempt
|
||||
|
||||
case State.Open:
|
||||
// Check if timeout has elapsed
|
||||
if (_openedAt.HasValue) {
|
||||
var elapsed = (DateTime.UtcNow - _openedAt.Value).TotalSeconds;
|
||||
if (elapsed >= OpenTimeoutSeconds) {
|
||||
// Transition to half-open for test
|
||||
_state = State.HalfOpen;
|
||||
_logger.LogLine(
|
||||
$"[CIRCUIT] OPEN → HALF_OPEN (testing after {elapsed:F1}s)");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false; // Still in open state, block connection
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Record a successful connection.
|
||||
/// </summary>
|
||||
public void RecordSuccess() {
|
||||
lock (this) {
|
||||
if (_state == State.HalfOpen) {
|
||||
// Test succeeded, close circuit
|
||||
_state = State.Closed;
|
||||
_failureCount = 0;
|
||||
_successCount = 0;
|
||||
_openedAt = null;
|
||||
_logger.LogLine($"[CIRCUIT] HALF_OPEN → CLOSED (test succeeded)");
|
||||
} else if (_state == State.Closed) {
|
||||
// Normal success, increment counter
|
||||
_successCount++;
|
||||
if (_failureCount > 0) {
|
||||
_failureCount = Math.Max(0, _failureCount - 1); // Decay failures
|
||||
}
|
||||
if (_failureCount == 0 && _successCount >= SuccessResetThreshold) {
|
||||
_successCount = 0; // Reset success counter
|
||||
_logger.LogLine($"[CIRCUIT] CLOSED (connection stable)");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Record a connection failure.
|
||||
/// </summary>
|
||||
public void RecordFailure() {
|
||||
lock (this) {
|
||||
_failureCount++;
|
||||
_successCount = 0; // Reset success counter on any failure
|
||||
|
||||
if (_state == State.HalfOpen) {
|
||||
// Test attempt failed, reopen circuit
|
||||
_state = State.Open;
|
||||
_openedAt = DateTime.UtcNow;
|
||||
_logger.LogLine(
|
||||
$"[CIRCUIT] HALF_OPEN → OPEN (test failed, failures={_failureCount})");
|
||||
} else if (_failureCount >= FailureThreshold && _state == State.Closed) {
|
||||
// Too many failures, open circuit
|
||||
_state = State.Open;
|
||||
_openedAt = DateTime.UtcNow;
|
||||
_logger.LogLine(
|
||||
$"[CIRCUIT] CLOSED → OPEN (failures={_failureCount}, threshold={FailureThreshold})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get human-readable status for UI display.
|
||||
/// </summary>
|
||||
public string GetStatusMessage() {
|
||||
lock (this) {
|
||||
switch (_state) {
|
||||
case State.Closed:
|
||||
return _failureCount > 0
|
||||
? $"Connection recovering ({_failureCount} recent failures)"
|
||||
: "Connection healthy";
|
||||
|
||||
case State.HalfOpen: return "Testing connection...";
|
||||
|
||||
case State.Open:
|
||||
if (_openedAt.HasValue) {
|
||||
var timeUntilTest = OpenTimeoutSeconds -
|
||||
(DateTime.UtcNow - _openedAt.Value).TotalSeconds;
|
||||
if (timeUntilTest > 0) {
|
||||
return $"Server unavailable. Retrying in {(int)timeUntilTest}s";
|
||||
}
|
||||
}
|
||||
return "Server unavailable. Testing...";
|
||||
|
||||
default: return "Unknown state";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1c49d026769d746aeac2ceff7c1f5dc4
|
||||
@@ -0,0 +1,149 @@
|
||||
using System;
|
||||
using Net.Eagle0.Eagle.Api;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
namespace eagle {
|
||||
/// <summary>
|
||||
/// Provides game state information for the connection status UI.
|
||||
/// Implement this interface to show game-specific status when connected.
|
||||
/// </summary>
|
||||
public interface IGameStateProvider {
|
||||
/// <summary>Server-reported game status. Null if no status received yet.</summary>
|
||||
ServerGameStatus ServerStatus { get; }
|
||||
|
||||
/// <summary>True if a command was submitted and we're awaiting response (for >
|
||||
/// 500ms).</summary>
|
||||
bool IsProcessingCommand { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simple UI component to display connection status and reconnection countdown.
|
||||
/// Attach to a TextMeshProUGUI component to display status.
|
||||
/// </summary>
|
||||
public class ConnectionStatusUI : MonoBehaviour {
|
||||
private TextMeshProUGUI _textComponent;
|
||||
private PersistentClientConnection _connection;
|
||||
private IGameStateProvider _gameStateProvider;
|
||||
|
||||
// Update interval in seconds
|
||||
private const float UpdateInterval = 0.5f;
|
||||
private float _timeSinceLastUpdate = 0f;
|
||||
|
||||
void Start() {
|
||||
_textComponent = GetComponent<TextMeshProUGUI>();
|
||||
if (_textComponent == null) {
|
||||
Debug.LogError(
|
||||
"ConnectionStatusUI must be attached to a TextMeshProUGUI component");
|
||||
enabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the PersistentClientConnection - this assumes it's accessible
|
||||
// In production, this would need proper dependency injection
|
||||
// For now, the connection will be set externally or found via another method
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the connection to monitor. Call this after creating the connection.
|
||||
/// </summary>
|
||||
public void SetConnection(PersistentClientConnection connection) {
|
||||
_connection = connection;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the game state provider for showing game-specific status.
|
||||
/// Call this when entering a game, clear it when leaving.
|
||||
/// </summary>
|
||||
public void SetGameStateProvider(IGameStateProvider provider) {
|
||||
_gameStateProvider = provider;
|
||||
}
|
||||
|
||||
void Update() {
|
||||
if (_connection == null || _textComponent == null) { return; }
|
||||
|
||||
_timeSinceLastUpdate += Time.deltaTime;
|
||||
if (_timeSinceLastUpdate < UpdateInterval) { return; }
|
||||
|
||||
_timeSinceLastUpdate = 0f;
|
||||
UpdateDisplay();
|
||||
}
|
||||
|
||||
private void UpdateDisplay() {
|
||||
// Check circuit breaker state first - it takes precedence
|
||||
var circuitState = _connection.CircuitBreaker.CurrentState;
|
||||
if (circuitState == ConnectionCircuitBreaker.State.Open) {
|
||||
var nextTest = _connection.CircuitBreaker.NextTestAttempt;
|
||||
if (nextTest.HasValue) {
|
||||
var timeUntilTest = nextTest.Value - DateTime.UtcNow;
|
||||
if (timeUntilTest.TotalSeconds > 0) {
|
||||
int seconds = (int)Math.Ceiling(timeUntilTest.TotalSeconds);
|
||||
_textComponent.text =
|
||||
$"<color=red>●</color> Server down. Retry in {seconds}s";
|
||||
return;
|
||||
}
|
||||
}
|
||||
_textComponent.text = "<color=red>●</color> Server unavailable";
|
||||
return;
|
||||
} else if (circuitState == ConnectionCircuitBreaker.State.HalfOpen) {
|
||||
_textComponent.text = "<color=yellow>●</color> Testing connection...";
|
||||
return;
|
||||
}
|
||||
|
||||
// Normal connection state display
|
||||
var state = _connection.CurrentState;
|
||||
var nextAttempt = _connection.NextReconnectAttempt;
|
||||
|
||||
string statusText = state switch {
|
||||
ConnectionState.Connected => GetConnectedStatusText(),
|
||||
ConnectionState.Connecting => "<color=yellow>●</color> Connecting...",
|
||||
ConnectionState.Disconnected => "<color=red>●</color> Disconnected",
|
||||
ConnectionState.Reconnecting => GetReconnectingText(nextAttempt),
|
||||
ConnectionState.SubscriptionPending => "<color=yellow>●</color> Subscribing...",
|
||||
_ => "<color=gray>●</color> Unknown"
|
||||
};
|
||||
|
||||
_textComponent.text = statusText;
|
||||
}
|
||||
|
||||
private string GetConnectedStatusText() {
|
||||
// If no game state provider, just show "Connected"
|
||||
if (_gameStateProvider == null) { return "<color=green>●</color> Connected"; }
|
||||
|
||||
// Processing takes priority (client knows it submitted a command)
|
||||
if (_gameStateProvider.IsProcessingCommand) {
|
||||
return "<color=green>●</color> Processing...";
|
||||
}
|
||||
|
||||
// Use server-reported status
|
||||
var serverStatus = _gameStateProvider.ServerStatus;
|
||||
if (serverStatus == null) {
|
||||
// No server status yet - waiting for first response
|
||||
return "<color=green>●</color> Connected";
|
||||
}
|
||||
|
||||
return serverStatus.Status switch {
|
||||
ServerGameStatus.Types.Status.YourTurn => "<color=green>●</color> Your turn",
|
||||
ServerGameStatus.Types.Status.WaitingForPlayers =>
|
||||
"<color=green>●</color> Waiting for other players",
|
||||
ServerGameStatus.Types.Status.GeneratingText =>
|
||||
"<color=green>●</color> Generating...",
|
||||
ServerGameStatus.Types.Status.ProcessingAction =>
|
||||
"<color=green>●</color> Processing...",
|
||||
_ => "<color=green>●</color> Connected"
|
||||
};
|
||||
}
|
||||
|
||||
private string GetReconnectingText(DateTime? nextAttempt) {
|
||||
if (!nextAttempt.HasValue) { return "<color=yellow>●</color> Reconnecting..."; }
|
||||
|
||||
var timeUntilRetry = nextAttempt.Value - DateTime.UtcNow;
|
||||
if (timeUntilRetry.TotalSeconds <= 0) {
|
||||
return "<color=yellow>●</color> Reconnecting...";
|
||||
}
|
||||
|
||||
int secondsRemaining = (int)Math.Ceiling(timeUntilRetry.TotalSeconds);
|
||||
return $"<color=orange>●</color> Retry in {secondsRemaining}s";
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 727a520b2deac4a28ae9c6a00e54c2e7
|
||||
+28
-7
@@ -24,8 +24,8 @@ namespace eagle {
|
||||
public RollPanelController rollPanelController;
|
||||
|
||||
public TextMeshProUGUI roundStatusLabel;
|
||||
public TextMeshProUGUI widthLabel;
|
||||
public TextMeshProUGUI heightLabel;
|
||||
public TextMeshProUGUI connectionStatusLabel;
|
||||
private bool _connectionStatusUIInitialized = false;
|
||||
|
||||
public GameObject alwaysOnLeftColumn;
|
||||
public GameObject factionsAndMovingArmiesRow;
|
||||
@@ -110,8 +110,15 @@ namespace eagle {
|
||||
|
||||
if (newWidth == _lastWidth && newHeight == _lastHeight) { return; }
|
||||
|
||||
widthLabel.text = newWidth.ToString();
|
||||
heightLabel.text = newHeight.ToString();
|
||||
// Initialize ConnectionStatusUI component once when connection is available
|
||||
if (!_connectionStatusUIInitialized &&
|
||||
errorHandler.PersistentClientConnection != null) {
|
||||
var statusUI = connectionStatusLabel.GetComponent<ConnectionStatusUI>();
|
||||
if (statusUI != null) {
|
||||
statusUI.SetConnection(errorHandler.PersistentClientConnection);
|
||||
_connectionStatusUIInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
_lastWidth = newWidth;
|
||||
_lastHeight = newHeight;
|
||||
@@ -158,6 +165,10 @@ namespace eagle {
|
||||
Model = null;
|
||||
chronicleCanvasController.Entries = new List<ChronicleEntry>();
|
||||
SetMusic();
|
||||
|
||||
// Clear game state provider when leaving game
|
||||
var statusUI = connectionStatusLabel.GetComponent<ConnectionStatusUI>();
|
||||
if (statusUI != null) { statusUI.SetGameStateProvider(null); }
|
||||
}
|
||||
|
||||
void MapControllerChangedTarget(List<ProvinceId> newTarget) {
|
||||
@@ -218,7 +229,8 @@ namespace eagle {
|
||||
if (pause) {
|
||||
ModelUpdater.StopListeningForUpdates();
|
||||
} else {
|
||||
ModelUpdater.StartListeningForUpdates();
|
||||
// Fire-and-forget - subscription is awaited internally and failures are logged
|
||||
_ = ModelUpdater.StartListeningForUpdates();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,7 +267,13 @@ namespace eagle {
|
||||
|
||||
ModelUpdater.ErrorHandler = errorHandler;
|
||||
|
||||
MainQueue.Q.EnqueueForNextUpdate(() => { ModelUpdater.StartListeningForUpdates(); });
|
||||
// Set up game state provider for connection status UI
|
||||
var statusUI = connectionStatusLabel.GetComponent<ConnectionStatusUI>();
|
||||
if (statusUI != null) { statusUI.SetGameStateProvider(ModelUpdater); }
|
||||
|
||||
// Fire-and-forget - subscription is awaited internally and failures are logged
|
||||
MainQueue.Q.EnqueueForNextUpdate(
|
||||
() => { _ = ModelUpdater.StartListeningForUpdates(); });
|
||||
|
||||
#if UNITY_EDITOR
|
||||
EditorApplication.playModeStateChanged += (state) => OnPlayModeStateChanged(state);
|
||||
@@ -294,6 +312,7 @@ namespace eagle {
|
||||
movingArmiesTableController.UpdateTables();
|
||||
factionsTableController.UpdateTables();
|
||||
heroesAndBattalionsPanelController.UpdateTables();
|
||||
freeHeroesTableController.UpdateUnaffiliatedHeroSelections();
|
||||
}
|
||||
|
||||
private void UpdateButtons() {
|
||||
@@ -649,7 +668,9 @@ namespace eagle {
|
||||
var shardokModel = Model.ShardokGameModels[selectedModel.ShardokGameId];
|
||||
|
||||
shardokCanvas.gameObject.SetActive(true);
|
||||
shardokCanvas.GetComponent<ShardokGameController>().SetUpGame(shardokModel);
|
||||
var shardokController = shardokCanvas.GetComponent<ShardokGameController>();
|
||||
shardokController.SetUpGame(shardokModel);
|
||||
shardokController.SetConnection(errorHandler.PersistentClientConnection);
|
||||
gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
@@ -61,7 +62,7 @@ namespace eagle {
|
||||
RollFetcher RollFetcher { get; }
|
||||
}
|
||||
|
||||
public class GameModelUpdater : IClientConnectionSubscriber {
|
||||
public class GameModelUpdater : IClientConnectionSubscriber, IGameStateProvider {
|
||||
private FactionId? PlayerId => _currentModel.PlayerId;
|
||||
public Int64? CurrentEagleToken => _currentModel.CommandToken;
|
||||
public Int64? CurrentShardokToken(string shardokGameId) {
|
||||
@@ -94,13 +95,21 @@ namespace eagle {
|
||||
|
||||
public long GameId { get; }
|
||||
|
||||
public int LastUnfilteredResultCount => _lastUnfilteredResultCount;
|
||||
public int LastUnfilteredResultCount {
|
||||
get {
|
||||
lock (_resultCountLock) { return _lastUnfilteredResultCount; }
|
||||
}
|
||||
}
|
||||
|
||||
public List<IClientConnectionSubscriber.ShardokViewStatus> ShardokViewStatuses =>
|
||||
_currentModel.ShardokGameModels
|
||||
.Select(sgm => new IClientConnectionSubscriber.ShardokViewStatus {
|
||||
shardokGameId = sgm.Key,
|
||||
filteredResultCount = sgm.Value.History.Count()
|
||||
.Select(sgm => {
|
||||
var needsResync = _shardokNeedsResync.GetValueOrDefault(sgm.Key, false);
|
||||
return new IClientConnectionSubscriber.ShardokViewStatus {
|
||||
shardokGameId = sgm.Key,
|
||||
filteredResultCount = needsResync ? 0 : sgm.Value.History.Count(),
|
||||
requestFullResync = needsResync
|
||||
};
|
||||
})
|
||||
.ToList();
|
||||
|
||||
@@ -117,10 +126,22 @@ namespace eagle {
|
||||
|
||||
public ErrorHandler ErrorHandler;
|
||||
|
||||
// Thread-safe: updated from gRPC thread via UpdateResultCounts, read from main thread
|
||||
private int _lastUnfilteredResultCount = 0;
|
||||
private readonly object _resultCountLock = new();
|
||||
|
||||
private readonly Logger _connectionLogger = Logger.GetLogger("ConnectionLogger");
|
||||
|
||||
// Track when a command was submitted for "Processing..." display
|
||||
// Only show "Processing..." if command has been pending for > 500ms
|
||||
private DateTime? _commandSubmittedTime;
|
||||
private const double ProcessingDisplayDelayMs = 500.0;
|
||||
|
||||
// Track which Shardok games need full state resync after connection drop
|
||||
// Thread-safe: accessed from both connection thread and Unity main thread
|
||||
private readonly ConcurrentDictionary<string, bool> _shardokNeedsResync =
|
||||
new ConcurrentDictionary<string, bool>();
|
||||
|
||||
private readonly RollFetcher _rollFetcher;
|
||||
|
||||
// State synced with server
|
||||
@@ -219,8 +240,16 @@ namespace eagle {
|
||||
}
|
||||
|
||||
private ShardokGameModel MakeGameModel(ShardokGameId shardokGameId) {
|
||||
var battleView =
|
||||
_currentModel.ShardokBattles.First(b => b.ShardokGameId == shardokGameId);
|
||||
var battleView = _currentModel.ShardokBattles.FirstOrDefault(
|
||||
b => b.ShardokGameId == shardokGameId);
|
||||
|
||||
if (battleView == null) {
|
||||
// Battle was removed (e.g., it ended) before we could create the model.
|
||||
// This can happen due to race conditions between Eagle and Shardok updates.
|
||||
Debug.LogWarning(
|
||||
$"Cannot create ShardokGameModel for {shardokGameId}: battle not found in ShardokBattles (likely already ended)");
|
||||
return null;
|
||||
}
|
||||
|
||||
PlayerId playerId = battleView.MyPlayerId ?? -1;
|
||||
|
||||
@@ -266,6 +295,14 @@ namespace eagle {
|
||||
|
||||
switch (updateItem.GameUpdateDetailsCase) {
|
||||
case GameUpdate.GameUpdateDetailsOneofCase.ActionResultResponse:
|
||||
// Clear processing state - we received a response from the server
|
||||
_commandSubmittedTime = null;
|
||||
|
||||
// Store server-reported game status for UI
|
||||
if (updateItem.ActionResultResponse.ServerGameStatus != null) {
|
||||
ServerStatus = updateItem.ActionResultResponse.ServerGameStatus;
|
||||
}
|
||||
|
||||
_lastUnfilteredResultCount =
|
||||
updateItem.ActionResultResponse.UnfilteredResultCountAfter;
|
||||
if (updateItem.ActionResultResponse.ActionResultViews.Any() ||
|
||||
@@ -289,18 +326,39 @@ namespace eagle {
|
||||
out var shardokGameModel)) {
|
||||
shardokGameModel =
|
||||
MakeGameModel(shardokGameId: oneResponse.ShardokGameId);
|
||||
|
||||
// Battle may have ended before we could create the model - remove
|
||||
// any stale reference and skip this update
|
||||
if (shardokGameModel == null) {
|
||||
_currentModel.ShardokGameModels.Remove(oneResponse.ShardokGameId);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
shardokGameModel.HandleUpdates(
|
||||
var updatesOk = shardokGameModel.HandleUpdates(
|
||||
oneResponse.ActionResultViews,
|
||||
oneResponse.NewResultViewCount);
|
||||
|
||||
if (!updatesOk) {
|
||||
// Missing results - mark for resync and clear history
|
||||
MarkShardokForResync(oneResponse.ShardokGameId);
|
||||
shardokGameModel.History.Clear();
|
||||
continue;
|
||||
}
|
||||
|
||||
shardokGameModel.HandleAvailableCommands(oneResponse.AvailableCommands);
|
||||
|
||||
// Clear resync flag after successfully receiving updates
|
||||
ClearShardokResyncFlag(oneResponse.ShardokGameId);
|
||||
|
||||
if (shardokGameModel.GameStatus.State ==
|
||||
GameStatus.Types.State.GameRunning ||
|
||||
shardokGameModel.GameStatus.State == GameStatus.Types.State.SetUp) {
|
||||
_currentModel.ShardokGameModels[oneResponse.ShardokGameId] =
|
||||
shardokGameModel;
|
||||
} else {
|
||||
// Game ended - remove from active models so UI knows battle is over
|
||||
_currentModel.ShardokGameModels.Remove(oneResponse.ShardokGameId);
|
||||
}
|
||||
}
|
||||
if (UpdateAction != null) UpdateAction.Invoke(_currentModel);
|
||||
@@ -321,10 +379,65 @@ namespace eagle {
|
||||
}
|
||||
}
|
||||
|
||||
public void StartListeningForUpdates() { PersistentConnection.Subscribe(this); }
|
||||
/// <summary>
|
||||
/// Update result counts immediately when an update is received from the server.
|
||||
/// Called from the gRPC thread BEFORE enqueueing to MainQueue, to ensure
|
||||
/// reconnects use accurate counts even when MainQueue is blocked (e.g., backgrounded).
|
||||
/// </summary>
|
||||
public void UpdateResultCounts(GameUpdate update) {
|
||||
switch (update.GameUpdateDetailsCase) {
|
||||
case GameUpdate.GameUpdateDetailsOneofCase.ActionResultResponse:
|
||||
lock (_resultCountLock) {
|
||||
_lastUnfilteredResultCount =
|
||||
update.ActionResultResponse.UnfilteredResultCountAfter;
|
||||
}
|
||||
break;
|
||||
|
||||
// Note: Shardok counts are tracked in ShardokGameModel.History.Count
|
||||
// which is updated in ReceiveGameUpdate on the main thread.
|
||||
// For now, Shardok reconnects may still get duplicate data, but
|
||||
// the primary issue (Eagle duplicates) is fixed here.
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to game updates. Returns true if subscription was acknowledged by server.
|
||||
/// </summary>
|
||||
public async Task<bool> StartListeningForUpdates() {
|
||||
return await PersistentConnection.Subscribe(this);
|
||||
}
|
||||
|
||||
public void StopListeningForUpdates() { PersistentConnection.Unsubscribe(this); }
|
||||
|
||||
/// <summary>
|
||||
/// Mark a Shardok game for full state resync on next connection.
|
||||
/// Used after connection drops to ensure state consistency.
|
||||
/// </summary>
|
||||
public void MarkShardokForResync(string shardokGameId) {
|
||||
_shardokNeedsResync[shardokGameId] = true;
|
||||
_connectionLogger.LogLine(
|
||||
$"[RESYNC] Marked Shardok game {shardokGameId} for full state resync");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear resync flag after successfully receiving full state.
|
||||
/// </summary>
|
||||
public void ClearShardokResyncFlag(string shardokGameId) {
|
||||
if (_shardokNeedsResync.TryRemove(shardokGameId, out _)) {
|
||||
_connectionLogger.LogLine(
|
||||
$"[RESYNC] Cleared resync flag for Shardok game {shardokGameId}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mark all active Shardok games for resync (called on disconnect).
|
||||
/// </summary>
|
||||
public void MarkAllShardokForResync() {
|
||||
foreach (var shardokGameId in _currentModel.ShardokGameModels.Keys) {
|
||||
MarkShardokForResync(shardokGameId);
|
||||
}
|
||||
}
|
||||
|
||||
public Task PostCommand(ProvinceId provinceId, SelectedCommand command) {
|
||||
_connectionLogger.LogLine(
|
||||
$"Posting command with token {_currentModel.CommandTokenString}");
|
||||
@@ -332,6 +445,7 @@ namespace eagle {
|
||||
_currentModel.AvailableCommandsByProvince.Clear();
|
||||
_currentModel.CommandToken = null;
|
||||
_currentModel.LastPostedToken = token;
|
||||
_commandSubmittedTime = DateTime.UtcNow;
|
||||
return PersistentConnection.PostEagleCommand(
|
||||
gameId: GameId,
|
||||
token: token,
|
||||
@@ -343,6 +457,10 @@ namespace eagle {
|
||||
}
|
||||
|
||||
private void HandleStartingState(GameStateView startingState) {
|
||||
var timestamp = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss.fff");
|
||||
_connectionLogger.LogLine(
|
||||
$"[STATE_RESYNC] timestamp={timestamp} round={startingState.CurrentRoundId} factions={startingState.Factions.Count} heroes={startingState.Heroes.Count}");
|
||||
|
||||
_currentModel.GsView = startingState;
|
||||
_currentModel.BattalionTypes =
|
||||
startingState.BattalionTypes.ToDictionary(bt => bt.TypeId, bt => bt);
|
||||
@@ -689,5 +807,21 @@ namespace eagle {
|
||||
Notify(result);
|
||||
}
|
||||
}
|
||||
|
||||
#region IGameStateProvider implementation
|
||||
|
||||
/// <summary>Server-reported game status from the last ActionResultResponse.</summary>
|
||||
public ServerGameStatus ServerStatus { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// True if a command was submitted and we're awaiting response.
|
||||
/// Only returns true if processing for > 500ms to avoid flashing.
|
||||
/// </summary>
|
||||
public bool IsProcessingCommand =>
|
||||
_commandSubmittedTime.HasValue &&
|
||||
(DateTime.UtcNow - _commandSubmittedTime.Value).TotalMilliseconds >
|
||||
ProcessingDisplayDelayMs;
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -59,7 +59,7 @@ namespace eagle {
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateUnaffiliatedHeroSelections() {
|
||||
public void UpdateUnaffiliatedHeroSelections() {
|
||||
UnaffiliatedHeroes.Each(
|
||||
(uh, i) => SetUnaffiliatedHeroRowSelections(
|
||||
unaffiliatedHeroesTable.ComponentAt<UnaffiliatedHeroRowController>(i),
|
||||
|
||||
+8
@@ -11,10 +11,18 @@ namespace eagle {
|
||||
|
||||
public void ReceiveGameUpdate(GameUpdate update);
|
||||
|
||||
/// <summary>
|
||||
/// Update the known result counts immediately when an update is received.
|
||||
/// Called from the gRPC thread BEFORE enqueueing to MainQueue, to ensure
|
||||
/// reconnects don't request stale data while MainQueue is blocked.
|
||||
/// </summary>
|
||||
public void UpdateResultCounts(GameUpdate update);
|
||||
|
||||
// Used for registering for stream updates
|
||||
struct ShardokViewStatus {
|
||||
public string shardokGameId;
|
||||
public Int32 filteredResultCount;
|
||||
public bool requestFullResync; // Request full state instead of delta
|
||||
}
|
||||
|
||||
struct StreamingTextStatus {
|
||||
|
||||
@@ -21,6 +21,8 @@ namespace eagle {
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
private void SetDefaultProvinceColor(ProvinceId provinceId) {
|
||||
// Model may be null during reconnection when UI is being reset
|
||||
if (Model?.Provinces == null || !Model.Provinces.ContainsKey(provinceId)) { return; }
|
||||
SetProvinceColor(provinceId, ColorForProvince(Model.Provinces[provinceId]));
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,14 @@ namespace eagle {
|
||||
}
|
||||
}
|
||||
|
||||
// Compare hero lists by ID since HeroView is a protobuf message with reference equality
|
||||
private static bool HeroListsMatch(List<HeroView> a, List<HeroView> b) {
|
||||
if (a == null && b == null) return true;
|
||||
if (a == null || b == null) return false;
|
||||
if (a.Count != b.Count) return false;
|
||||
return a.Select(h => h.Id).SequenceEqual(b.Select(h => h.Id));
|
||||
}
|
||||
|
||||
public void AddNote(
|
||||
string title,
|
||||
string text,
|
||||
@@ -48,7 +56,8 @@ namespace eagle {
|
||||
List<HeroView> displayedHeroes) {
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
var existingNote = _notes.FirstOrDefault(
|
||||
n => n.Title == title && n.DisplayedHeroes.SequenceEqual(displayedHeroes));
|
||||
n => n.Title == title &&
|
||||
HeroListsMatch(n.DisplayedHeroes, displayedHeroes));
|
||||
|
||||
if (existingNote == null) {
|
||||
_notes.Enqueue(new Notification(
|
||||
|
||||
+3
@@ -37,6 +37,9 @@ namespace eagle.Notifications {
|
||||
{ OutlawSpottedDetails, OutlawSpottedDetailsNotificationGenerator.Generator },
|
||||
{ PrisonerExchangeDetails, PrisonerExchangeDetailsNotificationGenerator.Generator },
|
||||
{ PrisonerExecutedDetails, PrisonerExecutedDetailsNotificationGenerator.Generator },
|
||||
{ PrisonerReleasedDetails, PrisonerReleasedDetailsNotificationGenerator.Generator },
|
||||
{ PrisonerExiledDetails, PrisonerExiledDetailsNotificationGenerator.Generator },
|
||||
{ PrisonerReturnedDetails, PrisonerReturnedDetailsNotificationGenerator.Generator },
|
||||
{ ProvinceHeldDetails, ProvinceHeldDetailsNotificationGenerator.Generator },
|
||||
{ QuestFailed, QuestFailedDetailsNotificationGenerator.Generator },
|
||||
{ QuestFulfilled, QuestFulfilledDetailsNotificationGenerator.Generator },
|
||||
|
||||
+3
-2
@@ -43,11 +43,12 @@ namespace eagle.Notifications.ARNNotifications {
|
||||
heroPlaceholders["ExecutedHero"] = (hero.NameTextId, "the prisoner");
|
||||
}
|
||||
|
||||
yield return new DynamicTextNotification(
|
||||
yield return DynamicTextNotification.StreamingDynamicNotification(
|
||||
title: noteTitle,
|
||||
textTemplate: textTemplate,
|
||||
heroPlaceholders: heroPlaceholders,
|
||||
singleProvinceId: note.ProvinceId,
|
||||
llmId: notification.LlmId,
|
||||
provinceIds: new List<int> { note.ProvinceId },
|
||||
displayedHeroes: new List<HeroView> { factionLeader, hero });
|
||||
}
|
||||
}
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
using System.Collections.Generic;
|
||||
using Net.Eagle0.Eagle.Views;
|
||||
|
||||
namespace eagle.Notifications.ARNNotifications {
|
||||
public static class PrisonerExiledDetailsNotificationGenerator {
|
||||
public static readonly ARNNotificationGenerator Generator = GenerateNotifications;
|
||||
|
||||
private static IEnumerable<Notification> GenerateNotifications(
|
||||
Net.Eagle0.Eagle.Common.Notification notification,
|
||||
IGameModel currentModel) {
|
||||
var note = notification.Details.PrisonerExiledDetails;
|
||||
|
||||
var hero = currentModel.Heroes[note.ExiledHeroId];
|
||||
var exilingFactionName = currentModel.FactionName(note.ExilingFactionId);
|
||||
|
||||
var factionLeader =
|
||||
currentModel.Heroes[currentModel.ActiveFactions[note.ExilingFactionId]
|
||||
.FactionHeadId];
|
||||
|
||||
string noteTitle = "Prisoner Exiled";
|
||||
string textTemplate;
|
||||
|
||||
var heroPlaceholders = new Dictionary<string, (string nameTextId, string fallback)>();
|
||||
|
||||
if (note.LastFactionId is {} lastFactionId) {
|
||||
var lastFaction = currentModel.MaybeDestroyedFaction(lastFactionId);
|
||||
|
||||
string victimDescription;
|
||||
if (lastFaction.FactionHeadId == hero.Id) {
|
||||
victimDescription = "faction leader {HeroName}";
|
||||
} else if (lastFaction.Leaders.Contains(hero.Id)) {
|
||||
victimDescription =
|
||||
$"{DisplayNames.ResolvedFactionString(lastFaction, currentModel)}'s sworn {DisplayNames.SiblingDescription(hero.PronounGender)} {{HeroName}}";
|
||||
} else {
|
||||
victimDescription =
|
||||
$"{DisplayNames.ResolvedFactionString(lastFaction, currentModel)}'s hero {{HeroName}}";
|
||||
}
|
||||
|
||||
textTemplate = $"{exilingFactionName} has exiled {victimDescription}!\n\n";
|
||||
heroPlaceholders["HeroName"] = (hero.NameTextId, "the hero");
|
||||
} else {
|
||||
textTemplate = $"{exilingFactionName} has exiled {{ExiledHero}}!\n\n";
|
||||
heroPlaceholders["ExiledHero"] = (hero.NameTextId, "the prisoner");
|
||||
}
|
||||
|
||||
yield return DynamicTextNotification.StreamingDynamicNotification(
|
||||
title: noteTitle,
|
||||
textTemplate: textTemplate,
|
||||
heroPlaceholders: heroPlaceholders,
|
||||
llmId: notification.LlmId,
|
||||
provinceIds: new List<int> { note.ProvinceId },
|
||||
displayedHeroes: new List<HeroView> { factionLeader, hero });
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: be877c33ab14e4b37b6402ca0826efa1
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
using System.Collections.Generic;
|
||||
using Net.Eagle0.Eagle.Views;
|
||||
|
||||
namespace eagle.Notifications.ARNNotifications {
|
||||
public static class PrisonerReleasedDetailsNotificationGenerator {
|
||||
public static readonly ARNNotificationGenerator Generator = GenerateNotifications;
|
||||
|
||||
private static IEnumerable<Notification> GenerateNotifications(
|
||||
Net.Eagle0.Eagle.Common.Notification notification,
|
||||
IGameModel currentModel) {
|
||||
var note = notification.Details.PrisonerReleasedDetails;
|
||||
|
||||
var hero = currentModel.Heroes[note.ReleasedHeroId];
|
||||
var releasingFactionName = currentModel.FactionName(note.ReleasingFactionId);
|
||||
|
||||
var factionLeader =
|
||||
currentModel.Heroes[currentModel.ActiveFactions[note.ReleasingFactionId]
|
||||
.FactionHeadId];
|
||||
|
||||
string noteTitle = "Prisoner Released";
|
||||
string textTemplate;
|
||||
|
||||
var heroPlaceholders = new Dictionary<string, (string nameTextId, string fallback)>();
|
||||
|
||||
if (note.LastFactionId is {} lastFactionId) {
|
||||
var lastFaction = currentModel.MaybeDestroyedFaction(lastFactionId);
|
||||
|
||||
string victimDescription;
|
||||
if (lastFaction.FactionHeadId == hero.Id) {
|
||||
victimDescription = "faction leader {HeroName}";
|
||||
} else if (lastFaction.Leaders.Contains(hero.Id)) {
|
||||
victimDescription =
|
||||
$"{DisplayNames.ResolvedFactionString(lastFaction, currentModel)}'s sworn {DisplayNames.SiblingDescription(hero.PronounGender)} {{HeroName}}";
|
||||
} else {
|
||||
victimDescription =
|
||||
$"{DisplayNames.ResolvedFactionString(lastFaction, currentModel)}'s hero {{HeroName}}";
|
||||
}
|
||||
|
||||
textTemplate = $"{releasingFactionName} has released {victimDescription}!\n\n";
|
||||
heroPlaceholders["HeroName"] = (hero.NameTextId, "the hero");
|
||||
} else {
|
||||
textTemplate = $"{releasingFactionName} has released {{ReleasedHero}}!\n\n";
|
||||
heroPlaceholders["ReleasedHero"] = (hero.NameTextId, "the prisoner");
|
||||
}
|
||||
|
||||
yield return DynamicTextNotification.StreamingDynamicNotification(
|
||||
title: noteTitle,
|
||||
textTemplate: textTemplate,
|
||||
heroPlaceholders: heroPlaceholders,
|
||||
llmId: notification.LlmId,
|
||||
provinceIds: new List<int> { note.ProvinceId },
|
||||
displayedHeroes: new List<HeroView> { factionLeader, hero });
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 21603b7e9604d459ea6141801d820f2e
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
using System.Collections.Generic;
|
||||
using Net.Eagle0.Eagle.Views;
|
||||
|
||||
namespace eagle.Notifications.ARNNotifications {
|
||||
public static class PrisonerReturnedDetailsNotificationGenerator {
|
||||
public static readonly ARNNotificationGenerator Generator = GenerateNotifications;
|
||||
|
||||
private static IEnumerable<Notification> GenerateNotifications(
|
||||
Net.Eagle0.Eagle.Common.Notification notification,
|
||||
IGameModel currentModel) {
|
||||
var note = notification.Details.PrisonerReturnedDetails;
|
||||
|
||||
var hero = currentModel.Heroes[note.ReturnedHeroId];
|
||||
var returningFactionName = currentModel.FactionName(note.ReturningFactionId);
|
||||
var toFactionName = currentModel.FactionName(note.ToFactionId);
|
||||
|
||||
var factionLeader =
|
||||
currentModel.Heroes[currentModel.ActiveFactions[note.ReturningFactionId]
|
||||
.FactionHeadId];
|
||||
|
||||
string noteTitle = "Prisoner Returned";
|
||||
string textTemplate;
|
||||
|
||||
var heroPlaceholders = new Dictionary<string, (string nameTextId, string fallback)>();
|
||||
|
||||
if (note.LastFactionId is {} lastFactionId) {
|
||||
var lastFaction = currentModel.MaybeDestroyedFaction(lastFactionId);
|
||||
|
||||
string victimDescription;
|
||||
if (lastFaction.FactionHeadId == hero.Id) {
|
||||
victimDescription = "faction leader {HeroName}";
|
||||
} else if (lastFaction.Leaders.Contains(hero.Id)) {
|
||||
victimDescription =
|
||||
$"{DisplayNames.ResolvedFactionString(lastFaction, currentModel)}'s sworn {DisplayNames.SiblingDescription(hero.PronounGender)} {{HeroName}}";
|
||||
} else {
|
||||
victimDescription =
|
||||
$"{DisplayNames.ResolvedFactionString(lastFaction, currentModel)}'s hero {{HeroName}}";
|
||||
}
|
||||
|
||||
textTemplate =
|
||||
$"{returningFactionName} has returned {victimDescription} to {toFactionName}!\n\n";
|
||||
heroPlaceholders["HeroName"] = (hero.NameTextId, "the hero");
|
||||
} else {
|
||||
textTemplate =
|
||||
$"{returningFactionName} has returned {{ReturnedHero}} to {toFactionName}!\n\n";
|
||||
heroPlaceholders["ReturnedHero"] = (hero.NameTextId, "the prisoner");
|
||||
}
|
||||
|
||||
yield return DynamicTextNotification.StreamingDynamicNotification(
|
||||
title: noteTitle,
|
||||
textTemplate: textTemplate,
|
||||
heroPlaceholders: heroPlaceholders,
|
||||
llmId: notification.LlmId,
|
||||
provinceIds: new List<int> { note.ProvinceId },
|
||||
displayedHeroes: new List<HeroView> { factionLeader, hero });
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9c378d55ca4214d53b63c2bb14a614a4
|
||||
+38
-9
@@ -6,14 +6,42 @@ namespace eagle.Notifications.ARNNotifications {
|
||||
public static class ProfessionGainedDetailsNotificationGenerator {
|
||||
public static readonly ARNNotificationGenerator Generator = GenerateNotifications;
|
||||
|
||||
private static readonly Dictionary<Profession, string> ProfessionNames =
|
||||
new Dictionary<Profession, string> {
|
||||
{ Profession.Mage, "Mage" },
|
||||
{ Profession.Necromancer, "Necromancer" },
|
||||
{ Profession.Engineer, "Engineer" },
|
||||
{ Profession.Paladin, "Paladin" },
|
||||
{ Profession.Ranger, "Ranger" },
|
||||
{ Profession.Champion, "Champion" }
|
||||
};
|
||||
|
||||
private static readonly Dictionary<Profession, string> ProfessionTitles =
|
||||
new Dictionary<Profession, string> {
|
||||
{ Profession.Mage, "Arcane Awakening" },
|
||||
{ Profession.Necromancer, "Dark Pact Sealed" },
|
||||
{ Profession.Engineer, "Genius Unleashed" },
|
||||
{ Profession.Paladin, "Divine Calling" },
|
||||
{ Profession.Ranger, "One with the Wild" },
|
||||
{ Profession.Champion, "Born for Battle" }
|
||||
};
|
||||
|
||||
private static string GetProfessionName(Profession profession) {
|
||||
return profession switch { Profession.Mage => "Mage",
|
||||
Profession.Necromancer => "Necromancer",
|
||||
Profession.Engineer => "Engineer",
|
||||
Profession.Paladin => "Paladin",
|
||||
Profession.Ranger => "Ranger",
|
||||
Profession.Champion => "Champion",
|
||||
_ => "Unknown" };
|
||||
return ProfessionNames.TryGetValue(profession, out var name) ? name : "Unknown";
|
||||
}
|
||||
|
||||
private static string GetArticle(string word) {
|
||||
if (string.IsNullOrEmpty(word)) return "a";
|
||||
char firstChar = char.ToLower(word[0]);
|
||||
return (firstChar == 'a' || firstChar == 'e' || firstChar == 'i' || firstChar == 'o' ||
|
||||
firstChar == 'u')
|
||||
? "an"
|
||||
: "a";
|
||||
}
|
||||
|
||||
private static string GetProfessionTitle(Profession profession) {
|
||||
return ProfessionTitles.TryGetValue(profession, out var title) ? title
|
||||
: "Profession Gained";
|
||||
}
|
||||
|
||||
private static IEnumerable<Notification> GenerateNotifications(
|
||||
@@ -23,18 +51,19 @@ namespace eagle.Notifications.ARNNotifications {
|
||||
var hero = currentModel.Heroes[details.HeroId];
|
||||
var factionName = currentModel.FactionName(details.FactionId);
|
||||
var professionName = GetProfessionName(details.NewProfession);
|
||||
var article = GetArticle(professionName);
|
||||
|
||||
var affectedProvinces = currentModel.ProvincesForFaction(details.FactionId);
|
||||
|
||||
string textTemplate =
|
||||
$"{{Hero}} of {factionName} gained the {professionName} profession.\n\n";
|
||||
$"{{Hero}} of {factionName} became {article} {professionName}.\n\n";
|
||||
|
||||
var heroPlaceholders = new Dictionary<string, (string nameTextId, string fallback)> {
|
||||
{ "Hero", (hero.NameTextId, "A hero") }
|
||||
};
|
||||
|
||||
yield return DynamicTextNotification.StreamingDynamicNotification(
|
||||
title: "Profession Gained",
|
||||
title: GetProfessionTitle(details.NewProfession),
|
||||
textTemplate: textTemplate,
|
||||
heroPlaceholders: heroPlaceholders,
|
||||
llmId: notification.LlmId,
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 11e2f9347861144c8983e71df3abef74
|
||||
+5
@@ -102,6 +102,11 @@ namespace eagle.Notifications {
|
||||
}
|
||||
|
||||
public void Append(string text, List<ProvinceId> provinceIds) {
|
||||
// Skip if this exact text is already in the notification (prevents duplicates
|
||||
// when the same update is processed multiple times, e.g., after resuming from
|
||||
// background)
|
||||
if (Text.Contains(text)) { return; }
|
||||
|
||||
if (ShouldAppend) {
|
||||
Text += "\n" + text;
|
||||
ProvinceIds.AddRange(provinceIds);
|
||||
|
||||
+527
-108
@@ -16,6 +16,14 @@ namespace eagle {
|
||||
using ShardokGameId = String;
|
||||
using ProvinceId = Int32;
|
||||
|
||||
public enum ConnectionState {
|
||||
Disconnected,
|
||||
Connecting,
|
||||
Connected,
|
||||
Reconnecting,
|
||||
SubscriptionPending // Connected but waiting for subscription acknowledgment
|
||||
}
|
||||
|
||||
public interface ILobbySubscriber {
|
||||
void ReceiveLobbyUpdate(LobbyResponse lobbyResponse);
|
||||
void ReceivePregeneratedTextUpdate(PregeneratedTextResponse response);
|
||||
@@ -26,20 +34,95 @@ namespace eagle {
|
||||
|
||||
public class PersistentClientConnection : IDisposable {
|
||||
private const double TimeoutSeconds = 300.0;
|
||||
private const double HeartbeatTimerSeconds = 10.0;
|
||||
// Idle timeout = 2x HTTP/2 keepalive interval (15s * 2 = 30s)
|
||||
private const double MaxIdleSeconds = 30.0;
|
||||
|
||||
private DateTime _lastResponseReceived = DateTime.UtcNow;
|
||||
private int _lastIdleWarningLevel = 0; // 0=none, 1=10s warning, 2=20s warning
|
||||
private readonly Metadata _credentials;
|
||||
private readonly CancellationToken _cancellationToken;
|
||||
|
||||
private readonly Logger _remoteEagleClientLogger = Logger.GetLogger("ConnectionLogger");
|
||||
private readonly Logger _timingsLogger = Logger.GetLogger("ConnectionLogger");
|
||||
private Timer _timer = null;
|
||||
private volatile bool _isConnecting = false;
|
||||
private Timer _idleCheckTimer = null;
|
||||
private Timer _heartbeatTimer = null;
|
||||
private const double HeartbeatIntervalSeconds = 10.0;
|
||||
|
||||
// Connection metrics for diagnostics
|
||||
private DateTime? _lastConnectAttempt = null;
|
||||
private DateTime? _lastSuccessfulConnect = null;
|
||||
private DateTime? _lastDisconnect = null;
|
||||
private StatusCode? _lastDisconnectReason = null;
|
||||
|
||||
// Exponential backoff for reconnection
|
||||
private int _consecutiveFailures = 0;
|
||||
private const double MinBackoffSeconds = 2.0;
|
||||
private const double MaxBackoffSeconds = 32.0;
|
||||
|
||||
// Connection state for UI monitoring
|
||||
private ConnectionState _currentState = ConnectionState.Disconnected;
|
||||
public ConnectionState CurrentState => _currentState;
|
||||
public DateTime? NextReconnectAttempt { get; private set; } = null;
|
||||
|
||||
// Circuit breaker for preventing cascading failures
|
||||
private ConnectionCircuitBreaker _circuitBreaker = new ConnectionCircuitBreaker();
|
||||
public ConnectionCircuitBreaker CircuitBreaker => _circuitBreaker;
|
||||
|
||||
private DateTime? GetDeadlineFromNow() {
|
||||
return DateTime.UtcNow.AddSeconds(TimeoutSeconds);
|
||||
}
|
||||
|
||||
private int GetTotalShardokGames() {
|
||||
int total = 0;
|
||||
lock (this) {
|
||||
foreach (var sub in _subscribers.Values) { total += sub.ShardokViewStatuses.Count; }
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
private void
|
||||
LogConnectionEvent(string eventType, string details = null, StatusCode? statusCode = null) {
|
||||
var timestamp = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss.fff");
|
||||
var shardokCount = GetTotalShardokGames();
|
||||
var shardokInfo = shardokCount > 0 ? $" shardok_games={shardokCount}" : "";
|
||||
var statusInfo = statusCode.HasValue ? $" status={statusCode.Value}" : "";
|
||||
var detailsInfo = !string.IsNullOrEmpty(details) ? $" details=\"{details}\"" : "";
|
||||
var timeSinceLastConnect =
|
||||
_lastSuccessfulConnect.HasValue
|
||||
? $" seconds_since_connect={(DateTime.UtcNow - _lastSuccessfulConnect.Value).TotalSeconds:F1}"
|
||||
: "";
|
||||
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"[CONNECTION] timestamp={timestamp} event={eventType}{shardokInfo}{statusInfo}{detailsInfo}{timeSinceLastConnect}");
|
||||
}
|
||||
|
||||
private double GetBackoffSeconds() {
|
||||
if (_consecutiveFailures == 0) { return MinBackoffSeconds; }
|
||||
|
||||
// Exponential backoff: 2, 4, 8, 16, 32 (capped)
|
||||
double backoff = MinBackoffSeconds * Math.Pow(2, _consecutiveFailures);
|
||||
return Math.Min(backoff, MaxBackoffSeconds);
|
||||
}
|
||||
|
||||
private void ScheduleReconnect(string reason) {
|
||||
double backoffSeconds = GetBackoffSeconds();
|
||||
_consecutiveFailures++;
|
||||
_currentState = ConnectionState.Reconnecting;
|
||||
NextReconnectAttempt = DateTime.UtcNow.AddSeconds(backoffSeconds);
|
||||
|
||||
LogConnectionEvent(
|
||||
"schedule_reconnect",
|
||||
$"{reason}, backoff={backoffSeconds:F1}s, attempt={_consecutiveFailures}");
|
||||
|
||||
_retryTimer?.Dispose();
|
||||
_retryTimer = new Timer();
|
||||
_retryTimer.AutoReset = false;
|
||||
_retryTimer.Interval = backoffSeconds * 1000;
|
||||
_retryTimer.Elapsed += (sender, args) => Task.Run(() => Connect());
|
||||
_retryTimer.Enabled = true;
|
||||
}
|
||||
|
||||
private readonly Eagle.EagleClient _grpcClient;
|
||||
private AsyncDuplexStreamingCall<UpdateStreamRequest, UpdateStreamResponse> _streamingCall;
|
||||
|
||||
@@ -51,6 +134,11 @@ namespace eagle {
|
||||
|
||||
private List<PostCommandRequest> _pendingCommands = new();
|
||||
|
||||
// Pending subscription acknowledgments - used to wait for server confirmation
|
||||
private readonly Dictionary<EagleGameId, TaskCompletionSource<SubscriptionAck>>
|
||||
_pendingSubscriptionAcks = new();
|
||||
private const int SubscriptionAckTimeoutMs = 10000; // 10 seconds
|
||||
|
||||
public PersistentClientConnection(
|
||||
Eagle.EagleClient grpcClient,
|
||||
Metadata credentials,
|
||||
@@ -60,17 +148,27 @@ namespace eagle {
|
||||
_cancellationToken = cancellationToken;
|
||||
}
|
||||
|
||||
private void StreamOneGame(IClientConnectionSubscriber subscriber) {
|
||||
/// <summary>
|
||||
/// Sends a subscription request for a game and awaits server acknowledgment.
|
||||
/// Returns true if the subscription was acknowledged successfully, false otherwise.
|
||||
/// IMPORTANT: Must be called outside of lock(this) to allow awaiting.
|
||||
/// </summary>
|
||||
private async Task<bool> StreamOneGameAsync(IClientConnectionSubscriber subscriber) {
|
||||
var gameId = subscriber.GameId;
|
||||
var streamGameRequest = new StreamGameRequest {
|
||||
GameId = subscriber.GameId,
|
||||
GameId = gameId,
|
||||
UnfilteredResultCount = subscriber.LastUnfilteredResultCount
|
||||
};
|
||||
|
||||
streamGameRequest.ShardokViewStatuses.AddRange(subscriber.ShardokViewStatuses.Select(
|
||||
// Get Shardok statuses (may include resync flags)
|
||||
var shardokStatuses = subscriber.ShardokViewStatuses;
|
||||
streamGameRequest.ShardokViewStatuses.AddRange(shardokStatuses.Select(
|
||||
status => new StreamGameRequest.Types.ShardokViewStatus {
|
||||
FilteredResultCount = status.filteredResultCount,
|
||||
ShardokGameId = status.shardokGameId
|
||||
ShardokGameId = status.shardokGameId,
|
||||
RequestFullResync = status.requestFullResync
|
||||
}));
|
||||
|
||||
streamGameRequest.StreamingTextStatuses.AddRange(
|
||||
subscriber.StreamingTextStatuses.Select(
|
||||
status => new StreamGameRequest.Types.StreamingTextStatus {
|
||||
@@ -81,50 +179,207 @@ namespace eagle {
|
||||
|
||||
var request = new UpdateStreamRequest { StreamGameRequest = streamGameRequest };
|
||||
|
||||
DoWithStreamingCall(async (sc) => {
|
||||
// Create a TaskCompletionSource to wait for the server's acknowledgment
|
||||
var ackTcs = new TaskCompletionSource<SubscriptionAck>();
|
||||
lock (_pendingSubscriptionAcks) { _pendingSubscriptionAcks[gameId] = ackTcs; }
|
||||
|
||||
_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;
|
||||
});
|
||||
|
||||
if (!sendSuccess) {
|
||||
// Write failed - clean up and return
|
||||
lock (_pendingSubscriptionAcks) { _pendingSubscriptionAcks.Remove(gameId); }
|
||||
LogConnectionEvent("subscribe_send_failed", $"game={gameId}");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Wait for server acknowledgment with timeout
|
||||
using var timeoutCts = new CancellationTokenSource();
|
||||
try {
|
||||
var ackTask = ackTcs.Task;
|
||||
var timeoutTask = Task.Delay(SubscriptionAckTimeoutMs, timeoutCts.Token);
|
||||
var completedTask = await Task.WhenAny(ackTask, timeoutTask);
|
||||
|
||||
if (completedTask == timeoutTask) {
|
||||
// Timeout waiting for ack
|
||||
lock (_pendingSubscriptionAcks) { _pendingSubscriptionAcks.Remove(gameId); }
|
||||
LogConnectionEvent("subscribe_ack_timeout", $"game={gameId}");
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"[SUBSCRIBE] Timeout waiting for SubscriptionAck for game {gameId}");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cancel the timeout task since ack was received
|
||||
timeoutCts.Cancel();
|
||||
|
||||
var ack = await ackTask;
|
||||
if (ack.Success) {
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"[SUBSCRIBE] Subscription confirmed for game {gameId}, " +
|
||||
$"confirmedResultCount={ack.ConfirmedResultCount}");
|
||||
|
||||
// Clear resync flags ONLY after successful acknowledgment
|
||||
if (subscriber is GameModelUpdater updater) {
|
||||
foreach (var status in shardokStatuses.Where(s => s.requestFullResync)) {
|
||||
updater.ClearShardokResyncFlag(status.shardokGameId);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
LogConnectionEvent(
|
||||
"subscribe_ack_failed",
|
||||
$"game={gameId}, error={ack.ErrorMessage}");
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"[SUBSCRIBE] Server rejected subscription for game {gameId}: {ack.ErrorMessage}");
|
||||
return false;
|
||||
}
|
||||
} catch (OperationCanceledException) {
|
||||
// Task was cancelled (e.g., connection drop) - not an error
|
||||
lock (_pendingSubscriptionAcks) { _pendingSubscriptionAcks.Remove(gameId); }
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"[SUBSCRIBE] Cancelled waiting for ack for game {gameId}");
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
lock (_pendingSubscriptionAcks) { _pendingSubscriptionAcks.Remove(gameId); }
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"[SUBSCRIBE] Exception waiting for ack for game {gameId}: {e.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Connect() {
|
||||
_remoteEagleClientLogger.LogLine($"Connect() called");
|
||||
|
||||
// Dispose existing streaming call before creating new one
|
||||
lock (this) {
|
||||
_streamingCall?.Dispose();
|
||||
_streamingCall = null;
|
||||
|
||||
// Cancel and dispose existing thread cancellation token
|
||||
_threadCancellationTokenSource?.Cancel();
|
||||
_threadCancellationTokenSource?.Dispose();
|
||||
_threadCancellationTokenSource = new CancellationTokenSource();
|
||||
_currentThreadToken = _threadCancellationTokenSource.Token;
|
||||
// Prevent concurrent connection attempts
|
||||
if (_isConnecting) {
|
||||
_remoteEagleClientLogger.LogLine($"Connect() skipped - already connecting");
|
||||
return;
|
||||
}
|
||||
|
||||
var call = _grpcClient.StreamUpdates(
|
||||
headers: _credentials,
|
||||
deadline: GetDeadlineFromNow(),
|
||||
cancellationToken: _cancellationToken);
|
||||
Queue<PostCommandRequest> pendingCommands = new Queue<PostCommandRequest>();
|
||||
try {
|
||||
lock (this) {
|
||||
_streamingCall = call;
|
||||
_streamingCallThread = new Thread(HandleStreamingCall);
|
||||
_streamingCallThread.Start();
|
||||
// Check circuit breaker
|
||||
if (!_circuitBreaker.ShouldAttemptConnection()) {
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"[CIRCUIT] Connection attempt blocked - circuit {_circuitBreaker.CurrentState}");
|
||||
LogConnectionEvent("connect_blocked", $"circuit={_circuitBreaker.CurrentState}");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var kv in _subscribers) { StreamOneGame(kv.Value); }
|
||||
_isConnecting = true;
|
||||
try {
|
||||
_lastConnectAttempt = DateTime.UtcNow;
|
||||
_currentState = _consecutiveFailures > 0 ? ConnectionState.Reconnecting
|
||||
: ConnectionState.Connecting;
|
||||
NextReconnectAttempt = null;
|
||||
LogConnectionEvent("connect_attempt");
|
||||
|
||||
// Dispose existing streaming call before creating new one
|
||||
lock (this) {
|
||||
_streamingCall?.Dispose();
|
||||
_streamingCall = null;
|
||||
|
||||
// Cancel and dispose existing thread cancellation token
|
||||
_threadCancellationTokenSource?.Cancel();
|
||||
_threadCancellationTokenSource?.Dispose();
|
||||
_threadCancellationTokenSource = new CancellationTokenSource();
|
||||
_currentThreadToken = _threadCancellationTokenSource.Token;
|
||||
}
|
||||
|
||||
await TryPendingCommands();
|
||||
} catch (Exception e) {
|
||||
_remoteEagleClientLogger.LogLine($"Got exception {e} in Connect()");
|
||||
lock (this) {
|
||||
while (pendingCommands.Any()) {
|
||||
_pendingCommands.Add(pendingCommands.Dequeue());
|
||||
var call = _grpcClient.StreamUpdates(
|
||||
headers: _credentials,
|
||||
deadline: GetDeadlineFromNow(),
|
||||
cancellationToken: _cancellationToken);
|
||||
Queue<PostCommandRequest> pendingCommands = new Queue<PostCommandRequest>();
|
||||
try {
|
||||
List<IClientConnectionSubscriber> subscribersToStream;
|
||||
lock (this) {
|
||||
_streamingCall = call;
|
||||
_streamingCallThread = new Thread(HandleStreamingCall);
|
||||
_streamingCallThread.Start();
|
||||
|
||||
// Collect subscribers under lock
|
||||
subscribersToStream = _subscribers.Values.ToList();
|
||||
}
|
||||
|
||||
// Stream subscriptions OUTSIDE lock (can await)
|
||||
// Set state to SubscriptionPending while waiting for acks
|
||||
if (subscribersToStream.Any()) {
|
||||
_currentState = ConnectionState.SubscriptionPending;
|
||||
}
|
||||
|
||||
bool allSucceeded = true;
|
||||
foreach (var subscriber in subscribersToStream) {
|
||||
if (!await StreamOneGameAsync(subscriber)) { allSucceeded = false; }
|
||||
}
|
||||
|
||||
if (!allSucceeded) {
|
||||
// Subscription failed - this is critical because the game won't receive
|
||||
// updates. Trigger reconnect rather than proceeding to Connected state.
|
||||
LogConnectionEvent(
|
||||
"subscribe_failed",
|
||||
"Subscription failed - triggering reconnect");
|
||||
|
||||
// Cleanup resources before reconnecting (consistent with other failure
|
||||
// paths in HandleStreamingCall). This prevents the background thread from
|
||||
// triggering its own reconnect when Connect() disposes the streaming call.
|
||||
lock (this) {
|
||||
_streamingCall?.Dispose();
|
||||
_streamingCall = null;
|
||||
_threadCancellationTokenSource?.Cancel();
|
||||
}
|
||||
MarkAllShardokGamesForResync();
|
||||
CancelAllPendingSubscriptionAcks();
|
||||
|
||||
_circuitBreaker.RecordFailure();
|
||||
ScheduleReconnect("SubscriptionFailed");
|
||||
return;
|
||||
}
|
||||
|
||||
_lastSuccessfulConnect = DateTime.UtcNow;
|
||||
_consecutiveFailures = 0; // Reset backoff on successful connection
|
||||
_currentState = ConnectionState.Connected;
|
||||
_circuitBreaker.RecordSuccess();
|
||||
LogConnectionEvent("connect_success");
|
||||
|
||||
// CRITICAL: Cancel any pending scheduled reconnect to prevent it from
|
||||
// firing and killing this working connection
|
||||
if (_retryTimer != null) {
|
||||
_retryTimer.Enabled = false;
|
||||
_retryTimer.Dispose();
|
||||
_retryTimer = null;
|
||||
}
|
||||
|
||||
// CRITICAL: Reset idle timer baseline before starting idle check
|
||||
// Otherwise, on reconnect, _lastResponseReceived still has the old
|
||||
// timestamp and CheckForIdleTimeout() will immediately kill this
|
||||
// new connection thinking it's been idle for minutes
|
||||
_lastResponseReceived = DateTime.UtcNow;
|
||||
_lastIdleWarningLevel = 0;
|
||||
|
||||
// Start monitoring for idle timeout (relies on HTTP/2 keepalive)
|
||||
StartIdleCheckTimer();
|
||||
|
||||
// Start sending heartbeats with sync status
|
||||
StartHeartbeatTimer();
|
||||
|
||||
await TryPendingCommands();
|
||||
} catch (Exception e) {
|
||||
_circuitBreaker.RecordFailure();
|
||||
LogConnectionEvent("connect_failed", e.GetType().Name);
|
||||
_remoteEagleClientLogger.LogLine($"Got exception {e} in Connect()");
|
||||
lock (this) {
|
||||
while (pendingCommands.Any()) {
|
||||
_pendingCommands.Add(pendingCommands.Dequeue());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally { _isConnecting = false; }
|
||||
}
|
||||
|
||||
private async Task<bool> TryPendingCommands() {
|
||||
@@ -229,6 +484,10 @@ namespace eagle {
|
||||
|
||||
public void Disconnect() {
|
||||
lock (this) {
|
||||
_lastDisconnect = DateTime.UtcNow;
|
||||
LogConnectionEvent("disconnect_explicit");
|
||||
StopIdleCheckTimer();
|
||||
StopHeartbeatTimer();
|
||||
_streamingCall?.Dispose();
|
||||
_streamingCall = null;
|
||||
}
|
||||
@@ -237,11 +496,8 @@ namespace eagle {
|
||||
public void Dispose() {
|
||||
lock (this) {
|
||||
// Dispose timers first to stop any pending callbacks
|
||||
if (_timer != null) {
|
||||
_timer.Enabled = false;
|
||||
_timer.Dispose();
|
||||
_timer = null;
|
||||
}
|
||||
StopIdleCheckTimer();
|
||||
StopHeartbeatTimer();
|
||||
|
||||
if (_retryTimer != null) {
|
||||
_retryTimer.Enabled = false;
|
||||
@@ -280,11 +536,19 @@ namespace eagle {
|
||||
_lobbySubscriber = subscriber;
|
||||
}
|
||||
|
||||
public void Subscribe(IClientConnectionSubscriber subscriber) {
|
||||
/// <summary>
|
||||
/// Subscribe to a game and wait for server acknowledgment.
|
||||
/// Returns true if subscription was acknowledged, false if it failed or timed out.
|
||||
/// </summary>
|
||||
public async Task<bool> Subscribe(IClientConnectionSubscriber subscriber) {
|
||||
IClientConnectionSubscriber toStream;
|
||||
lock (this) {
|
||||
_subscribers[subscriber.GameId] = subscriber;
|
||||
StreamOneGame(subscriber);
|
||||
toStream = subscriber;
|
||||
}
|
||||
|
||||
// Stream OUTSIDE lock (can await)
|
||||
return await StreamOneGameAsync(toStream);
|
||||
}
|
||||
|
||||
public async Task PostError(EagleGameId gameId, string errorMessage, string stackTrace) {
|
||||
@@ -442,6 +706,15 @@ namespace eagle {
|
||||
case GameUpdate.GameUpdateDetailsOneofCase.ActionResultResponse:
|
||||
case GameUpdate.GameUpdateDetailsOneofCase.ShardokActionResultResponse:
|
||||
case GameUpdate.GameUpdateDetailsOneofCase.StreamingTextResponse:
|
||||
// Update result counts IMMEDIATELY on the gRPC thread, before enqueueing.
|
||||
// This ensures reconnects use accurate counts even when MainQueue is blocked
|
||||
// (e.g., when Unity is backgrounded).
|
||||
lock (this) {
|
||||
if (_subscribers.TryGetValue(gameUpdate.GameId, out var sub)) {
|
||||
sub.UpdateResultCounts(gameUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
MainQueue.Q.Enqueue(async () => {
|
||||
IClientConnectionSubscriber subscriber;
|
||||
lock (this) {
|
||||
@@ -472,14 +745,12 @@ namespace eagle {
|
||||
threadToken = _currentThreadToken;
|
||||
}
|
||||
|
||||
SetUpTimer();
|
||||
|
||||
var waitStartTime = DateTime.UtcNow;
|
||||
while (sc != null && !_currentThreadToken.IsCancellationRequested &&
|
||||
await sc.ResponseStream.MoveNext(_cancellationToken)) {
|
||||
var receivedTime = DateTime.UtcNow;
|
||||
_lastResponseReceived = receivedTime;
|
||||
SetUpTimer();
|
||||
_lastIdleWarningLevel = 0; // Reset idle warnings on data received
|
||||
var waitTime = (receivedTime - waitStartTime).TotalMilliseconds;
|
||||
if (waitTime > 100.0) { _timingsLogger.LogLine($"WAIT {waitTime} ms"); }
|
||||
|
||||
@@ -488,7 +759,7 @@ namespace eagle {
|
||||
|
||||
switch (current.ResponseDetailsCase) {
|
||||
case UpdateStreamResponse.ResponseDetailsOneofCase.HeartbeatResponse:
|
||||
_remoteEagleClientLogger.LogLine("Got a heartbeat response!");
|
||||
HandleHeartbeatResponse(current.HeartbeatResponse);
|
||||
break;
|
||||
|
||||
case UpdateStreamResponse.ResponseDetailsOneofCase.GameUpdate:
|
||||
@@ -553,6 +824,25 @@ namespace eagle {
|
||||
mapInfo.Map);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case UpdateStreamResponse.ResponseDetailsOneofCase.SubscriptionAck:
|
||||
var ack = current.SubscriptionAck;
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"[SUBSCRIBE] Received SubscriptionAck for game {ack.GameId}, " +
|
||||
$"success={ack.Success}");
|
||||
// Get TCS under lock, but call TrySetResult outside to avoid
|
||||
// potential deadlock from synchronous continuations
|
||||
TaskCompletionSource<SubscriptionAck> ackTcs = null;
|
||||
lock (_pendingSubscriptionAcks) {
|
||||
if (_pendingSubscriptionAcks.TryGetValue(
|
||||
ack.GameId,
|
||||
out ackTcs)) {
|
||||
_pendingSubscriptionAcks.Remove(ack.GameId);
|
||||
}
|
||||
}
|
||||
ackTcs?.TrySetResult(ack);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -566,113 +856,242 @@ namespace eagle {
|
||||
"How did we get here? This is not my beautiful wife!");
|
||||
} catch (RpcException e) {
|
||||
lock (this) {
|
||||
_lastDisconnect = DateTime.UtcNow;
|
||||
_lastDisconnectReason = e.StatusCode;
|
||||
|
||||
// Mark all active Shardok games for full state resync
|
||||
MarkAllShardokGamesForResync();
|
||||
CancelAllPendingSubscriptionAcks();
|
||||
|
||||
switch (e.StatusCode) {
|
||||
case StatusCode.Cancelled:
|
||||
LogConnectionEvent("disconnect", "Cancelled", StatusCode.Cancelled);
|
||||
_remoteEagleClientLogger.LogLine($"Got exception {e}");
|
||||
if (_cancellationToken.IsCancellationRequested ||
|
||||
_currentThreadToken.IsCancellationRequested) {
|
||||
return;
|
||||
}
|
||||
// Only check the main cancellation token, not the thread-specific one
|
||||
// The thread token gets cancelled during normal reconnection and
|
||||
// shouldn't prevent it
|
||||
if (_cancellationToken.IsCancellationRequested) { return; }
|
||||
|
||||
Connect();
|
||||
ScheduleReconnect("Cancelled");
|
||||
|
||||
break;
|
||||
|
||||
case StatusCode.Internal:
|
||||
LogConnectionEvent("disconnect", "Internal error", StatusCode.Internal);
|
||||
_remoteEagleClientLogger.LogLine($"Got exception {e}");
|
||||
Connect();
|
||||
ScheduleReconnect("Internal");
|
||||
break;
|
||||
|
||||
case StatusCode.DeadlineExceeded:
|
||||
LogConnectionEvent(
|
||||
"disconnect",
|
||||
"Deadline exceeded",
|
||||
StatusCode.DeadlineExceeded);
|
||||
_remoteEagleClientLogger.LogLine("Got DeadlineExceeded");
|
||||
Connect();
|
||||
ScheduleReconnect("DeadlineExceeded");
|
||||
break;
|
||||
|
||||
case StatusCode.Unavailable:
|
||||
LogConnectionEvent("disconnect", "Unavailable", StatusCode.Unavailable);
|
||||
_remoteEagleClientLogger.LogLine("Got Unavailable");
|
||||
_retryTimer?.Dispose();
|
||||
_retryTimer = new Timer();
|
||||
_retryTimer.AutoReset = false;
|
||||
_retryTimer.Interval = 5000;
|
||||
_retryTimer.Elapsed += (sender, args) => Connect();
|
||||
_retryTimer.Enabled = true;
|
||||
ScheduleReconnect("Unavailable");
|
||||
break;
|
||||
|
||||
default:
|
||||
LogConnectionEvent(
|
||||
"disconnect",
|
||||
$"Unknown: {e.StatusCode}",
|
||||
e.StatusCode);
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"Something went very wrong, caught exception {e}");
|
||||
ScheduleReconnect($"Unknown-{e.StatusCode}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (ObjectDisposedException e) {
|
||||
_lastDisconnect = DateTime.UtcNow;
|
||||
LogConnectionEvent("disconnect", "ObjectDisposed");
|
||||
_remoteEagleClientLogger.LogLine($"Got ObjectDisposedException {e}");
|
||||
Connect();
|
||||
|
||||
// Mark all active Shardok games for full state resync
|
||||
MarkAllShardokGamesForResync();
|
||||
CancelAllPendingSubscriptionAcks();
|
||||
|
||||
ScheduleReconnect("ObjectDisposed");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
private void SetUpTimer() {
|
||||
if (_timer != null) {
|
||||
_timer.Enabled = false;
|
||||
_timer.Dispose();
|
||||
_timer = null;
|
||||
/// <summary>
|
||||
/// Mark all active Shardok games for resync across all subscribers.
|
||||
/// Called when connection drops to ensure state consistency on reconnect.
|
||||
/// </summary>
|
||||
private void MarkAllShardokGamesForResync() {
|
||||
foreach (var updater in _subscribers.Values.OfType<GameModelUpdater>()) {
|
||||
updater.MarkAllShardokForResync();
|
||||
}
|
||||
|
||||
_timer = new Timer { AutoReset = false, Interval = HeartbeatTimerSeconds * 1000 };
|
||||
_timer.Elapsed += TimerFired;
|
||||
_timer.Enabled = true;
|
||||
}
|
||||
|
||||
private void SendHeartbeatRequest(
|
||||
AsyncDuplexStreamingCall<UpdateStreamRequest, UpdateStreamResponse> sc) {
|
||||
var heartbeatRequest = new UpdateStreamRequest {
|
||||
HeartbeatRequest =
|
||||
new HeartbeatRequest {
|
||||
ClientTimestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds()
|
||||
}
|
||||
/// <summary>
|
||||
/// Cancel all pending subscription acknowledgments.
|
||||
/// Called when connection drops to unblock any waiting subscription tasks.
|
||||
/// </summary>
|
||||
private void CancelAllPendingSubscriptionAcks() {
|
||||
lock (_pendingSubscriptionAcks) {
|
||||
foreach (var tcs in _pendingSubscriptionAcks.Values) { tcs.TrySetCanceled(); }
|
||||
|
||||
_pendingSubscriptionAcks.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void StartIdleCheckTimer() {
|
||||
StopIdleCheckTimer();
|
||||
_idleCheckTimer = new Timer {
|
||||
AutoReset = true,
|
||||
Interval = 5000 // Check every 5 seconds
|
||||
};
|
||||
sc.RequestStream.WriteAsync(heartbeatRequest);
|
||||
_idleCheckTimer.Elapsed += (sender, args) => CheckForIdleTimeout();
|
||||
_idleCheckTimer.Enabled = true;
|
||||
}
|
||||
|
||||
private void TimerFired(object sender, ElapsedEventArgs e) {
|
||||
if (sender != _timer) {
|
||||
// This timer was canceled; ignore
|
||||
return;
|
||||
private void StopIdleCheckTimer() {
|
||||
if (_idleCheckTimer != null) {
|
||||
_idleCheckTimer.Enabled = false;
|
||||
_idleCheckTimer.Dispose();
|
||||
_idleCheckTimer = null;
|
||||
}
|
||||
if (_cancellationToken.IsCancellationRequested ||
|
||||
_currentThreadToken.IsCancellationRequested) {
|
||||
return;
|
||||
}
|
||||
|
||||
private void CheckForIdleTimeout() {
|
||||
if (_cancellationToken.IsCancellationRequested) { return; }
|
||||
|
||||
var idleTime = (DateTime.UtcNow - _lastResponseReceived).TotalSeconds;
|
||||
|
||||
// Early warnings at 10s and 20s to help diagnose slow connections
|
||||
if (idleTime > 20.0 && _lastIdleWarningLevel < 2) {
|
||||
_lastIdleWarningLevel = 2;
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"[IDLE_WARNING] No messages received in {idleTime:F1}s (timeout at {MaxIdleSeconds}s)");
|
||||
LogConnectionEvent("idle_warning_20s", $"idle_time={idleTime:F1}s");
|
||||
} else if (idleTime > 10.0 && _lastIdleWarningLevel < 1) {
|
||||
_lastIdleWarningLevel = 1;
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"[IDLE_WARNING] No messages received in {idleTime:F1}s (timeout at {MaxIdleSeconds}s)");
|
||||
LogConnectionEvent("idle_warning_10s", $"idle_time={idleTime:F1}s");
|
||||
}
|
||||
|
||||
AsyncDuplexStreamingCall<UpdateStreamRequest, UpdateStreamResponse> sc;
|
||||
lock (this) { sc = _streamingCall; }
|
||||
if (idleTime > MaxIdleSeconds) {
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"[IDLE_TIMEOUT] No messages received in {idleTime:F1}s (max={MaxIdleSeconds}s), forcing reconnect");
|
||||
LogConnectionEvent("idle_timeout", $"idle_time={idleTime:F1}s");
|
||||
|
||||
if (sc == null) {
|
||||
// Already canceled
|
||||
return;
|
||||
}
|
||||
|
||||
_timer.Enabled = false;
|
||||
_timer.Dispose();
|
||||
_timer = null;
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var lastResponseReceived = _lastResponseReceived;
|
||||
|
||||
var secondsSinceResponse = (now - lastResponseReceived).TotalSeconds;
|
||||
if (secondsSinceResponse > 2.0 * HeartbeatTimerSeconds) {
|
||||
var toCancel = _streamingCall;
|
||||
|
||||
if (toCancel != null) {
|
||||
toCancel.Dispose();
|
||||
lock (this) {
|
||||
_streamingCall = null;
|
||||
Connect();
|
||||
lock (this) { _streamingCall = null; }
|
||||
Task.Run(() => Connect());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void StartHeartbeatTimer() {
|
||||
StopHeartbeatTimer();
|
||||
_heartbeatTimer =
|
||||
new Timer { AutoReset = true, Interval = HeartbeatIntervalSeconds * 1000 };
|
||||
_heartbeatTimer.Elapsed += (sender, args) => Task.Run(() => SendHeartbeat());
|
||||
_heartbeatTimer.Enabled = true;
|
||||
}
|
||||
|
||||
private void StopHeartbeatTimer() {
|
||||
if (_heartbeatTimer != null) {
|
||||
_heartbeatTimer.Enabled = false;
|
||||
_heartbeatTimer.Dispose();
|
||||
_heartbeatTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendHeartbeat() {
|
||||
if (_cancellationToken.IsCancellationRequested) { return; }
|
||||
if (_currentState != ConnectionState.Connected) { return; }
|
||||
|
||||
// Build sync status for all subscribed games
|
||||
var heartbeatRequest = new HeartbeatRequest {
|
||||
ClientTimestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
|
||||
};
|
||||
|
||||
List<IClientConnectionSubscriber> subscribers;
|
||||
lock (this) { subscribers = _subscribers.Values.ToList(); }
|
||||
|
||||
foreach (var subscriber in subscribers) {
|
||||
var gameSyncStatus = new GameSyncStatus {
|
||||
GameId = subscriber.GameId,
|
||||
UnfilteredResultCount = subscriber.LastUnfilteredResultCount
|
||||
};
|
||||
|
||||
foreach (var shardokStatus in subscriber.ShardokViewStatuses) {
|
||||
gameSyncStatus.ShardokSyncStatuses.Add(new ShardokSyncStatus {
|
||||
ShardokGameId = shardokStatus.shardokGameId,
|
||||
FilteredResultCount = shardokStatus.filteredResultCount
|
||||
});
|
||||
}
|
||||
|
||||
heartbeatRequest.GameSyncStatuses.Add(gameSyncStatus);
|
||||
}
|
||||
|
||||
var request = new UpdateStreamRequest { HeartbeatRequest = heartbeatRequest };
|
||||
|
||||
var sent = await SendUpdateStreamRequestAsync(request);
|
||||
if (sent) {
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"[HEARTBEAT] Sent heartbeat with {heartbeatRequest.GameSyncStatuses.Count} games");
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleHeartbeatResponse(HeartbeatResponse response) {
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"[HEARTBEAT] Got response, server_timestamp={response.ServerTimestamp}");
|
||||
|
||||
// Check for sync mismatches reported by server
|
||||
if (response.GameSyncResults.Count > 0) {
|
||||
foreach (var syncResult in response.GameSyncResults) {
|
||||
if (!syncResult.EagleInSync) {
|
||||
_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}");
|
||||
}
|
||||
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (secondsSinceResponse > HeartbeatTimerSeconds) {
|
||||
SendHeartbeatRequest(sc);
|
||||
|
||||
// Trigger resync by reconnecting - this will re-subscribe with current counts
|
||||
// and the server will send missing updates
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
"[SYNC_MISMATCH] Detected sync mismatch, triggering reconnect to resync");
|
||||
LogConnectionEvent("sync_mismatch_reconnect", "Triggering reconnect to resync");
|
||||
|
||||
// Schedule reconnect to resync
|
||||
lock (this) {
|
||||
_streamingCall?.Dispose();
|
||||
_streamingCall = null;
|
||||
_threadCancellationTokenSource?.Cancel();
|
||||
}
|
||||
MarkAllShardokGamesForResync();
|
||||
CancelAllPendingSubscriptionAcks();
|
||||
ScheduleReconnect("SyncMismatch");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@ public class EagleConnection : IDisposable {
|
||||
public readonly string playerName;
|
||||
public readonly Metadata credentials;
|
||||
public readonly string authHeader;
|
||||
private const double KeepAliveSeconds = 45.0;
|
||||
// Reduced from 45s to 15s for better NAT/firewall traversal
|
||||
private const double KeepAliveSeconds = 15.0;
|
||||
|
||||
public static CancellationToken EagleCancellationToken =>
|
||||
ConnectionKiller.EagleCancellationToken;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,12 @@ public class MainQueue : MonoBehaviour {
|
||||
private readonly Queue<Action> _actionQueue = new();
|
||||
private readonly Queue<Action> _nextUpdateQueue = new();
|
||||
|
||||
// Limit actions per frame to prevent blocking when resuming from background
|
||||
private const int MaxActionsPerFrame = 10;
|
||||
|
||||
// Track queue depth for logging
|
||||
private int _lastLoggedQueueDepth = 0;
|
||||
|
||||
private MainQueue() {}
|
||||
|
||||
void Awake() {
|
||||
@@ -15,6 +21,19 @@ public class MainQueue : MonoBehaviour {
|
||||
|
||||
// Update is called once per frame
|
||||
void Update() {
|
||||
int actionsProcessed = 0;
|
||||
int queueDepthBefore;
|
||||
|
||||
lock (_actionQueue) { queueDepthBefore = _actionQueue.Count; }
|
||||
|
||||
// Log when queue has built up (e.g., after resuming from background)
|
||||
if (queueDepthBefore > MaxActionsPerFrame && queueDepthBefore != _lastLoggedQueueDepth) {
|
||||
Debug.Log($"[MainQueue] Processing backlog: {queueDepthBefore} actions queued");
|
||||
_lastLoggedQueueDepth = queueDepthBefore;
|
||||
} else if (queueDepthBefore <= MaxActionsPerFrame) {
|
||||
_lastLoggedQueueDepth = 0;
|
||||
}
|
||||
|
||||
Action possibleAction;
|
||||
do {
|
||||
possibleAction = null;
|
||||
@@ -22,8 +41,11 @@ public class MainQueue : MonoBehaviour {
|
||||
if (_actionQueue.Count > 0) { possibleAction = _actionQueue.Dequeue(); }
|
||||
}
|
||||
|
||||
if (possibleAction != null) { possibleAction.Invoke(); }
|
||||
} while (possibleAction != null);
|
||||
if (possibleAction != null) {
|
||||
possibleAction.Invoke();
|
||||
actionsProcessed++;
|
||||
}
|
||||
} while (possibleAction != null && actionsProcessed < MaxActionsPerFrame);
|
||||
|
||||
lock (_nextUpdateQueue) {
|
||||
foreach (Action action in _nextUpdateQueue) { Enqueue(action); }
|
||||
|
||||
+13
@@ -74,6 +74,8 @@ namespace Shardok {
|
||||
public ActionResultTypeManager actionResultTypeManager;
|
||||
|
||||
public Canvas eagleCanvas;
|
||||
public TMP_Text connectionStatusLabel;
|
||||
private bool _connectionStatusUIInitialized = false;
|
||||
|
||||
public ShardokGameModel Model { get; private set; }
|
||||
|
||||
@@ -353,6 +355,17 @@ namespace Shardok {
|
||||
});
|
||||
}
|
||||
|
||||
public void SetConnection(eagle.PersistentClientConnection connection) {
|
||||
if (!_connectionStatusUIInitialized && connectionStatusLabel != null &&
|
||||
connection != null) {
|
||||
var statusUI = connectionStatusLabel.GetComponent<eagle.ConnectionStatusUI>();
|
||||
if (statusUI != null) {
|
||||
statusUI.SetConnection(connection);
|
||||
_connectionStatusUIInitialized = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateReserves() {
|
||||
reservesController.HeaderLabelText = "Reserves";
|
||||
reservesController.AllowSelection = false;
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using eagle;
|
||||
using eagle0;
|
||||
using UnityEngine;
|
||||
using Net.Eagle0.Shardok.Api;
|
||||
using Net.Eagle0.Shardok.Common;
|
||||
using UnityGoDiceInterface;
|
||||
@@ -173,7 +174,10 @@ public class ShardokGameModel {
|
||||
return null;
|
||||
}
|
||||
|
||||
public void HandleUpdates(
|
||||
/// <summary>
|
||||
/// Returns true if updates were handled successfully, false if a resync is needed.
|
||||
/// </summary>
|
||||
public bool HandleUpdates(
|
||||
IEnumerable<ActionResultView> newHistory,
|
||||
int expectedNewResultCount) {
|
||||
var existingHistoryCount = History.Count();
|
||||
@@ -182,12 +186,16 @@ public class ShardokGameModel {
|
||||
var diff = existingHistoryCount + newHistoryCount - expectedNewResultCount;
|
||||
History.RemoveRange(existingHistoryCount - diff, diff);
|
||||
} else if (expectedNewResultCount > existingHistoryCount + newHistoryCount) {
|
||||
throw new ArgumentException(
|
||||
$"Should have {expectedNewResultCount} results but we have {existingHistoryCount + newHistoryCount}");
|
||||
// Missing results - likely due to dropped packets on bad network.
|
||||
// Request a full resync instead of crashing.
|
||||
Debug.Log(
|
||||
$"ShardokGameModel: Missing results (expected {expectedNewResultCount}, have {existingHistoryCount + newHistoryCount}). Requesting resync.");
|
||||
return false;
|
||||
}
|
||||
foreach (ActionResultView entry in newHistory) { HandleNewHistoryEntry(entry); }
|
||||
|
||||
if (newHistoryCount > 0 && UpdateAction != null) { UpdateAction.Invoke(); }
|
||||
return true;
|
||||
}
|
||||
|
||||
public void HandleAvailableCommands(AvailableCommands newCommands) {
|
||||
|
||||
File diff suppressed because one or more lines are too long
+5
-5
@@ -328,9 +328,9 @@ Texture2D:
|
||||
Hash: 00000000000000000000000000000000
|
||||
m_IsAlphaChannelOptional: 0
|
||||
serializedVersion: 4
|
||||
m_Width: 0
|
||||
m_Height: 0
|
||||
m_CompleteImageSize: 0
|
||||
m_Width: 1
|
||||
m_Height: 1
|
||||
m_CompleteImageSize: 1
|
||||
m_MipsStripped: 0
|
||||
m_TextureFormat: 1
|
||||
m_MipCount: 1
|
||||
@@ -355,8 +355,8 @@ Texture2D:
|
||||
m_LightmapFormat: 0
|
||||
m_ColorSpace: 0
|
||||
m_PlatformBlob:
|
||||
image data: 0
|
||||
_typelessdata:
|
||||
image data: 1
|
||||
_typelessdata: 00
|
||||
m_StreamData:
|
||||
serializedVersion: 2
|
||||
offset: 0
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using eagle;
|
||||
using Google.Protobuf;
|
||||
@@ -18,10 +20,33 @@ namespace common {
|
||||
public static void SetUpConnection(HttpClient httpClient) {
|
||||
headshotFetcher = new("headshots", httpClient);
|
||||
}
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
// Client that doesn't follow redirects, so we can retry each hop independently
|
||||
private readonly HttpClient _noRedirectClient;
|
||||
|
||||
// Auth header to send only to eagle0.net (not to S3 signed URLs)
|
||||
private readonly System.Net.Http.Headers.AuthenticationHeaderValue _authHeader;
|
||||
|
||||
private static readonly Uri BaseUri = new("https://eagle0.net/assets/headshots/");
|
||||
|
||||
private const int MaxRetryAttempts = 5;
|
||||
private const int MaxRedirectHops = 5;
|
||||
private const int RequestTimeoutSeconds = 10;
|
||||
private static readonly int[] RetryDelaysMs = { 500, 1000, 2000, 3000, 5000 };
|
||||
private const int PeriodicRetryIntervalMs = 30000; // 30 seconds
|
||||
|
||||
// Result of a single hop fetch attempt
|
||||
private class HopResult {
|
||||
public bool Success { get; set; }
|
||||
public Uri RedirectUri { get; set; } // Non-null if this was a redirect
|
||||
public bool GotContent { get; set; } // True if we received and saved content
|
||||
public bool ShouldRetry { get; set; } // True if this failure is retryable
|
||||
}
|
||||
|
||||
private readonly HashSet<string> _failedPaths = new();
|
||||
private readonly object _failedPathsLock = new();
|
||||
private Timer _periodicRetryTimer;
|
||||
|
||||
ResourceFetcher(string relativeLocalPath, HttpClient httpClient) {
|
||||
_localPath = Path.Combine(
|
||||
Application.persistentDataPath,
|
||||
@@ -29,7 +54,22 @@ namespace common {
|
||||
"Resources",
|
||||
relativeLocalPath);
|
||||
Directory.CreateDirectory(_localPath);
|
||||
_httpClient = httpClient;
|
||||
|
||||
// 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
|
||||
// (sending it to S3 signed URLs causes HTTP 400)
|
||||
_authHeader = httpClient.DefaultRequestHeaders.Authorization;
|
||||
|
||||
// Start periodic retry timer
|
||||
_periodicRetryTimer = new Timer(
|
||||
PeriodicRetryCallback,
|
||||
null,
|
||||
PeriodicRetryIntervalMs,
|
||||
PeriodicRetryIntervalMs);
|
||||
}
|
||||
|
||||
private readonly string _localPath;
|
||||
@@ -63,13 +103,131 @@ namespace common {
|
||||
}
|
||||
|
||||
private async Task<bool> FetchRemote(string path) {
|
||||
// Send the request to the server to fetch the headshot
|
||||
var response = await _httpClient.SendAsync(new HttpRequestMessage {
|
||||
Method = HttpMethod.Get,
|
||||
RequestUri = new Uri(BaseUri, path)
|
||||
});
|
||||
Uri currentUri = new Uri(BaseUri, path);
|
||||
|
||||
return await ReceiveHeadshot(response.Content, path);
|
||||
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;
|
||||
}
|
||||
|
||||
// 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.
|
||||
private async Task<HopResult> FetchOneHopWithRetry(Uri uri, string path, int hopIndex) {
|
||||
for (int attempt = 0; attempt < MaxRetryAttempts; attempt++) {
|
||||
try {
|
||||
var request =
|
||||
new HttpRequestMessage { Method = HttpMethod.Get, RequestUri = uri };
|
||||
// Only send auth header to eagle0.net, not to S3 signed URLs
|
||||
if (_authHeader != null && uri.Host.EndsWith("eagle0.net")) {
|
||||
request.Headers.Authorization = _authHeader;
|
||||
}
|
||||
var response = await _noRedirectClient.SendAsync(request);
|
||||
|
||||
var statusCode = (int)response.StatusCode;
|
||||
|
||||
// Success - got content
|
||||
if (response.IsSuccessStatusCode) {
|
||||
await ReceiveHeadshot(response.Content, path);
|
||||
return new HopResult { Success = true, GotContent = true };
|
||||
}
|
||||
|
||||
// Redirect - return the location to follow
|
||||
if (statusCode >= 300 && statusCode < 400) {
|
||||
var location = response.Headers.Location;
|
||||
if (location != null) {
|
||||
var redirectUri =
|
||||
location.IsAbsoluteUri ? location : new Uri(uri, location);
|
||||
return new HopResult { Success = true, RedirectUri = redirectUri };
|
||||
}
|
||||
Debug.Log(
|
||||
$"ResourceFetcher: hop {hopIndex} got redirect but no Location header for {path}");
|
||||
}
|
||||
|
||||
Debug.Log(
|
||||
$"ResourceFetcher: hop {hopIndex} HTTP {statusCode} for {path} (attempt {attempt + 1}/{MaxRetryAttempts})");
|
||||
|
||||
// Don't retry on 4xx client errors (except 408, 429)
|
||||
if (statusCode >= 400 && statusCode < 500 && statusCode != 408 &&
|
||||
statusCode != 429) {
|
||||
return new HopResult { Success = false, ShouldRetry = false };
|
||||
}
|
||||
|
||||
if (attempt < MaxRetryAttempts - 1) {
|
||||
await Task.Delay(RetryDelaysMs[attempt]);
|
||||
}
|
||||
} catch (HttpRequestException e) {
|
||||
Debug.Log(
|
||||
$"ResourceFetcher: hop {hopIndex} network error for {path} (attempt {attempt + 1}/{MaxRetryAttempts}): {e.Message}");
|
||||
if (attempt < MaxRetryAttempts - 1) {
|
||||
await Task.Delay(RetryDelaysMs[attempt]);
|
||||
}
|
||||
} catch (TaskCanceledException e) {
|
||||
Debug.Log(
|
||||
$"ResourceFetcher: hop {hopIndex} timeout for {path} (attempt {attempt + 1}/{MaxRetryAttempts}): {e.Message}");
|
||||
if (attempt < MaxRetryAttempts - 1) {
|
||||
await Task.Delay(RetryDelaysMs[attempt]);
|
||||
}
|
||||
} catch (WebException e) {
|
||||
Debug.Log(
|
||||
$"ResourceFetcher: hop {hopIndex} connection error for {path} (attempt {attempt + 1}/{MaxRetryAttempts}): {e.Message}");
|
||||
if (attempt < MaxRetryAttempts - 1) {
|
||||
await Task.Delay(RetryDelaysMs[attempt]);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Debug.Log(
|
||||
$"ResourceFetcher: hop {hopIndex} IO error for {path} (attempt {attempt + 1}/{MaxRetryAttempts}): {e.Message}");
|
||||
if (attempt < MaxRetryAttempts - 1) {
|
||||
await Task.Delay(RetryDelaysMs[attempt]);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Debug.LogError(
|
||||
$"ResourceFetcher: hop {hopIndex} unexpected error for {path}: {e}");
|
||||
return new HopResult { Success = false, ShouldRetry = false };
|
||||
}
|
||||
}
|
||||
|
||||
// All retries exhausted for this hop
|
||||
return new HopResult { Success = false, ShouldRetry = false };
|
||||
}
|
||||
|
||||
private void MarkFailed(string path) {
|
||||
lock (_failedPathsLock) { _failedPaths.Add(path); }
|
||||
}
|
||||
|
||||
private void ClearFailed(string path) {
|
||||
lock (_failedPathsLock) { _failedPaths.Remove(path); }
|
||||
}
|
||||
|
||||
private void PeriodicRetryCallback(object state) {
|
||||
List<string> pathsToRetry;
|
||||
lock (_failedPathsLock) {
|
||||
if (_failedPaths.Count == 0) return;
|
||||
pathsToRetry = _failedPaths.ToList();
|
||||
_failedPaths.Clear();
|
||||
}
|
||||
|
||||
Debug.Log($"ResourceFetcher: Retrying {pathsToRetry.Count} failed headshot(s)");
|
||||
foreach (var path in pathsToRetry) { FetchRemote(path); }
|
||||
}
|
||||
|
||||
private class ImageLoadQueueItem {
|
||||
@@ -131,15 +289,15 @@ namespace common {
|
||||
}
|
||||
|
||||
public void Prefetch(IEnumerable<string> paths) {
|
||||
var toFetch = paths.Where(path => !File.Exists(FullLocalPath(path))).ToList();
|
||||
|
||||
foreach (var path in toFetch) {
|
||||
if (string.IsNullOrEmpty(path)) {
|
||||
Debug.LogWarning("ResourceFetcher: Attempted to prefetch an empty path.");
|
||||
}
|
||||
|
||||
FetchRemote(path);
|
||||
List<string> toFetch;
|
||||
lock (_failedPathsLock) {
|
||||
toFetch = paths.Where(path => !string.IsNullOrEmpty(path) &&
|
||||
!File.Exists(FullLocalPath(path)) &&
|
||||
!_failedPaths.Contains(path))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
foreach (var path in toFetch) { FetchRemote(path); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,17 +4,18 @@
|
||||
"com.unity.2d.sprite": "1.0.0",
|
||||
"com.unity.2d.tilemap": "1.0.0",
|
||||
"com.unity.ai.navigation": "2.0.9",
|
||||
"com.unity.analytics": "3.8.1",
|
||||
"com.unity.collab-proxy": "2.9.3",
|
||||
"com.unity.analytics": "3.8.2",
|
||||
"com.unity.collab-proxy": "2.10.2",
|
||||
"com.unity.ext.nunit": "2.0.5",
|
||||
"com.unity.ide.rider": "3.0.38",
|
||||
"com.unity.ide.visualstudio": "2.0.23",
|
||||
"com.unity.multiplayer.center": "1.0.0",
|
||||
"com.unity.nuget.newtonsoft-json": "3.2.1",
|
||||
"com.unity.ide.visualstudio": "2.0.25",
|
||||
"com.unity.multiplayer.center": "1.0.1",
|
||||
"com.unity.nuget.newtonsoft-json": "3.2.2",
|
||||
"com.unity.test-framework": "1.6.0",
|
||||
"com.unity.timeline": "1.8.9",
|
||||
"com.unity.ugui": "2.0.0",
|
||||
"com.unity.modules.accessibility": "1.0.0",
|
||||
"com.unity.modules.adaptiveperformance": "1.0.0",
|
||||
"com.unity.modules.ai": "1.0.0",
|
||||
"com.unity.modules.androidjni": "1.0.0",
|
||||
"com.unity.modules.animation": "1.0.0",
|
||||
@@ -41,6 +42,7 @@
|
||||
"com.unity.modules.unitywebrequestaudio": "1.0.0",
|
||||
"com.unity.modules.unitywebrequesttexture": "1.0.0",
|
||||
"com.unity.modules.unitywebrequestwww": "1.0.0",
|
||||
"com.unity.modules.vectorgraphics": "1.0.0",
|
||||
"com.unity.modules.vehicles": "1.0.0",
|
||||
"com.unity.modules.video": "1.0.0",
|
||||
"com.unity.modules.vr": "1.0.0",
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"url": "https://packages.unity.com"
|
||||
},
|
||||
"com.unity.analytics": {
|
||||
"version": "3.8.1",
|
||||
"version": "3.8.2",
|
||||
"depth": 0,
|
||||
"source": "registry",
|
||||
"dependencies": {
|
||||
@@ -42,7 +42,7 @@
|
||||
"url": "https://packages.unity.com"
|
||||
},
|
||||
"com.unity.collab-proxy": {
|
||||
"version": "2.9.3",
|
||||
"version": "2.10.2",
|
||||
"depth": 0,
|
||||
"source": "registry",
|
||||
"dependencies": {},
|
||||
@@ -64,16 +64,16 @@
|
||||
"url": "https://packages.unity.com"
|
||||
},
|
||||
"com.unity.ide.visualstudio": {
|
||||
"version": "2.0.23",
|
||||
"version": "2.0.25",
|
||||
"depth": 0,
|
||||
"source": "registry",
|
||||
"dependencies": {
|
||||
"com.unity.test-framework": "1.1.9"
|
||||
"com.unity.test-framework": "1.1.31"
|
||||
},
|
||||
"url": "https://packages.unity.com"
|
||||
},
|
||||
"com.unity.multiplayer.center": {
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"depth": 0,
|
||||
"source": "builtin",
|
||||
"dependencies": {
|
||||
@@ -81,14 +81,14 @@
|
||||
}
|
||||
},
|
||||
"com.unity.nuget.newtonsoft-json": {
|
||||
"version": "3.2.1",
|
||||
"version": "3.2.2",
|
||||
"depth": 0,
|
||||
"source": "registry",
|
||||
"dependencies": {},
|
||||
"url": "https://packages.unity.com"
|
||||
},
|
||||
"com.unity.services.analytics": {
|
||||
"version": "6.1.0",
|
||||
"version": "6.1.1",
|
||||
"depth": 1,
|
||||
"source": "registry",
|
||||
"dependencies": {
|
||||
@@ -99,7 +99,7 @@
|
||||
"url": "https://packages.unity.com"
|
||||
},
|
||||
"com.unity.services.core": {
|
||||
"version": "1.14.0",
|
||||
"version": "1.16.0",
|
||||
"depth": 2,
|
||||
"source": "registry",
|
||||
"dependencies": {
|
||||
@@ -146,6 +146,14 @@
|
||||
"source": "builtin",
|
||||
"dependencies": {}
|
||||
},
|
||||
"com.unity.modules.adaptiveperformance": {
|
||||
"version": "1.0.0",
|
||||
"depth": 0,
|
||||
"source": "builtin",
|
||||
"dependencies": {
|
||||
"com.unity.modules.subsystems": "1.0.0"
|
||||
}
|
||||
},
|
||||
"com.unity.modules.ai": {
|
||||
"version": "1.0.0",
|
||||
"depth": 0,
|
||||
@@ -353,6 +361,16 @@
|
||||
"com.unity.modules.imageconversion": "1.0.0"
|
||||
}
|
||||
},
|
||||
"com.unity.modules.vectorgraphics": {
|
||||
"version": "1.0.0",
|
||||
"depth": 0,
|
||||
"source": "builtin",
|
||||
"dependencies": {
|
||||
"com.unity.modules.uielements": "1.0.0",
|
||||
"com.unity.modules.imageconversion": "1.0.0",
|
||||
"com.unity.modules.imgui": "1.0.0"
|
||||
}
|
||||
},
|
||||
"com.unity.modules.vehicles": {
|
||||
"version": "1.0.0",
|
||||
"depth": 0,
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
m_EditorVersion: 6000.2.7f2
|
||||
m_EditorVersionWithRevision: 6000.2.7f2 (2b518236b676)
|
||||
m_EditorVersion: 6000.3.0f1
|
||||
m_EditorVersionWithRevision: 6000.3.0f1 (d1870ce95baf)
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
|
||||
|
||||
go_library(
|
||||
name = "admin_server_lib",
|
||||
srcs = ["admin_server.go"],
|
||||
importpath = "github.com/nolen777/eagle0/src/main/go/net/eagle0/admin_server",
|
||||
visibility = ["//visibility:private"],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:api_go_proto", # keep
|
||||
"@org_golang_google_grpc//:grpc",
|
||||
"@org_golang_google_grpc//credentials/insecure",
|
||||
],
|
||||
)
|
||||
|
||||
go_binary(
|
||||
name = "admin_server",
|
||||
embed = [":admin_server_lib"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
@@ -0,0 +1,179 @@
|
||||
// Package main provides an HTTP admin server for Eagle game management.
|
||||
// It connects to the Eagle gRPC service and exposes REST endpoints.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
eagle "github.com/nolen777/eagle0/src/main/protobuf/net/eagle0/eagle/api/eagle"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
var (
|
||||
eagleAddr = flag.String("eagle-addr", "localhost:40032", "Eagle gRPC server address")
|
||||
httpPort = flag.Int("http-port", 8080, "HTTP server port")
|
||||
grpcClient eagle.EagleClient
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
// Connect to Eagle gRPC server
|
||||
conn, err := grpc.NewClient(*eagleAddr,
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to connect to Eagle server: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
grpcClient = eagle.NewEagleClient(conn)
|
||||
|
||||
// Set up HTTP routes
|
||||
http.HandleFunc("/games", handleGames)
|
||||
http.HandleFunc("/games/", handleGameHistory)
|
||||
http.HandleFunc("/health", handleHealth)
|
||||
|
||||
addr := fmt.Sprintf(":%d", *httpPort)
|
||||
log.Printf("Admin server starting on %s, connecting to Eagle at %s", addr, *eagleAddr)
|
||||
if err := http.ListenAndServe(addr, nil); err != nil {
|
||||
log.Fatalf("HTTP server failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
// PlayerInfo represents a player in a running game
|
||||
type PlayerInfo struct {
|
||||
FactionID int32 `json:"faction_id"`
|
||||
FactionName string `json:"faction_name"`
|
||||
LeaderName string `json:"leader_name"`
|
||||
IsHuman bool `json:"is_human"`
|
||||
UserName string `json:"user_name,omitempty"`
|
||||
}
|
||||
|
||||
// GameInfo represents a running game with hex-formatted ID
|
||||
type GameInfo struct {
|
||||
GameID string `json:"game_id"`
|
||||
CurrentRound int32 `json:"current_round"`
|
||||
ActionCount int32 `json:"action_count"`
|
||||
Players []PlayerInfo `json:"players"`
|
||||
RunStatus string `json:"run_status"`
|
||||
}
|
||||
|
||||
// GamesResponse is the response for /games endpoint
|
||||
type GamesResponse struct {
|
||||
Games []GameInfo `json:"games"`
|
||||
}
|
||||
|
||||
// formatGameID formats a game ID as unsigned hex
|
||||
func formatGameID(id int64) string {
|
||||
return fmt.Sprintf("%x", uint64(id))
|
||||
}
|
||||
|
||||
func handleGames(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
resp, err := grpcClient.GetRunningGames(ctx, &eagle.GetRunningGamesRequest{})
|
||||
if err != nil {
|
||||
log.Printf("Failed to get running games: %v", err)
|
||||
http.Error(w, fmt.Sprintf("Failed to get running games: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Convert to JSON response with hex game IDs
|
||||
games := make([]GameInfo, len(resp.Games))
|
||||
for i, game := range resp.Games {
|
||||
players := make([]PlayerInfo, len(game.Players))
|
||||
for j, p := range game.Players {
|
||||
players[j] = PlayerInfo{
|
||||
FactionID: p.FactionId,
|
||||
FactionName: p.FactionName,
|
||||
LeaderName: p.LeaderName,
|
||||
IsHuman: p.IsHuman,
|
||||
UserName: p.UserName,
|
||||
}
|
||||
}
|
||||
games[i] = GameInfo{
|
||||
GameID: formatGameID(game.GameId),
|
||||
CurrentRound: game.CurrentRound,
|
||||
ActionCount: game.ActionCount,
|
||||
Players: players,
|
||||
RunStatus: game.RunStatus,
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(GamesResponse{Games: games})
|
||||
}
|
||||
|
||||
func handleGameHistory(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse game ID from URL: /games/{id}/history
|
||||
path := strings.TrimPrefix(r.URL.Path, "/games/")
|
||||
parts := strings.Split(path, "/")
|
||||
if len(parts) < 2 || parts[1] != "history" {
|
||||
http.Error(w, "Invalid path. Use /games/{id}/history", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
gameIDUint, err := strconv.ParseUint(parts[0], 16, 64)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid game ID (expected hex format)", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
gameID := int64(gameIDUint)
|
||||
|
||||
// Parse query parameters
|
||||
startIndex := 0
|
||||
limit := 100
|
||||
if s := r.URL.Query().Get("start"); s != "" {
|
||||
if v, err := strconv.Atoi(s); err == nil {
|
||||
startIndex = v
|
||||
}
|
||||
}
|
||||
if l := r.URL.Query().Get("limit"); l != "" {
|
||||
if v, err := strconv.Atoi(l); err == nil {
|
||||
limit = v
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
resp, err := grpcClient.GetGameHistory(ctx, &eagle.GetGameHistoryRequest{
|
||||
GameId: gameID,
|
||||
StartIndex: int32(startIndex),
|
||||
Limit: int32(limit),
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("Failed to get game history for game %d: %v", gameID, err)
|
||||
http.Error(w, fmt.Sprintf("Failed to get game history: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
@@ -212,9 +212,11 @@ proto_library(
|
||||
visibility = ["//src/main/protobuf/net/eagle0:__subpackages__"],
|
||||
)
|
||||
|
||||
# keep: gazelle wants go_grpc_v2 and shardok_internal_interface_proto, but that breaks Go compilation
|
||||
# (go_grpc_v2 only generates _grpc.pb.go, not message types; shardok_internal_interface needs separate target)
|
||||
go_proto_library(
|
||||
name = "common_go_proto",
|
||||
compilers = ["@io_bazel_rules_go//proto:go_grpc_v2"],
|
||||
compilers = ["@io_bazel_rules_go//proto:go_proto"], # keep
|
||||
importpath = "github.com/nolen777/eagle0/src/main/protobuf/net/eagle0/common",
|
||||
protos = [
|
||||
":common_unit_proto",
|
||||
@@ -222,11 +224,26 @@ go_proto_library(
|
||||
":hostility_proto",
|
||||
":player_info_proto",
|
||||
":random_units_proto",
|
||||
":shardok_internal_interface_proto",
|
||||
":victory_condition_proto",
|
||||
# keep: shardok_internal_interface_proto must NOT be here - it needs go_grpc compiler in separate target
|
||||
],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/shardok/common:common_go_proto",
|
||||
# keep: other deps not needed for non-gRPC protos
|
||||
],
|
||||
)
|
||||
|
||||
# Separate target for shardok_internal_interface since it contains a gRPC service
|
||||
# Uses go_grpc (v1) which generates both messages and gRPC code
|
||||
go_proto_library(
|
||||
name = "shardok_internal_interface_go_grpc",
|
||||
compilers = ["@io_bazel_rules_go//proto:go_grpc"],
|
||||
importpath = "github.com/nolen777/eagle0/src/main/protobuf/net/eagle0/common/shardok_internal_interface",
|
||||
protos = [":shardok_internal_interface_proto"],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
":common_go_proto",
|
||||
"//src/main/protobuf/net/eagle0/shardok/api:api_go_proto",
|
||||
"//src/main/protobuf/net/eagle0/shardok/common:common_go_proto",
|
||||
"//src/main/protobuf/net/eagle0/shardok/storage:storage_go_proto",
|
||||
|
||||
@@ -212,10 +212,35 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
# Non-gRPC message protos
|
||||
go_proto_library(
|
||||
name = "api_messages_go_proto",
|
||||
compilers = ["@io_bazel_rules_go//proto:go_proto"],
|
||||
importpath = "github.com/nolen777/eagle0/src/main/protobuf/net/eagle0/eagle/api",
|
||||
protos = [
|
||||
":available_command_proto",
|
||||
":command_proto",
|
||||
":pregenerated_text_proto",
|
||||
":selected_command_proto",
|
||||
":streaming_text_response_proto",
|
||||
],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/common:common_go_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api/command/util:util_go_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:common_go_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/views:views_go_proto",
|
||||
"//src/main/protobuf/net/eagle0/shardok/api:api_go_proto",
|
||||
"//src/main/protobuf/net/eagle0/shardok/common:common_go_proto",
|
||||
],
|
||||
)
|
||||
|
||||
# Eagle gRPC service - uses go_grpc (v1) which generates both messages and gRPC code
|
||||
# (go_grpc_v2 only generates gRPC code, not the message types like UpdateStreamRequest)
|
||||
go_proto_library(
|
||||
name = "api_go_proto",
|
||||
compilers = ["@io_bazel_rules_go//proto:go_grpc_v2"],
|
||||
importpath = "github.com/nolen777/eagle0/src/main/protobuf/net/eagle0/eagle/api",
|
||||
compilers = ["@io_bazel_rules_go//proto:go_grpc"], # keep
|
||||
importpath = "github.com/nolen777/eagle0/src/main/protobuf/net/eagle0/eagle/api/eagle", # keep
|
||||
protos = [
|
||||
":available_command_proto",
|
||||
":command_proto",
|
||||
|
||||
@@ -28,6 +28,10 @@ service Eagle {
|
||||
rpc StreamUpdates(stream UpdateStreamRequest) returns (stream UpdateStreamResponse) {}
|
||||
|
||||
rpc AddSettings (AddSettingsRequest) returns (AddSettingsResponse) {}
|
||||
|
||||
// Admin endpoints
|
||||
rpc GetRunningGames(GetRunningGamesRequest) returns (GetRunningGamesResponse) {}
|
||||
rpc GetGameHistory(GetGameHistoryRequest) returns (GetGameHistoryResponse) {}
|
||||
}
|
||||
|
||||
message PostCommandRequest {
|
||||
@@ -80,6 +84,8 @@ message UpdateStreamRequest {
|
||||
message ShardokViewStatus {
|
||||
string shardok_game_id = 1;
|
||||
int32 filtered_result_count = 2;
|
||||
// Request full state resync instead of delta update (used after connection drops)
|
||||
bool request_full_resync = 3;
|
||||
}
|
||||
repeated ShardokViewStatus shardok_view_statuses = 3;
|
||||
|
||||
@@ -114,9 +120,18 @@ message UpdateStreamResponse {
|
||||
CustomBattleResponse custom_battle_response = 7;
|
||||
HexMapResponse hex_map_response = 8;
|
||||
PostCommandResponse post_command_response = 9;
|
||||
SubscriptionAck subscription_ack = 10;
|
||||
}
|
||||
}
|
||||
|
||||
// Acknowledgment sent by server after processing a StreamGameRequest
|
||||
message SubscriptionAck {
|
||||
int64 game_id = 1;
|
||||
bool success = 2;
|
||||
string error_message = 3; // Only set if success is false
|
||||
int32 confirmed_result_count = 4; // Server confirms the starting point for updates
|
||||
}
|
||||
|
||||
message HexMapsRequest {
|
||||
message OneMapRequestInfo {
|
||||
string map_name = 1;
|
||||
@@ -156,10 +171,43 @@ message GameUpdate {
|
||||
|
||||
message HeartbeatRequest {
|
||||
int64 client_timestamp = 1;
|
||||
// Sync verification: client reports its known action counts per game
|
||||
repeated GameSyncStatus game_sync_statuses = 2;
|
||||
}
|
||||
|
||||
// Client's known sync state for a single game
|
||||
message GameSyncStatus {
|
||||
int64 game_id = 1;
|
||||
// Eagle action count (matches ActionResultResponse.unfiltered_result_count_after)
|
||||
int32 unfiltered_result_count = 2;
|
||||
// Shardok action counts per tactical battle
|
||||
repeated ShardokSyncStatus shardok_sync_statuses = 3;
|
||||
}
|
||||
|
||||
message ShardokSyncStatus {
|
||||
string shardok_game_id = 1;
|
||||
// Matches ShardokActionResultResponse.filtered_result_count_after
|
||||
int32 filtered_result_count = 2;
|
||||
}
|
||||
|
||||
message HeartbeatResponse {
|
||||
int64 server_timestamp = 1;
|
||||
// Sync verification results - only included if there are mismatches
|
||||
repeated GameSyncResult game_sync_results = 2;
|
||||
}
|
||||
|
||||
// Server's sync verification result for a single game
|
||||
message GameSyncResult {
|
||||
int64 game_id = 1;
|
||||
bool eagle_in_sync = 2;
|
||||
int32 server_unfiltered_result_count = 3; // Server's count for comparison
|
||||
repeated ShardokSyncResult shardok_sync_results = 4;
|
||||
}
|
||||
|
||||
message ShardokSyncResult {
|
||||
string shardok_game_id = 1;
|
||||
bool in_sync = 2;
|
||||
int32 server_filtered_result_count = 3; // Server's count for comparison
|
||||
}
|
||||
|
||||
message NewGameOptions {
|
||||
@@ -241,6 +289,25 @@ message ActionResultResponse {
|
||||
int32 unfiltered_result_count_after = 1;
|
||||
repeated .net.eagle0.eagle.views.ActionResultView action_result_views = 2;
|
||||
AvailableCommands available_commands = 3;
|
||||
// Server-reported game status for connection status UI
|
||||
ServerGameStatus server_game_status = 4;
|
||||
}
|
||||
|
||||
// Server-reported game status for the connection status indicator.
|
||||
// Tells the client what the server is doing/waiting for.
|
||||
message ServerGameStatus {
|
||||
enum Status {
|
||||
UNKNOWN = 0;
|
||||
YOUR_TURN = 1; // Player has commands available
|
||||
WAITING_FOR_PLAYERS = 2; // Waiting for other player(s) to act
|
||||
GENERATING_TEXT = 3; // LLM text generation in progress
|
||||
PROCESSING_ACTION = 4; // Server is processing an action
|
||||
}
|
||||
Status status = 1;
|
||||
// For WAITING_FOR_PLAYERS: which faction(s) we're waiting for
|
||||
repeated int32 waiting_for_faction_ids = 2;
|
||||
// For GENERATING_TEXT: which LLM stream is being generated
|
||||
string generating_llm_id = 3;
|
||||
}
|
||||
|
||||
message ShardokActionResultResponse {
|
||||
@@ -302,3 +369,47 @@ message AddSettingsKeyValue {
|
||||
string key = 1;
|
||||
string value = 2;
|
||||
}
|
||||
|
||||
// Admin messages
|
||||
message GetRunningGamesRequest {}
|
||||
|
||||
message GetRunningGamesResponse {
|
||||
repeated RunningGameInfo games = 1;
|
||||
}
|
||||
|
||||
message RunningGameInfo {
|
||||
int64 game_id = 1;
|
||||
int32 current_round = 2;
|
||||
int32 action_count = 3;
|
||||
repeated RunningGamePlayerInfo players = 4;
|
||||
string run_status = 5;
|
||||
}
|
||||
|
||||
message RunningGamePlayerInfo {
|
||||
int32 faction_id = 1;
|
||||
string faction_name = 2;
|
||||
string leader_name = 3;
|
||||
bool is_human = 4;
|
||||
string user_name = 5;
|
||||
}
|
||||
|
||||
message GetGameHistoryRequest {
|
||||
int64 game_id = 1;
|
||||
int32 start_index = 2; // optional: start from this action index
|
||||
int32 limit = 3; // optional: max number of actions to return (0 = all)
|
||||
}
|
||||
|
||||
message GetGameHistoryResponse {
|
||||
int64 game_id = 1;
|
||||
int32 total_action_count = 2;
|
||||
repeated GameHistoryEntry entries = 3;
|
||||
}
|
||||
|
||||
message GameHistoryEntry {
|
||||
int32 index = 1;
|
||||
string action_type = 2;
|
||||
int32 round_id = 3;
|
||||
.google.protobuf.Int32Value acting_faction_id = 4;
|
||||
.google.protobuf.Int32Value province_id = 5;
|
||||
string summary = 6; // human-readable summary of the action
|
||||
}
|
||||
|
||||
@@ -195,6 +195,28 @@ message PrisonerExecutedDetails {
|
||||
int32 executing_faction_id = 4;
|
||||
}
|
||||
|
||||
message PrisonerReleasedDetails {
|
||||
.google.protobuf.Int32Value last_faction_id = 1;
|
||||
int32 province_id = 2;
|
||||
int32 released_hero_id = 3;
|
||||
int32 releasing_faction_id = 4;
|
||||
}
|
||||
|
||||
message PrisonerExiledDetails {
|
||||
.google.protobuf.Int32Value last_faction_id = 1;
|
||||
int32 province_id = 2;
|
||||
int32 exiled_hero_id = 3;
|
||||
int32 exiling_faction_id = 4;
|
||||
}
|
||||
|
||||
message PrisonerReturnedDetails {
|
||||
.google.protobuf.Int32Value last_faction_id = 1;
|
||||
int32 province_id = 2;
|
||||
int32 returned_hero_id = 3;
|
||||
int32 returning_faction_id = 4;
|
||||
int32 to_faction_id = 5;
|
||||
}
|
||||
|
||||
message ProvinceConqueredDetails {
|
||||
int32 province_id = 1;
|
||||
int32 conquering_faction_id = 2;
|
||||
@@ -282,6 +304,9 @@ message NotificationDetails {
|
||||
OutlawSpottedDetails outlaw_spotted_details = 10;
|
||||
PrisonerExchangeDetails prisoner_exchange_details = 17;
|
||||
PrisonerExecutedDetails prisoner_executed_details = 19;
|
||||
PrisonerReleasedDetails prisoner_released_details = 36;
|
||||
PrisonerExiledDetails prisoner_exiled_details = 37;
|
||||
PrisonerReturnedDetails prisoner_returned_details = 38;
|
||||
ProvinceConqueredDetails province_conquered_details = 2;
|
||||
ProvinceExpansionDetails province_expansion_details = 1;
|
||||
ProvinceHeldDetails province_held_details = 15;
|
||||
|
||||
@@ -17,7 +17,8 @@ message BeastInfo {
|
||||
string plural_name = 3;
|
||||
double likelihood = 4;
|
||||
double max_count_multiplier = 5;
|
||||
double relative_power = 6;
|
||||
double min_relative_power = 6;
|
||||
double max_relative_power = 12;
|
||||
double economy_devastation = 7;
|
||||
double agriculture_devastation = 8;
|
||||
double infrastructure_devastation = 9;
|
||||
|
||||
@@ -805,7 +805,6 @@ go_proto_library(
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/common:common_go_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:api_go_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:common_go_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/views:views_go_proto",
|
||||
"//src/main/protobuf/net/eagle0/shardok/api:api_go_proto",
|
||||
|
||||
@@ -68,6 +68,10 @@ message GeneratedTextRequestDetails {
|
||||
TruceOfferMessage truce_offer_message = 2;
|
||||
TruceResolutionMessage truce_resolution_message = 8;
|
||||
ProfessionGainedMessage profession_gained_message = 30;
|
||||
PrisonerExecutedMessage prisoner_executed_message = 31;
|
||||
PrisonerReleasedMessage prisoner_released_message = 32;
|
||||
PrisonerExiledMessage prisoner_exiled_message = 33;
|
||||
PrisonerReturnedMessage prisoner_returned_message = 34;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,3 +305,32 @@ message ProfessionGainedMessage {
|
||||
int32 faction_id = 2;
|
||||
.net.eagle0.eagle.common.Profession new_profession = 3;
|
||||
}
|
||||
|
||||
message PrisonerExecutedMessage {
|
||||
int32 executed_hero_id = 1;
|
||||
int32 last_faction_id = 2;
|
||||
int32 executing_faction_id = 3;
|
||||
int32 province_id = 4;
|
||||
}
|
||||
|
||||
message PrisonerReleasedMessage {
|
||||
int32 released_hero_id = 1;
|
||||
int32 last_faction_id = 2;
|
||||
int32 releasing_faction_id = 3;
|
||||
int32 province_id = 4;
|
||||
}
|
||||
|
||||
message PrisonerExiledMessage {
|
||||
int32 exiled_hero_id = 1;
|
||||
int32 last_faction_id = 2;
|
||||
int32 exiling_faction_id = 3;
|
||||
int32 province_id = 4;
|
||||
}
|
||||
|
||||
message PrisonerReturnedMessage {
|
||||
int32 returned_hero_id = 1;
|
||||
int32 last_faction_id = 2;
|
||||
int32 returning_faction_id = 3;
|
||||
int32 to_faction_id = 4;
|
||||
int32 province_id = 5;
|
||||
}
|
||||
|
||||
@@ -1,63 +1,63 @@
|
||||
singularName pluralName likelihood maxCountMultiplier relativePower economyDevastation agricultureDevastation infrastructureDevastation averageGoldPer averageFoodPer
|
||||
agitator agitators 0.1 1 1 5 0 0 1 0
|
||||
asshole assholes 0.05 5 1 2 2 2 0.5 0
|
||||
bandit bandits 0.2 1 1 5 0 0 1 0
|
||||
bear bears 0.04 0.1 10 0 5 5 1 3
|
||||
boar boars 0.04 0.2 5 0 5 5 0 10
|
||||
brigand brigands 0.2 1 1 5 0 0 1 0
|
||||
buccaneer buccaneers 0.05 1 2 5 0 0 2 0
|
||||
cannibal cannibals 0.04 1 3 5 0 0 1 0
|
||||
chimpanzee chimpanzees 0.04 3 1 0 0 5 0 0
|
||||
clown clowns 0.04 1 5 5 0 0 1 0
|
||||
cossack cossacks 0.04 1 5 5 0 0 1 0
|
||||
crocodile crocodiles 0.04 0.04 10 0 5 0 0 1
|
||||
crook crooks 0.1 1 1 5 0 0 1 0
|
||||
cultist cultists 0.05 1 3 5 0 0 1 0
|
||||
dacoit dacoits 0.05 1 1 5 0 0 1 0
|
||||
demon demons 0.01 0.5 50 5 5 0 50 0
|
||||
desperado desperadoes 0.1 1 1 5 0 0 1 0
|
||||
dragon dragons 0.04 0.001 1000 10 10 10 5000 200
|
||||
elephant elephants 0.04 0.1 15 0 5 5 0 8
|
||||
freebooter freebooters 0.05 1 1 5 0 0 1 0
|
||||
giant giants 0.01 0.01 100 10 10 10 50 3
|
||||
heretic heretics 0.05 1 3 5 0 0 1 0
|
||||
highwayman highwaymen 0.1 1 1.5 5 0 0 1 0
|
||||
hippogryph hippogryphs 0.04 0.05 50 5 5 5 1000 100
|
||||
hippopotamus hippopotamuses 0.04 0.1 10 0 0 5 0 5
|
||||
honey badger honey badgers 0.3 3 0.3 0 5 0 0 0.5
|
||||
hooligan hooligans 0.1 1 1 5 0 0 1 0
|
||||
instigator instigators 0.1 1 1 5 0 0 1 0
|
||||
lion lions 0.04 0.2 5 0 5 0 2 1.5
|
||||
mammoth mammoths 0.1 1 1.5 5 0 0 1 0
|
||||
marauder marauders 0.1 1 1.5 5 0 0 1 0
|
||||
miscreant miscreants 0.01 1 1 5 0 0 1 0
|
||||
mobster mobsters 0.05 1 2 5 0 0 2 0
|
||||
ne'er-do-well ne'er-do-wells 0.1 1 0.8 5 0 0 1 0
|
||||
nihilist nihilists 0.1 1 1 5 0 0 1 0
|
||||
ogre ogres 0.02 0.05 20 10 5 5 20 3
|
||||
orc orcs 0.1 1 1.5 5 0 0 1 0
|
||||
particularist particularists 0.1 1 1 5 0 0 1 0
|
||||
pirate pirates 0.05 1 2 5 0 0 2 0
|
||||
proud boy proud boys 0.1 1 1 5 0 0 1 0
|
||||
rabble-rouser rabble-rousers 0.1 1 1 5 0 0 1 0
|
||||
raccoon raccoons 0.2 20 0.2 0 5 0 0 0.1
|
||||
rebel rebels 0.04 2 1 5 0 0 1 0
|
||||
robber robbers 0.1 1 1 5 0 0 1 0
|
||||
ruffian ruffians 0.1 1 1 5 0 0 1 0
|
||||
scalawag scalawags 0.1 1 0.7 5 0 0 0.5 0
|
||||
separatist separatists 0.1 1 1 5 0 0 1 0
|
||||
skeleton skeletons 0.01 1 1 5 0 0 1 0
|
||||
snake snakes 0.2 20 0.2 0 5 0 0 0.1
|
||||
spider spiders 0.1 500 0.01 0 5 0 0 0.001
|
||||
street tough street toughs 0.1 1 1 5 0 0 1 0
|
||||
terrorist terrorists 0.04 1 5 5 0 0 1 0
|
||||
thief thieves 0.1 1 1 5 0 0 1 0
|
||||
tiger tigers 0.1 0.2 3 0 5 0 1 1
|
||||
traitor traitors 0.1 1 2 5 0 0 1 0
|
||||
troll trolls 0.02 0.1 10 5 10 5 50 3
|
||||
unknown unknown 0 0 0 5 0 0 0 0
|
||||
vampire vampires 0.01 0.5 50 5 5 0 50 0
|
||||
velociraptor velociraptors 0.02 0.15 20 10 5 5 20 3
|
||||
wolf wolves 0.3 3 0.3 0 5 0 0 0.5
|
||||
wolverine wolverines 0.4 3 0.3 0 5 0 0 0.5
|
||||
zombie zombies 0.01 1 1 5 0 0 1 0
|
||||
singularName pluralName likelihood maxCountMultiplier minRelativePower maxRelativePower economyDevastation agricultureDevastation infrastructureDevastation averageGoldPer averageFoodPer
|
||||
agitator agitators 0.1 1 1 1 5 0 0 1 0
|
||||
asshole assholes 0.05 5 1 1 2 2 2 0.5 0
|
||||
bandit bandits 0.2 1 1 1 5 0 0 1 0
|
||||
bear bears 0.04 0.1 10 10 0 5 5 1 3
|
||||
boar boars 0.04 0.2 5 5 0 5 5 0 10
|
||||
brigand brigands 0.2 1 1 1 5 0 0 1 0
|
||||
buccaneer buccaneers 0.05 1 2 2 5 0 0 2 0
|
||||
cannibal cannibals 0.04 1 3 3 5 0 0 1 0
|
||||
chimpanzee chimpanzees 0.04 3 1 1 0 0 5 0 0
|
||||
clown clowns 0.04 1 5 50 5 0 0 1 0
|
||||
cossack cossacks 0.04 1 5 5 5 0 0 1 0
|
||||
crocodile crocodiles 0.04 0.04 10 10 0 5 0 0 1
|
||||
crook crooks 0.1 1 1 1 5 0 0 1 0
|
||||
cultist cultists 0.05 1 3 3 5 0 0 1 0
|
||||
dacoit dacoits 0.05 1 1 1 5 0 0 1 0
|
||||
demon demons 0.01 0.5 50 50 5 5 0 50 0
|
||||
desperado desperadoes 0.1 1 1 1 5 0 0 1 0
|
||||
dragon dragons 0.04 0.001 1000 1000 10 10 10 5000 200
|
||||
elephant elephants 0.04 0.1 15 15 0 5 5 0 8
|
||||
freebooter freebooters 0.05 1 1 1 5 0 0 1 0
|
||||
giant giants 0.01 0.01 100 100 10 10 10 50 3
|
||||
heretic heretics 0.05 1 3 3 5 0 0 1 0
|
||||
highwayman highwaymen 0.1 1 1.5 1.5 5 0 0 1 0
|
||||
hippogryph hippogryphs 0.04 0.05 50 50 5 5 5 1000 100
|
||||
hippopotamus hippopotamuses 0.04 0.1 10 10 0 0 5 0 5
|
||||
honey badger honey badgers 0.3 3 0.3 0.3 0 5 0 0 0.5
|
||||
hooligan hooligans 0.1 1 1 1 5 0 0 1 0
|
||||
instigator instigators 0.1 1 1 1 5 0 0 1 0
|
||||
lion lions 0.04 0.2 5 5 0 5 0 2 1.5
|
||||
mammoth mammoths 0.1 1 1.5 1.5 5 0 0 1 0
|
||||
marauder marauders 0.1 1 1.5 1.5 5 0 0 1 0
|
||||
miscreant miscreants 0.01 1 1 1 5 0 0 1 0
|
||||
mobster mobsters 0.05 1 2 2 5 0 0 2 0
|
||||
ne'er-do-well ne'er-do-wells 0.1 1 0.8 0.8 5 0 0 1 0
|
||||
nihilist nihilists 0.1 1 1 1 5 0 0 1 0
|
||||
ogre ogres 0.02 0.05 20 20 10 5 5 20 3
|
||||
orc orcs 0.1 1 1.5 1.5 5 0 0 1 0
|
||||
particularist particularists 0.1 1 1 1 5 0 0 1 0
|
||||
pirate pirates 0.05 1 2 2 5 0 0 2 0
|
||||
proud boy proud boys 0.1 1 1 1 5 0 0 1 0
|
||||
rabble-rouser rabble-rousers 0.1 1 1 1 5 0 0 1 0
|
||||
raccoon raccoons 0.2 20 0.2 0.2 0 5 0 0 0.1
|
||||
rebel rebels 0.04 2 1 1 5 0 0 1 0
|
||||
robber robbers 0.1 1 1 1 5 0 0 1 0
|
||||
ruffian ruffians 0.1 1 1 1 5 0 0 1 0
|
||||
scalawag scalawags 0.1 1 0.7 0.7 5 0 0 0.5 0
|
||||
separatist separatists 0.1 1 1 1 5 0 0 1 0
|
||||
skeleton skeletons 0.01 1 1 1 5 0 0 1 0
|
||||
snake snakes 0.2 20 0.2 0.2 0 5 0 0 0.1
|
||||
spider spiders 0.1 500 0.01 0.01 0 5 0 0 0.001
|
||||
street tough street toughs 0.1 1 1 1 5 0 0 1 0
|
||||
terrorist terrorists 0.04 1 5 5 5 0 0 1 0
|
||||
thief thieves 0.1 1 1 1 5 0 0 1 0
|
||||
tiger tigers 0.1 0.2 3 3 0 5 0 1 1
|
||||
traitor traitors 0.1 1 2 2 5 0 0 1 0
|
||||
troll trolls 0.02 0.1 10 10 5 10 5 50 3
|
||||
unknown unknown 0 0 0 0 5 0 0 0 0
|
||||
vampire vampires 0.01 0.5 50 50 5 5 0 50 0
|
||||
velociraptor velociraptors 0.02 0.15 20 20 10 5 5 20 3
|
||||
wolf wolves 0.3 3 0.3 0.3 0 5 0 0 0.5
|
||||
wolverine wolverines 0.4 3 0.3 0.3 0 5 0 0 0.5
|
||||
zombie zombies 0.01 1 1 1 5 0 0 1 0
|
||||
|
@@ -7,7 +7,7 @@ import net.eagle0.eagle.{FactionId, GameId, ProvinceId}
|
||||
import net.eagle0.eagle.api.available_command.AvailableCommand
|
||||
import net.eagle0.eagle.api.command.OneProvinceAvailableCommands
|
||||
import net.eagle0.eagle.api.selected_command.SelectedCommand
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.internal.game_state.GameState as GameStateProto
|
||||
import net.eagle0.eagle.library.util.*
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.{
|
||||
AlmsCommandSelector,
|
||||
@@ -16,6 +16,7 @@ import net.eagle0.eagle.library.util.command_choice_helpers.{
|
||||
}
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.CommandChooserImplicits.*
|
||||
import net.eagle0.eagle.library.Engine
|
||||
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
|
||||
|
||||
case class AIClientWithSelectedCommand(
|
||||
client: AIClient,
|
||||
@@ -54,13 +55,13 @@ case class AIClient(
|
||||
else
|
||||
chooseFrom(
|
||||
maybeCommandsMap.get.commandsByProvince,
|
||||
engine.currentState,
|
||||
GameStateConverter.toProto(engine.currentState),
|
||||
functionalRandom
|
||||
)
|
||||
}
|
||||
|
||||
private def chooseMidGameCommandFrom(
|
||||
gameState: GameState,
|
||||
gameState: GameStateProto,
|
||||
oneProvinceAvailableCommands: OneProvinceAvailableCommands,
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[AIClientWithSelectedCommand] =
|
||||
@@ -115,7 +116,7 @@ case class AIClient(
|
||||
|
||||
private def chooseFrom(
|
||||
acs: Map[ProvinceId, OneProvinceAvailableCommands],
|
||||
gs: GameState,
|
||||
gs: GameStateProto,
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[AIClientWithSelectedCommand] = {
|
||||
val opac = acs.head._2
|
||||
@@ -186,7 +187,7 @@ case class AIClient(
|
||||
),
|
||||
(
|
||||
fid: FactionId,
|
||||
gs: GameState,
|
||||
gs: GameStateProto,
|
||||
acs: Vector[AvailableCommand],
|
||||
functionalRandom
|
||||
) =>
|
||||
|
||||
@@ -25,6 +25,7 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:alms_command_selector",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:attack_decision_command_chooser",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:command_chooser",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/shardok_interface:battle_resolution",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -11,6 +11,7 @@ scala_library(
|
||||
"//src/test/scala/net/eagle0/eagle/service/controller:__pkg__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/shardok_interface:battle_update",
|
||||
],
|
||||
deps = [
|
||||
@@ -20,6 +21,7 @@ scala_library(
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/views:action_result_view_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/shardok_interface:battle_resolution",
|
||||
"//src/main/scala/net/eagle0/eagle/shardok_interface:battle_update",
|
||||
],
|
||||
@@ -55,7 +57,6 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/command:command_factory",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:deterministic_single_result_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_state_proto_sequencer",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:eagle_require",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/hero_generator",
|
||||
@@ -69,11 +70,8 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types/base:action_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:battalion_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/date",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/faction",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:battalion_type_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/hero",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/date",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/faction",
|
||||
@@ -147,14 +145,17 @@ scala_library(
|
||||
"//src/main/protobuf/net/eagle0/shardok/api:command_descriptor_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/shardok/storage:action_result_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/date",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/shardok/api:action_result_view_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/shardok/api:command_descriptor_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/shardok/storage:action_result_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/date",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -203,15 +204,15 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:deterministic_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:deterministic_single_result_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:vigor_xp_applier",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:eagle_require",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/hero_generator",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/validations:scala_runtime_validator",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_faction_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_hero_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:notification_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province/concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types/base:action_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:action_result_proto_converter",
|
||||
@@ -219,6 +220,7 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:battalion_type_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/date",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/faction",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/hero",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type",
|
||||
|
||||
@@ -4,7 +4,7 @@ import net.eagle0.eagle.{FactionId, GameId, ProvinceId}
|
||||
import net.eagle0.eagle.api.command.AvailableCommands
|
||||
import net.eagle0.eagle.api.selected_command.SelectedCommand
|
||||
import net.eagle0.eagle.internal.action_result.ActionResult
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.shardok_interface.{BattleResolution, BattleUpdate}
|
||||
import net.eagle0.eagle.views.action_result_view.ActionResultView
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import net.eagle0.eagle.{FactionId, GameId, ProvinceId}
|
||||
import net.eagle0.eagle.api.command.AvailableCommands
|
||||
import net.eagle0.eagle.api.selected_command.SelectedCommand
|
||||
import net.eagle0.eagle.internal.action_result.ActionResult
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.library.actions.applier.{ActionResultProtoApplier, ActionResultProtoApplierImpl}
|
||||
import net.eagle0.eagle.library.actions.availability.AvailableCommandsFactory
|
||||
import net.eagle0.eagle.library.actions.impl.action.{
|
||||
@@ -22,12 +21,9 @@ import net.eagle0.eagle.library.util.validations.RuntimeValidator
|
||||
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
|
||||
import net.eagle0.eagle.library.EngineImpl.{appliedResults, withUpdateChecks}
|
||||
import net.eagle0.eagle.model.action_result.ActionResultT
|
||||
import net.eagle0.eagle.model.proto_converters.date.DateConverter
|
||||
import net.eagle0.eagle.model.proto_converters.faction.FactionConverter
|
||||
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
|
||||
import net.eagle0.eagle.model.proto_converters.hero.HeroConverter
|
||||
import net.eagle0.eagle.model.proto_converters.province.ProvinceConverter
|
||||
import net.eagle0.eagle.model.proto_converters.BattalionConverter
|
||||
import net.eagle0.eagle.model.proto_converters.BattalionTypeConverter
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.shardok_interface.{BattleResolution, BattleUpdate}
|
||||
import net.eagle0.eagle.views.action_result_view.ActionResultView
|
||||
|
||||
@@ -39,7 +35,7 @@ object EngineImpl {
|
||||
): EngineImpl =
|
||||
EngineImpl(
|
||||
gameId = gameId,
|
||||
currentState = history.last.gameState.withGameId(gameId),
|
||||
currentState = GameStateConverter.fromProto(history.last.gameState).copy(gameId = gameId),
|
||||
heroGenerator = heroGenerator,
|
||||
history = history
|
||||
)
|
||||
@@ -49,7 +45,7 @@ object EngineImpl {
|
||||
): EngineAndResultsImpl =
|
||||
engineAndResultsImpl.recursiveTransform(eng =>
|
||||
RoundPhaseAdvancer.checkForPhaseAdvancement(
|
||||
currentState = eng.currentState,
|
||||
currentState = GameStateConverter.toProto(eng.currentState),
|
||||
actionResultProtoApplier = eng.actionResultProtoApplier,
|
||||
history = eng.history,
|
||||
availableCommandsFactory = eng.availableCommandsFactory,
|
||||
@@ -64,19 +60,14 @@ object EngineImpl {
|
||||
engineAndResultsImpl.recursiveTransformT(eng =>
|
||||
CheckForFulfilledQuestsAction(
|
||||
gameId = eng.gameId,
|
||||
currentDate = DateConverter.fromProto(eng.currentState.currentDate),
|
||||
currentDate = eng.currentState.currentDate.get,
|
||||
currentRoundId = eng.currentState.currentRoundId,
|
||||
provinces = eng.currentState.provinces.values
|
||||
.map(ProvinceConverter.fromProto)
|
||||
.toVector,
|
||||
factions = eng.currentState.factions.values
|
||||
.map(FactionConverter.fromProto)
|
||||
.toVector,
|
||||
battalions = eng.currentState.battalions.values.toVector
|
||||
.map(BattalionConverter.fromProto),
|
||||
getHero = hid => eng.currentState.heroes.get(hid).map(HeroConverter.fromProto),
|
||||
battalionTypes = eng.currentState.battalionTypes.toVector,
|
||||
heroBackstoryTextIdLookup = hid => eng.currentState.heroes(hid).backstoryVersions.last.textId
|
||||
provinces = eng.currentState.provinces.values.toVector,
|
||||
factions = eng.currentState.factions.values.toVector,
|
||||
battalions = eng.currentState.battalions.values.toVector,
|
||||
getHero = hid => eng.currentState.heroes.get(hid),
|
||||
battalionTypes = eng.currentState.battalionTypes.map(BattalionTypeConverter.toProto),
|
||||
heroBackstoryTextIdLookup = hid => eng.currentState.heroes(hid).backstoryTextId
|
||||
).results
|
||||
)
|
||||
|
||||
@@ -98,7 +89,9 @@ object EngineImpl {
|
||||
): EngineAndResults = withUpdateChecks(
|
||||
EngineAndResultsImpl(
|
||||
engine = engine.copy(
|
||||
currentState = results.lastOption.map(_.gameState).getOrElse(engine.currentState),
|
||||
currentState = results.lastOption
|
||||
.map(awrs => GameStateConverter.fromProto(awrs.gameState))
|
||||
.getOrElse(engine.currentState),
|
||||
history = engine.history.withNewResults(results)
|
||||
),
|
||||
results = results.map(_.actionResult)
|
||||
@@ -136,7 +129,7 @@ final case class EngineAndResultsImpl(
|
||||
): EngineAndResultsImpl = recursiveTransform { eng =>
|
||||
val results = f(eng)
|
||||
RandomStateProtoSequencer(
|
||||
initialStateProto = eng.currentState,
|
||||
initialState = eng.currentState,
|
||||
actionResultProtoApplier = eng.actionResultProtoApplier,
|
||||
functionalRandom = SeededRandom(eng.currentState.randomSeed)
|
||||
).withActionResultTs(_ => results).results.newValue
|
||||
@@ -183,7 +176,7 @@ case class EngineImpl(
|
||||
.filterForPlayer(
|
||||
results = history.since(knownUnfilteredCount),
|
||||
factionId = factionId,
|
||||
startingState = history.stateAfter(knownUnfilteredCount)
|
||||
startingState = GameStateConverter.toProto(history.stateAfter(knownUnfilteredCount))
|
||||
),
|
||||
unfilteredCountAfter = history.count
|
||||
)
|
||||
@@ -195,15 +188,16 @@ case class EngineImpl(
|
||||
def getAvailablePlayerCommands(
|
||||
fid: FactionId
|
||||
): Option[AvailableCommands] = {
|
||||
val currentStateProto = GameStateConverter.toProto(currentState)
|
||||
val commandsByProvince =
|
||||
availableCommandsFactory.availablePlayerCommands(currentState, fid)
|
||||
availableCommandsFactory.availablePlayerCommands(currentStateProto, fid)
|
||||
|
||||
Option.when(commandsByProvince.nonEmpty) {
|
||||
AvailableCommands(
|
||||
token = tokenForFaction(fid),
|
||||
commandsByProvince = commandsByProvince,
|
||||
suggestedProvinceId = AvailableCommandsFactory
|
||||
.suggestedProvinceId(fid, commandsByProvince, currentState)
|
||||
.suggestedProvinceId(fid, commandsByProvince, currentStateProto)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -211,17 +205,19 @@ case class EngineImpl(
|
||||
def tokenForFaction(fid: FactionId): Long =
|
||||
currentState.factionCommandCounts.getOrElse(fid, 0).toLong
|
||||
|
||||
def resolveBattle(battleResolution: BattleResolution): EngineAndResults =
|
||||
def resolveBattle(battleResolution: BattleResolution): EngineAndResults = {
|
||||
val currentStateProto = GameStateConverter.toProto(currentState)
|
||||
appliedResults(
|
||||
engine = this,
|
||||
results = actionResultProtoApplier.applyActionResults(
|
||||
startingState = currentState,
|
||||
startingState = currentStateProto,
|
||||
results = ResolveBattleAction(
|
||||
startingGameState = currentState,
|
||||
startingGameState = currentStateProto,
|
||||
resolvedBattles = Vector(battleResolution)
|
||||
).execute(actionResultProtoApplier).map(_.actionResult)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
def receiveBattleUpdate(battleUpdate: BattleUpdate): EngineAndResults = {
|
||||
if battleUpdate.newUnfilteredCount < battleUpdate.results.size + this.history
|
||||
@@ -287,7 +283,7 @@ case class EngineImpl(
|
||||
val availableCommand = availableCommandOpt.get
|
||||
|
||||
val sequencer = RandomStateProtoSequencer(
|
||||
initialStateProto = this.currentState,
|
||||
initialState = this.currentState,
|
||||
actionResultProtoApplier = actionResultProtoApplier,
|
||||
functionalRandom = SeededRandom(this.currentState.randomSeed)
|
||||
).withActionResults { gs =>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package net.eagle0.eagle.library
|
||||
|
||||
import net.eagle0.eagle.{FactionId, RoundId, ShardokGameId}
|
||||
import net.eagle0.eagle.common.date.Date
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.library.actions.impl.common.ActionWithResultingState
|
||||
import net.eagle0.eagle.model.state.date.Date
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.shardok.api.action_result_view.ActionResultView as ShardokActionResultView
|
||||
import net.eagle0.shardok.api.command_descriptor.AvailableCommands as ShardokAvailableCommands
|
||||
import net.eagle0.shardok.storage.action_result.ActionResult as ShardokActionResult
|
||||
|
||||
@@ -12,9 +12,11 @@ import net.eagle0.eagle.library.actions.impl.action.*
|
||||
import net.eagle0.eagle.library.actions.impl.command.CommandFactory
|
||||
import net.eagle0.eagle.library.actions.impl.common.ActionWithResultingState
|
||||
import net.eagle0.eagle.library.util.hero_generator.HeroGenerator
|
||||
import net.eagle0.eagle.library.util.validations.ScalaRuntimeValidator
|
||||
import net.eagle0.eagle.library.util.EagleRequire.internalValidated
|
||||
import net.eagle0.eagle.model.proto_converters.date.DateConverter
|
||||
import net.eagle0.eagle.model.proto_converters.faction.FactionConverter
|
||||
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
|
||||
import net.eagle0.eagle.model.proto_converters.hero.HeroConverter
|
||||
import net.eagle0.eagle.model.proto_converters.province.ProvinceConverter
|
||||
import net.eagle0.eagle.model.proto_converters.ActionResultProtoConverter
|
||||
@@ -67,15 +69,15 @@ object RoundPhaseAdvancer {
|
||||
)
|
||||
|
||||
case NEW_ROUND =>
|
||||
NewRoundAction(currentState, history).execute(actionResultProtoApplier)
|
||||
NewRoundAction(GameStateConverter.fromProto(currentState), history).execute(actionResultProtoApplier)
|
||||
|
||||
case PRISONER_EXCHANGE =>
|
||||
PrisonerExchangeAction(currentState).execute(actionResultProtoApplier)
|
||||
|
||||
case PROVINCE_EVENTS =>
|
||||
PerformProvinceEventsAction(
|
||||
currentState
|
||||
).execute(actionResultProtoApplier)
|
||||
GameStateConverter.fromProto(currentState)
|
||||
).execute(currentState, actionResultProtoApplier)
|
||||
|
||||
case FORCED_TURN_BACK =>
|
||||
PerformForcedTurnBackAction(currentState).execute(
|
||||
@@ -84,7 +86,7 @@ object RoundPhaseAdvancer {
|
||||
|
||||
case PROVINCE_MOVE_RESOLUTION =>
|
||||
PerformProvinceMoveResolutionAction(
|
||||
currentState
|
||||
GameStateConverter.fromProto(currentState)
|
||||
).execute(actionResultProtoApplier)
|
||||
|
||||
case HANDLE_RIOT =>
|
||||
@@ -93,15 +95,16 @@ object RoundPhaseAdvancer {
|
||||
then Vector.empty
|
||||
else
|
||||
EndHandleRiotsPhaseAction(
|
||||
gameState = currentState,
|
||||
gameState = GameStateConverter.fromProto(currentState),
|
||||
commandsForProvince = pid =>
|
||||
availableCommandsFactory
|
||||
.handleRiotPhaseCommandsForOneProvince(
|
||||
currentState,
|
||||
currentState.provinces(pid)
|
||||
),
|
||||
commandFactory = commandFactory
|
||||
).execute(actionResultProtoApplier)
|
||||
commandFactory = commandFactory,
|
||||
applier = ActionResultTApplierImpl(ScalaRuntimeValidator)
|
||||
).execute(currentState, actionResultProtoApplier)
|
||||
|
||||
case HERO_DEPARTURES =>
|
||||
PerformHeroDeparturesAction(
|
||||
@@ -111,7 +114,7 @@ object RoundPhaseAdvancer {
|
||||
|
||||
case UNAFFILIATED_HERO_ACTIONS =>
|
||||
PerformUnaffiliatedHeroesAction(
|
||||
gameState = currentState,
|
||||
gameState = GameStateConverter.fromProto(currentState),
|
||||
heroGenerator = heroGenerator
|
||||
).execute(actionResultProtoApplier)
|
||||
|
||||
@@ -120,21 +123,27 @@ object RoundPhaseAdvancer {
|
||||
.hasAvailablePleaseRecruitMePhaseCommands(currentState)
|
||||
then Vector.empty
|
||||
else
|
||||
EndPleaseRecruitMePhaseAction(currentState).execute(
|
||||
actionResultProtoApplier
|
||||
Vector(
|
||||
actionResultProtoApplier.applyActionResult(
|
||||
currentState,
|
||||
ActionResultProtoConverter.toProto(
|
||||
EndPleaseRecruitMePhaseAction(GameStateConverter.fromProto(currentState)).immediateExecute
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
case VASSAL_COMMANDS =>
|
||||
val vassalCommandResults = PerformVassalCommandsPhaseAction(
|
||||
gameState = currentState,
|
||||
gameState = GameStateConverter.fromProto(currentState),
|
||||
commandsForProvince = availableCommandsFactory
|
||||
.commandPhaseCommandsForProvince(currentState, _),
|
||||
commandFactory = commandFactory
|
||||
).execute(actionResultProtoApplier)
|
||||
commandFactory = commandFactory,
|
||||
applier = ActionResultTApplierImpl(ScalaRuntimeValidator)
|
||||
).execute(currentState, actionResultProtoApplier)
|
||||
|
||||
if vassalCommandResults.nonEmpty then vassalCommandResults
|
||||
else
|
||||
EndVassalCommandsPhaseAction(currentState).execute(
|
||||
EndVassalCommandsPhaseAction(GameStateConverter.fromProto(currentState)).execute(
|
||||
actionResultProtoApplier
|
||||
)
|
||||
|
||||
@@ -149,7 +158,7 @@ object RoundPhaseAdvancer {
|
||||
currentState,
|
||||
EndPlayerCommandsPhaseAction(
|
||||
currentState,
|
||||
ActionResultTApplierImpl(actionResultProtoApplier)
|
||||
ActionResultTApplierImpl(ScalaRuntimeValidator)
|
||||
).results(functionalRandom = SeededRandom(currentState.randomSeed))
|
||||
.map(
|
||||
ActionResultProtoConverter.toProto(_)
|
||||
@@ -157,8 +166,15 @@ object RoundPhaseAdvancer {
|
||||
)
|
||||
|
||||
case HOSTILE_ARMY_SETUP =>
|
||||
PerformHostileArmySetupAction(currentState).execute(
|
||||
actionResultProtoApplier
|
||||
Vector(
|
||||
actionResultProtoApplier.applyActionResult(
|
||||
currentState,
|
||||
ActionResultProtoConverter.toProto(
|
||||
PerformHostileArmySetupAction(
|
||||
GameStateConverter.fromProto(currentState)
|
||||
).immediateExecute
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
case FREE_FOR_ALL_DECISION =>
|
||||
@@ -171,7 +187,7 @@ object RoundPhaseAdvancer {
|
||||
// There may eventually be VassalAttackDecisions, but for now we're leaving that on the player
|
||||
actionResultProtoApplier.applyActionResults(
|
||||
currentState,
|
||||
EndFreeForAllDecisionPhaseAction(currentState).results.map(
|
||||
EndFreeForAllDecisionPhaseAction(GameStateConverter.fromProto(currentState)).results.map(
|
||||
ActionResultProtoConverter.toProto(_)
|
||||
)
|
||||
)
|
||||
@@ -180,14 +196,19 @@ object RoundPhaseAdvancer {
|
||||
val requestResults = RequestFreeForAllBattlesAction(
|
||||
currentState
|
||||
).execute(actionResultProtoApplier)
|
||||
requestResults ++ EndFreeForAllBattleRequestPhaseAction(
|
||||
requestResults.lastOption.map(_.gameState).getOrElse(currentState)
|
||||
).execute(actionResultProtoApplier)
|
||||
val latestState = requestResults.lastOption.map(_.gameState).getOrElse(currentState)
|
||||
requestResults :+ actionResultProtoApplier.applyActionResult(
|
||||
latestState,
|
||||
ActionResultProtoConverter.toProto(EndFreeForAllBattleRequestPhaseAction.immediateExecute)
|
||||
)
|
||||
|
||||
case FREE_FOR_ALL_BATTLE_RESOLUTION =>
|
||||
if currentState.outstandingBattles.isEmpty then
|
||||
EndFreeForAllBattleResolutionPhaseAction(currentState).execute(
|
||||
actionResultProtoApplier
|
||||
Vector(
|
||||
actionResultProtoApplier.applyActionResult(
|
||||
currentState,
|
||||
ActionResultProtoConverter.toProto(EndFreeForAllBattleResolutionPhaseAction.immediateExecute)
|
||||
)
|
||||
)
|
||||
else Vector.empty // wait for battles to resolve
|
||||
|
||||
@@ -245,21 +266,27 @@ object RoundPhaseAdvancer {
|
||||
then Vector.empty
|
||||
else {
|
||||
val vassalCommandResults = PerformVassalDefenseDecisionsAction(
|
||||
gameState = currentState,
|
||||
gameState = GameStateConverter.fromProto(currentState),
|
||||
commandsForProvince = availableCommandsFactory
|
||||
.defensePhaseCommandsForProvince(currentState, _),
|
||||
commandFactory = commandFactory
|
||||
).execute(actionResultProtoApplier)
|
||||
commandFactory = commandFactory,
|
||||
applier = ActionResultTApplierImpl(ScalaRuntimeValidator)
|
||||
).execute(currentState, actionResultProtoApplier)
|
||||
|
||||
if vassalCommandResults.nonEmpty then vassalCommandResults
|
||||
else
|
||||
EndDefenseDecisionPhaseAction(currentState).execute(
|
||||
actionResultProtoApplier
|
||||
Vector(
|
||||
actionResultProtoApplier.applyActionResult(
|
||||
currentState,
|
||||
ActionResultProtoConverter.toProto(
|
||||
EndDefenseDecisionPhaseAction(GameStateConverter.fromProto(currentState)).immediateExecute
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
case TRUCE_TURN_BACK =>
|
||||
TruceTurnBackPhaseAction(currentState).execute(actionResultProtoApplier)
|
||||
TruceTurnBackPhaseAction(GameStateConverter.fromProto(currentState)).execute(actionResultProtoApplier)
|
||||
|
||||
case BATTLE_REQUEST =>
|
||||
val requestBattlesAction = RequestBattlesAction(
|
||||
@@ -288,19 +315,31 @@ object RoundPhaseAdvancer {
|
||||
ActionResultProtoConverter.toProto(_)
|
||||
)
|
||||
)
|
||||
requestResults ++ EndBattleRequestPhaseAction(
|
||||
requestResults.lastOption.map(_.gameState).getOrElse(currentState)
|
||||
).execute(actionResultProtoApplier)
|
||||
val latestState = requestResults.lastOption.map(_.gameState).getOrElse(currentState)
|
||||
requestResults :+ actionResultProtoApplier.applyActionResult(
|
||||
latestState,
|
||||
ActionResultProtoConverter.toProto(
|
||||
EndBattleRequestPhaseAction(GameStateConverter.fromProto(latestState)).immediateExecute
|
||||
)
|
||||
)
|
||||
|
||||
case FOOD_CONSUMPTION =>
|
||||
PerformFoodConsumptionPhaseAction(currentState).execute(
|
||||
actionResultProtoApplier
|
||||
Vector(
|
||||
actionResultProtoApplier.applyActionResult(
|
||||
currentState,
|
||||
ActionResultProtoConverter.toProto(
|
||||
PerformFoodConsumptionPhaseAction(GameStateConverter.fromProto(currentState)).immediateExecute
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
case BATTLE_RESOLUTION =>
|
||||
if currentState.outstandingBattles.isEmpty then
|
||||
EndBattleResolutionPhaseAction(currentState).execute(
|
||||
actionResultProtoApplier
|
||||
Vector(
|
||||
actionResultProtoApplier.applyActionResult(
|
||||
currentState,
|
||||
ActionResultProtoConverter.toProto(EndBattleResolutionPhaseAction.immediateExecute)
|
||||
)
|
||||
)
|
||||
else Vector.empty // wait for battles to resolve
|
||||
|
||||
@@ -312,8 +351,8 @@ object RoundPhaseAdvancer {
|
||||
actionResultProtoApplier.applyActionResults(
|
||||
currentState,
|
||||
EndBattleAftermathPhaseAction(
|
||||
currentState,
|
||||
ActionResultTApplierImpl(actionResultProtoApplier)
|
||||
GameStateConverter.fromProto(currentState),
|
||||
ActionResultTApplierImpl(ScalaRuntimeValidator)
|
||||
)
|
||||
.randomResults(
|
||||
SeededRandom(currentState.randomSeed)
|
||||
@@ -333,14 +372,14 @@ object RoundPhaseAdvancer {
|
||||
currentState,
|
||||
EndDiplomacyResolutionPhaseAction(
|
||||
currentState,
|
||||
actionResultTApplier = ActionResultTApplierImpl(actionResultProtoApplier)
|
||||
actionResultTApplier = ActionResultTApplierImpl(ScalaRuntimeValidator)
|
||||
).randomResults(SeededRandom(currentState.randomSeed))
|
||||
.newValue
|
||||
.map(ActionResultProtoConverter.toProto)
|
||||
)
|
||||
|
||||
case RECON_RESOLUTION =>
|
||||
PerformReconResolutionAction(currentState).execute(
|
||||
PerformReconResolutionAction(GameStateConverter.fromProto(currentState)).execute(
|
||||
actionResultProtoApplier
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package net.eagle0.eagle.library.actions.applier
|
||||
|
||||
import net.eagle0.eagle.model.action_result.ActionResultT
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
|
||||
case class ActionResultWithResultingState(
|
||||
actionResult: ActionResultT,
|
||||
resultingState: GameState
|
||||
)
|
||||
|
||||
trait ActionResultApplier {
|
||||
def xpForStatBump(stat: Int): Int
|
||||
|
||||
def applyActionResults(
|
||||
startingState: GameState,
|
||||
results: Iterable[ActionResultT]
|
||||
): Vector[ActionResultWithResultingState]
|
||||
|
||||
def applyActionResult(
|
||||
startingState: GameState,
|
||||
result: ActionResultT
|
||||
): ActionResultWithResultingState
|
||||
}
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
package net.eagle0.eagle.library.actions.applier
|
||||
|
||||
import net.eagle0.eagle.library.util.validations.ScalaValidator
|
||||
import net.eagle0.eagle.model.action_result.ActionResultT
|
||||
import net.eagle0.eagle.model.state.battalion.BattalionT
|
||||
import net.eagle0.eagle.model.state.date.Date
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.hero.HeroT
|
||||
import net.eagle0.eagle.model.state.province.ProvinceT
|
||||
import net.eagle0.eagle.model.state.run_status.RunStatus
|
||||
import net.eagle0.eagle.model.state.RoundPhase
|
||||
// Import extension methods
|
||||
import GameStateExtensions.*
|
||||
|
||||
/**
|
||||
* Implementation of ActionResultApplier that applies ActionResultT directly to Scala GameState.
|
||||
*
|
||||
* This uses extension methods on GameState to apply each type of change.
|
||||
*/
|
||||
object ActionResultApplierImpl {
|
||||
def apply(validator: Option[ScalaValidator]): ActionResultApplierImpl = new ActionResultApplierImpl(validator)
|
||||
|
||||
// Type class for single-argument validation
|
||||
trait Validatable[T]:
|
||||
def validate(v: ScalaValidator, value: T): T
|
||||
|
||||
given Validatable[HeroT] with
|
||||
def validate(v: ScalaValidator, value: HeroT): HeroT = v.validate(value)
|
||||
|
||||
given Validatable[GameState] with
|
||||
def validate(v: ScalaValidator, value: GameState): GameState = v.validate(value)
|
||||
|
||||
given Validatable[ActionResultT] with
|
||||
def validate(v: ScalaValidator, value: ActionResultT): ActionResultT = v.validate(value)
|
||||
|
||||
// Type class for validation with GameState context
|
||||
trait ValidatableWithGameState[T]:
|
||||
def validate(v: ScalaValidator, value: T, gs: GameState): T
|
||||
|
||||
given ValidatableWithGameState[BattalionT] with
|
||||
def validate(v: ScalaValidator, value: BattalionT, gs: GameState): BattalionT = v.validate(value, gs)
|
||||
}
|
||||
|
||||
class ActionResultApplierImpl(validator: Option[ScalaValidator]) extends ActionResultApplier {
|
||||
import ActionResultApplierImpl.{Validatable, ValidatableWithGameState, given}
|
||||
|
||||
// Generic single-argument validate
|
||||
def validate[T: Validatable](value: T): T =
|
||||
validator.fold(value)(v => summon[Validatable[T]].validate(v, value))
|
||||
|
||||
// Generic validate with GameState context
|
||||
def validate[T: ValidatableWithGameState](value: T, gs: GameState): T =
|
||||
validator.fold(value)(v => summon[ValidatableWithGameState[T]].validate(v, value, gs))
|
||||
|
||||
// Province validation with RoundPhase (unique signature, no type class needed)
|
||||
def validate(province: ProvinceT, roundPhase: RoundPhase): ProvinceT =
|
||||
validator.fold(province)(_.validate(province, roundPhase))
|
||||
|
||||
override def xpForStatBump(stat: Int): Int = GameStateExtensions.xpForStatBump(stat)
|
||||
|
||||
override def applyActionResults(
|
||||
startingState: GameState,
|
||||
results: Iterable[ActionResultT]
|
||||
): Vector[ActionResultWithResultingState] =
|
||||
results
|
||||
.foldLeft((startingState, Vector.empty[ActionResultWithResultingState])) {
|
||||
case ((gameState, acc), result) =>
|
||||
val awrs = applyActionResult(gameState, result)
|
||||
(awrs.resultingState, acc :+ awrs)
|
||||
}
|
||||
._2
|
||||
|
||||
override def applyActionResult(
|
||||
startingState: GameState,
|
||||
result: ActionResultT
|
||||
): ActionResultWithResultingState = {
|
||||
// Validate the action result
|
||||
validate(result)
|
||||
|
||||
// Get date for applying hero changes - use newDate if present, otherwise current date from state
|
||||
val date: Date = result.newDate
|
||||
.orElse(startingState.currentDate)
|
||||
.getOrElse(
|
||||
throw new IllegalStateException("No date available for applying action result")
|
||||
)
|
||||
|
||||
// Validate new heroes before applying
|
||||
result.newHeroes.foreach(validate)
|
||||
|
||||
// Apply basic state updates
|
||||
val stateAfterBasics = applyBasicStateUpdates(startingState, result)
|
||||
|
||||
// Apply new provinces with validation
|
||||
val stateAfterNewProvinces = result.newProvinces.foldLeft(stateAfterBasics) { (gs, np) =>
|
||||
validate(np, gs.currentPhase)
|
||||
gs.applyNewProvinces(Vector(np))
|
||||
}
|
||||
|
||||
// Apply changed provinces with validation
|
||||
val stateAfterChangedProvinces = result.changedProvinces.foldLeft(stateAfterNewProvinces) { (gs, cp) =>
|
||||
val after = gs.applyChangedProvinces(Vector(cp))
|
||||
validate(after.provinces(cp.provinceId), gs.currentPhase)
|
||||
after
|
||||
}
|
||||
|
||||
// Apply changed battalions with validation
|
||||
val stateAfterChangedBattalions = stateAfterChangedProvinces.applyChangedBattalions(result.changedBattalions)
|
||||
result.changedBattalions.foreach { cb =>
|
||||
stateAfterChangedBattalions.battalions.get(cb.battalionId).foreach { b =>
|
||||
validate(b, stateAfterChangedBattalions)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply all remaining entity changes using extension methods
|
||||
val finalState = stateAfterChangedBattalions
|
||||
.applyProvinceActed(result.provinceIdActed)
|
||||
.applyNewBattalions(result.newBattalions, result.provinceId)
|
||||
.applyDestroyedBattalionIds(result.destroyedBattalionIds)
|
||||
.applyNewHeroes(result.newHeroes)
|
||||
.applyChangedHeroes(result.changedHeroes, date)
|
||||
.applyRemovedHeroes(result.removedHeroIds.toSet)
|
||||
.applyChangedFactions(result.changedFactions)
|
||||
.applyRemovedFactions(result.removedFactionIds.toSet)
|
||||
.applyNewFactions(result.newFactions)
|
||||
.applyNewBattle(result.newBattle)
|
||||
.applyNewNotifications(result.newNotifications)
|
||||
.applyRemovedNotifications(result.removedNotifications)
|
||||
.applyNewSeed(result.newRandomSeed)
|
||||
.applyChronicleEntry(result.newChronicleEntry)
|
||||
.applyCommandCountUpdate(result.actingFactionId)
|
||||
|
||||
// Validate final game state
|
||||
validate(finalState)
|
||||
|
||||
ActionResultWithResultingState(
|
||||
actionResult = result,
|
||||
resultingState = finalState
|
||||
)
|
||||
}
|
||||
|
||||
private def applyBasicStateUpdates(state: GameState, result: ActionResultT): GameState = {
|
||||
val afterRoundId = result.newRoundId
|
||||
.map(rid => state.copy(currentRoundId = rid))
|
||||
.getOrElse(state)
|
||||
|
||||
val afterPhase = result.newRoundPhase
|
||||
.map(phase => afterRoundId.copy(currentPhase = phase))
|
||||
.getOrElse(afterRoundId)
|
||||
|
||||
val afterDate = result.newDate
|
||||
.map(date => afterPhase.copy(currentDate = Some(date)))
|
||||
.getOrElse(afterPhase)
|
||||
|
||||
val afterRunStatus =
|
||||
if result.gameEnded.contains(true) then afterDate.copy(runStatus = RunStatus.Over)
|
||||
else afterDate
|
||||
|
||||
val afterVictor = result.newVictorFactionId
|
||||
.map(factionId => afterRunStatus.copy(victor = Some(factionId)))
|
||||
.getOrElse(afterRunStatus)
|
||||
|
||||
afterVictor.copy(actionResultCount = afterVictor.actionResultCount + 1)
|
||||
}
|
||||
}
|
||||
+16
-12
@@ -1,35 +1,39 @@
|
||||
package net.eagle0.eagle.library.actions.applier
|
||||
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.library.util.validations.Validator
|
||||
import net.eagle0.eagle.library.util.validations.ScalaValidator
|
||||
import net.eagle0.eagle.model.action_result.ActionResultT
|
||||
import net.eagle0.eagle.model.proto_converters.ActionResultProtoConverter
|
||||
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
|
||||
|
||||
object ActionResultTApplierImpl {
|
||||
def apply(validator: Validator): ActionResultTApplierImpl =
|
||||
new ActionResultTApplierImpl(new ActionResultProtoApplierImpl(validator))
|
||||
/** Creates an ActionResultTApplierImpl with no validation (for tests) */
|
||||
def apply(): ActionResultTApplierImpl =
|
||||
new ActionResultTApplierImpl(new ActionResultApplierImpl(None))
|
||||
|
||||
def apply(protoApplier: ActionResultProtoApplier): ActionResultTApplierImpl =
|
||||
new ActionResultTApplierImpl(protoApplier)
|
||||
def apply(validator: ScalaValidator): ActionResultTApplierImpl =
|
||||
new ActionResultTApplierImpl(new ActionResultApplierImpl(Some(validator)))
|
||||
|
||||
def apply(baseApplier: ActionResultApplier): ActionResultTApplierImpl =
|
||||
new ActionResultTApplierImpl(baseApplier)
|
||||
}
|
||||
|
||||
class ActionResultTApplierImpl(protoApplier: ActionResultProtoApplier) extends ActionResultTApplier {
|
||||
override def xpForStatBump(stat: Int): Int = protoApplier.xpForStatBump(stat)
|
||||
class ActionResultTApplierImpl(baseApplier: ActionResultApplier) extends ActionResultTApplier {
|
||||
override def xpForStatBump(stat: Int): Int = baseApplier.xpForStatBump(stat)
|
||||
|
||||
override def applyActionResults(
|
||||
startingState: GameState,
|
||||
results: Iterable[ActionResultT]
|
||||
): Vector[ActionResultTWithResultingState] = protoApplier
|
||||
): Vector[ActionResultTWithResultingState] = baseApplier
|
||||
.applyActionResults(
|
||||
startingState,
|
||||
results.map(ActionResultProtoConverter.toProto)
|
||||
GameStateConverter.fromProto(startingState),
|
||||
results
|
||||
)
|
||||
.zip(results)
|
||||
.map {
|
||||
case (actionWithResultingState, actionResult) =>
|
||||
ActionResultTWithResultingState(
|
||||
actionResult = actionResult,
|
||||
resultingState = actionWithResultingState.gameState
|
||||
resultingState = GameStateConverter.toProto(actionWithResultingState.resultingState)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,208 @@
|
||||
load("@rules_scala//scala:scala.bzl", "scala_library")
|
||||
|
||||
scala_library(
|
||||
name = "action_result_applier",
|
||||
srcs = ["ActionResultApplier.scala"],
|
||||
visibility = [
|
||||
"//src/main/scala/net/eagle0/eagle:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/library:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/actions:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "province_update_helpers",
|
||||
srcs = ["ProvinceUpdateHelpers.scala"],
|
||||
visibility = ["//visibility:private"],
|
||||
deps = [
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:eagle_require",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/hero:hero_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province/concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:army",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province/concrete",
|
||||
],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "province_update_helpers2",
|
||||
srcs = ["ProvinceUpdateHelpers2.scala"],
|
||||
visibility = ["//visibility:private"],
|
||||
deps = [
|
||||
"//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/action_result/changed_province/concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province/concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/quest",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/unaffiliated_hero",
|
||||
],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "game_state_province_extensions",
|
||||
srcs = ["GameStateProvinceExtensions.scala"],
|
||||
visibility = ["//visibility:private"],
|
||||
deps = [
|
||||
":province_update_helpers",
|
||||
":province_update_helpers2",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library:eagle_internal_exception",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:eagle_require",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province/concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province/concrete",
|
||||
],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "game_state_battalion_extensions",
|
||||
srcs = ["GameStateBattalionExtensions.scala"],
|
||||
visibility = ["//visibility:private"],
|
||||
deps = [
|
||||
"//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/action_result:changed_battalion_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_battalion_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:army",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:combat_unit",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/battalion/concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province/concrete",
|
||||
],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "game_state_hero_extensions",
|
||||
srcs = ["GameStateHeroExtensions.scala"],
|
||||
visibility = ["//visibility:private"],
|
||||
deps = [
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library:eagle_internal_exception",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:eagle_require",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_hero_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_hero_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/date",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero/backstory_version",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero/concrete",
|
||||
],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "game_state_faction_extensions",
|
||||
srcs = ["GameStateFactionExtensions.scala"],
|
||||
visibility = ["//visibility:private"],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/views:province_view_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/action_result:changed_faction_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_faction_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/faction",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/faction/concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "game_state_battle_extensions",
|
||||
srcs = ["GameStateBattleExtensions.scala"],
|
||||
visibility = ["//visibility:private"],
|
||||
deps = [
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/shardok_battle",
|
||||
],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "game_state_misc_extensions",
|
||||
srcs = ["GameStateMiscExtensions.scala"],
|
||||
visibility = ["//visibility:private"],
|
||||
deps = [
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:notification_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/chronicle_entry",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "game_state_extensions",
|
||||
srcs = ["GameStateExtensions.scala"],
|
||||
visibility = [
|
||||
"//src/main/scala/net/eagle0/eagle:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/actions:__subpackages__",
|
||||
],
|
||||
exports = [
|
||||
":game_state_battalion_extensions",
|
||||
":game_state_battle_extensions",
|
||||
":game_state_faction_extensions",
|
||||
":game_state_hero_extensions",
|
||||
":game_state_misc_extensions",
|
||||
":game_state_province_extensions",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_faction_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_hero_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:notification_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
deps = [
|
||||
":game_state_battalion_extensions",
|
||||
":game_state_battle_extensions",
|
||||
":game_state_faction_extensions",
|
||||
":game_state_hero_extensions",
|
||||
":game_state_misc_extensions",
|
||||
":game_state_province_extensions",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:extra_xp_for_stat_bump_over100",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:xp_for_stat_bump",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_faction_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_hero_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:notification_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "action_result_applier_impl",
|
||||
srcs = ["ActionResultApplierImpl.scala"],
|
||||
visibility = [
|
||||
"//src/main/scala/net/eagle0/eagle:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/actions:__subpackages__",
|
||||
],
|
||||
exports = [
|
||||
":action_result_applier",
|
||||
":game_state_extensions",
|
||||
],
|
||||
deps = [
|
||||
":action_result_applier",
|
||||
":game_state_extensions",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/validations:scala_validator",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/date",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/run_status",
|
||||
],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "action_result_proto_applier",
|
||||
srcs = ["ActionResultProtoApplier.scala"],
|
||||
@@ -9,11 +212,10 @@ scala_library(
|
||||
"//src/test/scala/net/eagle0/eagle/library/actions:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//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/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/hero:legacy_hero_utils",
|
||||
# "@maven//:com_thesamet_scalapb_lenses_3",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -27,23 +229,22 @@ scala_library(
|
||||
exports = [":action_result_proto_applier"],
|
||||
deps = [
|
||||
":action_result_proto_applier",
|
||||
"//src/main/scala/net/eagle0/common:functional_random",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:profession_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:faction_relationship_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:run_status_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:run_status_scala_proto",
|
||||
"//src/main/scala/net/eagle0/common:functional_random",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:xp_for_stat_bump",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:extra_xp_for_stat_bump_over100",
|
||||
"//src/main/scala/net/eagle0/eagle/library:eagle_internal_exception",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:extra_xp_for_stat_bump_over100",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:xp_for_stat_bump",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:eagle_require",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/hero:legacy_hero_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:province_event_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/hero:legacy_hero_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/validations:runtime_validator",
|
||||
# "@maven//:com_thesamet_scalapb_lenses_3",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -66,7 +267,6 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types/base:action_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero:gender",
|
||||
# "@maven//:com_thesamet_scalapb_lenses_3",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -78,25 +278,28 @@ scala_library(
|
||||
"//src/test/scala/net/eagle0/eagle/library/actions:__subpackages__",
|
||||
],
|
||||
exports = [
|
||||
":action_result_applier",
|
||||
":action_result_trait_applier",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/validations:runtime_validator",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/validations:scala_validator",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
deps = [
|
||||
":action_result_proto_applier_impl",
|
||||
":action_result_applier",
|
||||
":action_result_applier_impl",
|
||||
":action_result_trait_applier",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province/concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types/base:action_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/validations:scala_validator",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_faction_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_hero_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:notification_trait",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/validations:runtime_validator",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:action_result_proto_converter",
|
||||
# "@maven//:com_thesamet_scalapb_lenses_3",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types/base:action_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
package net.eagle0.eagle.library.actions.applier
|
||||
|
||||
import net.eagle0.eagle.*
|
||||
import net.eagle0.eagle.library.EagleInternalException
|
||||
import net.eagle0.eagle.model.action_result.concrete.ChangedBattalionC
|
||||
import net.eagle0.eagle.model.action_result.ChangedBattalionT
|
||||
import net.eagle0.eagle.model.state.{Army, CombatUnit, MovingArmy}
|
||||
import net.eagle0.eagle.model.state.battalion.concrete.BattalionC
|
||||
import net.eagle0.eagle.model.state.battalion.BattalionT
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.province.concrete.ProvinceC
|
||||
|
||||
object GameStateBattalionExtensions {
|
||||
|
||||
extension (gameState: GameState) {
|
||||
|
||||
def applyNewBattalions(
|
||||
newBattalions: Vector[BattalionT],
|
||||
provinceId: Option[ProvinceId]
|
||||
): GameState =
|
||||
if newBattalions.isEmpty then gameState
|
||||
else
|
||||
provinceId.map { pid =>
|
||||
val maxCurrentId =
|
||||
if gameState.battalions.isEmpty then 0
|
||||
else gameState.battalions.keys.max
|
||||
val newIds = (maxCurrentId + 1) to (maxCurrentId + newBattalions.size)
|
||||
val newBattalionsWithIds = newIds
|
||||
.zip(newBattalions)
|
||||
.map {
|
||||
case (id, batt) =>
|
||||
id -> assignBattalionId(batt, id)
|
||||
}
|
||||
.toMap
|
||||
val province = gameState.provinces(pid) match {
|
||||
case p: ProvinceC => p
|
||||
case p => throw new EagleInternalException(s"Unknown ProvinceT type: ${p.getClass}")
|
||||
}
|
||||
gameState.copy(
|
||||
battalions = gameState.battalions ++ newBattalionsWithIds,
|
||||
provinces = gameState.provinces.updated(
|
||||
pid,
|
||||
province.copy(battalionIds = province.battalionIds ++ newIds)
|
||||
)
|
||||
)
|
||||
}
|
||||
.getOrElse(
|
||||
gameState.copy(
|
||||
battalions = gameState.battalions ++ newBattalions.map(b => b.id -> b)
|
||||
)
|
||||
)
|
||||
|
||||
def applyChangedBattalions(changedBattalions: Vector[ChangedBattalionT]): GameState =
|
||||
if changedBattalions.isEmpty then gameState
|
||||
else {
|
||||
// Partition based on whether the target battalion has size 0
|
||||
val (zero, nonzero) = changedBattalions.partition {
|
||||
case cb: ChangedBattalionC => cb.to.size == 0
|
||||
case _ => false
|
||||
}
|
||||
val afterDestroyed = applyDestroyedBattalionIds(zero.map(_.battalionId))
|
||||
|
||||
val updatedBattalions = nonzero.collect {
|
||||
case cb: ChangedBattalionC =>
|
||||
val existingName = afterDestroyed.battalions.get(cb.battalionId).map(_.name)
|
||||
cb.battalionId -> preserveName(cb.to, existingName)
|
||||
}.toMap
|
||||
|
||||
afterDestroyed.copy(
|
||||
battalions = afterDestroyed.battalions ++ updatedBattalions
|
||||
)
|
||||
}
|
||||
|
||||
def applyDestroyedBattalionIds(destroyedBattalionIds: Vector[BattalionId]): GameState =
|
||||
if destroyedBattalionIds.isEmpty then gameState
|
||||
else {
|
||||
val destroyedBattalions = destroyedBattalionIds
|
||||
.filterNot(_ == -1)
|
||||
.flatMap(bid => gameState.battalions.get(bid).map(bid -> _))
|
||||
.toMap
|
||||
|
||||
val updatedProvinces = gameState.provinces.map {
|
||||
case (pid, province) =>
|
||||
province match {
|
||||
case p: ProvinceC =>
|
||||
pid -> p.copy(
|
||||
battalionIds = p.battalionIds.filterNot(destroyedBattalionIds.contains),
|
||||
incomingArmies = p.incomingArmies.map(a => movingArmyWithoutBattalions(a, destroyedBattalionIds))
|
||||
)
|
||||
case p => pid -> p
|
||||
}
|
||||
}
|
||||
|
||||
gameState.copy(
|
||||
battalions = gameState.battalions -- destroyedBattalionIds,
|
||||
destroyedBattalions = gameState.destroyedBattalions ++ destroyedBattalions,
|
||||
provinces = updatedProvinces
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private def assignBattalionId(batt: BattalionT, id: BattalionId): BattalionT = batt match {
|
||||
case b: BattalionC => b.copy(id = id)
|
||||
case b => throw new EagleInternalException(s"Unknown BattalionT type: ${b.getClass}")
|
||||
}
|
||||
|
||||
private def preserveName(batt: BattalionT, existingName: Option[String]): BattalionT =
|
||||
existingName match {
|
||||
case Some(name) =>
|
||||
batt match {
|
||||
case b: BattalionC => b.copy(name = name)
|
||||
case b => b
|
||||
}
|
||||
case None => batt
|
||||
}
|
||||
|
||||
private def movingArmyWithoutBattalions(
|
||||
movingArmy: MovingArmy,
|
||||
removedBattalionIds: Vector[BattalionId]
|
||||
): MovingArmy = {
|
||||
val newUnits = movingArmy.army.units.map(unitWithRemovedBattalions(_, removedBattalionIds))
|
||||
if newUnits.isEmpty then movingArmy.copy(army = movingArmy.army.copy(units = Vector.empty))
|
||||
else movingArmy.copy(army = movingArmy.army.copy(units = newUnits))
|
||||
}
|
||||
|
||||
private def unitWithRemovedBattalions(
|
||||
u: CombatUnit,
|
||||
removedBattalionIds: Vector[BattalionId]
|
||||
): CombatUnit =
|
||||
u.battalionId match {
|
||||
case Some(bid) if removedBattalionIds.contains(bid) => u.copy(battalionId = None)
|
||||
case _ => u
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package net.eagle0.eagle.library.actions.applier
|
||||
|
||||
import net.eagle0.eagle.*
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.shardok_battle.ShardokBattle
|
||||
|
||||
object GameStateBattleExtensions {
|
||||
|
||||
extension (gameState: GameState) {
|
||||
|
||||
def applyNewBattle(battle: Option[ShardokBattle]): GameState =
|
||||
battle.map { b =>
|
||||
gameState.copy(
|
||||
outstandingBattles = gameState.outstandingBattles :+ b,
|
||||
battleCounter = gameState.battleCounter.max(b.battleIndex)
|
||||
)
|
||||
}
|
||||
.getOrElse(gameState)
|
||||
|
||||
def applyResolvedBattle(shardokGameId: Option[ShardokGameId]): GameState =
|
||||
gameState.copy(
|
||||
outstandingBattles = gameState.outstandingBattles.filterNot(batt => shardokGameId.contains(batt.shardokGameId))
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package net.eagle0.eagle.library.actions.applier
|
||||
|
||||
import net.eagle0.eagle.library.settings.{ExtraXpForStatBumpOver100, XpForStatBump}
|
||||
export GameStateBattalionExtensions.*
|
||||
export GameStateBattleExtensions.*
|
||||
export GameStateFactionExtensions.*
|
||||
export GameStateHeroExtensions.*
|
||||
export GameStateMiscExtensions.*
|
||||
// Re-export all extension imports for convenient single import
|
||||
export GameStateProvinceExtensions.*
|
||||
|
||||
object GameStateExtensions {
|
||||
|
||||
def xpForStatBump(stat: Int): Int =
|
||||
if stat <= 99 then XpForStatBump.intValue
|
||||
else XpForStatBump.intValue + ExtraXpForStatBumpOver100.intValue * (stat - 99)
|
||||
}
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
package net.eagle0.eagle.library.actions.applier
|
||||
|
||||
import net.eagle0.eagle.*
|
||||
import net.eagle0.eagle.library.EagleInternalException
|
||||
import net.eagle0.eagle.model.action_result.concrete.ChangedFactionC
|
||||
import net.eagle0.eagle.model.action_result.ChangedFactionT
|
||||
import net.eagle0.eagle.model.state.faction.concrete.FactionC
|
||||
import net.eagle0.eagle.model.state.faction.FactionRelationship
|
||||
import net.eagle0.eagle.model.state.faction.FactionT
|
||||
import net.eagle0.eagle.model.state.faction.FactionT.OutgoingOfferRound
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.views.province_view.ProvinceView
|
||||
|
||||
object GameStateFactionExtensions {
|
||||
|
||||
private val trustMax: Int = 100
|
||||
|
||||
extension (gameState: GameState) {
|
||||
|
||||
def applyChangedFactions(changedFactions: Vector[ChangedFactionT]): GameState =
|
||||
if changedFactions.isEmpty then gameState
|
||||
else
|
||||
changedFactions.foldLeft(gameState) {
|
||||
case (gs, cf) =>
|
||||
gs.applyChangedFaction(cf)
|
||||
}
|
||||
|
||||
def applyChangedFaction(cf: ChangedFactionT): GameState = cf match {
|
||||
case cfc: ChangedFactionC => applyChangedFactionC(cfc)
|
||||
case _ => throw new EagleInternalException(s"Unknown ChangedFactionT type: ${cf.getClass}")
|
||||
}
|
||||
|
||||
private def applyChangedFactionC(cf: ChangedFactionC): GameState =
|
||||
if !gameState.factions.contains(cf.factionId) then gameState
|
||||
else {
|
||||
val existingFaction = gameState.factions(cf.factionId) match {
|
||||
case f: FactionC => f
|
||||
case f => throw new EagleInternalException(s"Unknown FactionT type: ${f.getClass}")
|
||||
}
|
||||
|
||||
val newFocusProvinceId =
|
||||
if cf.clearFocusProvinceId then None
|
||||
else cf.newFocusProvinceId.orElse(existingFaction.focusProvinceId)
|
||||
|
||||
val newLastActedProvinceIdThisRound =
|
||||
if cf.clearLastActedProvinceId then 0
|
||||
else cf.newLastActedProvinceId.getOrElse(existingFaction.lastActedProvinceIdThisRound)
|
||||
|
||||
// Update relationships with trust level changes
|
||||
val relationshipsAfterChanges = cf.changedFactionRelationships
|
||||
.foldLeft(
|
||||
existingFaction.factionRelationships
|
||||
.filterNot(fr =>
|
||||
cf.changedFactionRelationships.map(_.targetFactionId).contains(fr.targetFactionId) ||
|
||||
cf.removedFactionRelationshipFactionIds.contains(fr.targetFactionId)
|
||||
) ++ cf.changedFactionRelationships
|
||||
) { (relationships, _) =>
|
||||
relationships
|
||||
}
|
||||
|
||||
// Apply trust level updates
|
||||
val relationshipsAfterTrust = cf.trustLevelUpdates.foldLeft(relationshipsAfterChanges) {
|
||||
case (relationships, update) =>
|
||||
val existingRelationship = relationships
|
||||
.find(_.targetFactionId == update.targetFactionId)
|
||||
.getOrElse(
|
||||
FactionRelationship(
|
||||
targetFactionId = update.targetFactionId,
|
||||
relationshipLevel = FactionRelationship.RelationshipLevel.Hostile,
|
||||
trustValue = 0
|
||||
)
|
||||
)
|
||||
val newValue = Math.min(trustMax, update.delta + existingRelationship.trustValue)
|
||||
relationships.filterNot(_.targetFactionId == update.targetFactionId) :+
|
||||
existingRelationship.copy(trustValue = newValue)
|
||||
}
|
||||
|
||||
// Update reconned provinces
|
||||
val newReconnedProvinces = existingFaction.reconnedProvinces
|
||||
.filterNot(pv =>
|
||||
cf.updatedReconnedProvinces.map(_.id).contains(pv.id) ||
|
||||
cf.removedReconnedProvinceIds.contains(pv.id)
|
||||
) ++ cf.updatedReconnedProvinces
|
||||
|
||||
// Update outgoing offer rounds
|
||||
val newLastOutgoingTruceOfferRounds = existingFaction.lastOutgoingTruceOfferRounds
|
||||
.filterNot(oor => cf.newOutgoingTruceOfferFactionIds.map(_.fid).contains(oor.toFactionId)) ++
|
||||
cf.newOutgoingTruceOfferFactionIds.map(wrapper =>
|
||||
OutgoingOfferRound(toFactionId = wrapper.fid, roundId = gameState.currentRoundId)
|
||||
)
|
||||
|
||||
val newLastOutgoingAllianceOfferRounds = existingFaction.lastOutgoingAllianceOfferRounds
|
||||
.filterNot(oor => cf.newOutgoingAllianceOfferFactionIds.map(_.fid).contains(oor.toFactionId)) ++
|
||||
cf.newOutgoingAllianceOfferFactionIds.map(wrapper =>
|
||||
OutgoingOfferRound(toFactionId = wrapper.fid, roundId = gameState.currentRoundId)
|
||||
)
|
||||
|
||||
val newLastOutgoingInvitationRounds = existingFaction.lastOutgoingInvitationRounds
|
||||
.filterNot(oor => cf.newOutgoingInvitationFactionIds.map(_.fid).contains(oor.toFactionId)) ++
|
||||
cf.newOutgoingInvitationFactionIds.map(wrapper =>
|
||||
OutgoingOfferRound(toFactionId = wrapper.fid, roundId = gameState.currentRoundId)
|
||||
)
|
||||
|
||||
val newLastOutgoingRansomOfferRounds = existingFaction.lastOutgoingRansomOfferRounds
|
||||
.filterNot(oor => cf.newOutgoingRansomOfferFactionIds.map(_.fid).contains(oor.toFactionId)) ++
|
||||
cf.newOutgoingRansomOfferFactionIds.map(wrapper =>
|
||||
OutgoingOfferRound(toFactionId = wrapper.fid, roundId = gameState.currentRoundId)
|
||||
)
|
||||
|
||||
val updatedFaction = existingFaction.copy(
|
||||
factionHeadId = cf.newFactionHeadHeroId.getOrElse(existingFaction.factionHeadId),
|
||||
leaderIds = existingFaction.leaderIds.diff(cf.removedLeaderHeroIds) ++ cf.newLeaderHeroIds,
|
||||
prestigeModifiers =
|
||||
existingFaction.prestigeModifiers.diff(cf.removedPrestigeModifiers) ++ cf.newPrestigeModifiers,
|
||||
factionRelationships = relationshipsAfterTrust,
|
||||
incomingDiplomacyOffers = existingFaction.incomingDiplomacyOffers
|
||||
.filterNot(offer => cf.removedIncomingDiplomacyOfferFactionIds.contains(offer.originatingFactionId)) ++
|
||||
cf.newIncomingDiplomacyOffers,
|
||||
focusProvinceId = newFocusProvinceId,
|
||||
reconnedProvinces = newReconnedProvinces,
|
||||
lastActedProvinceIdThisRound = newLastActedProvinceIdThisRound,
|
||||
lastOutgoingTruceOfferRounds = newLastOutgoingTruceOfferRounds,
|
||||
lastOutgoingAllianceOfferRounds = newLastOutgoingAllianceOfferRounds,
|
||||
lastOutgoingInvitationRounds = newLastOutgoingInvitationRounds,
|
||||
lastOutgoingRansomOfferRounds = newLastOutgoingRansomOfferRounds
|
||||
)
|
||||
|
||||
gameState.copy(
|
||||
factions = gameState.factions.updated(cf.factionId, updatedFaction)
|
||||
)
|
||||
}
|
||||
|
||||
def applyRemovedFactions(removedFactionIds: Set[FactionId]): GameState = {
|
||||
val (removed, remaining) = gameState.factions.partition {
|
||||
case (fid, _) =>
|
||||
removedFactionIds.contains(fid)
|
||||
}
|
||||
gameState.copy(
|
||||
destroyedFactions = gameState.destroyedFactions ++ removed,
|
||||
factions = remaining
|
||||
)
|
||||
}
|
||||
|
||||
def applyNewFactions(newFactions: Vector[FactionT]): GameState =
|
||||
gameState.copy(
|
||||
factions = gameState.factions ++ newFactions.map(f => f.id -> f)
|
||||
)
|
||||
}
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
package net.eagle0.eagle.library.actions.applier
|
||||
|
||||
import net.eagle0.eagle.*
|
||||
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
|
||||
import net.eagle0.eagle.library.EagleInternalException
|
||||
import net.eagle0.eagle.model.action_result.concrete.{ChangedHeroC, StatAbsolute, StatDelta, StatNoChange}
|
||||
import net.eagle0.eagle.model.action_result.ChangedHeroT
|
||||
import net.eagle0.eagle.model.state.date.Date
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.hero.backstory_version.BackstoryVersion
|
||||
import net.eagle0.eagle.model.state.hero.concrete.HeroC
|
||||
import net.eagle0.eagle.model.state.hero.HeroT
|
||||
|
||||
object GameStateHeroExtensions {
|
||||
|
||||
extension (gameState: GameState) {
|
||||
|
||||
def applyNewHeroes(newHeroes: Vector[HeroT]): GameState =
|
||||
if newHeroes.isEmpty then gameState
|
||||
else {
|
||||
newHeroes.foreach(h =>
|
||||
internalRequire(
|
||||
!gameState.heroes.contains(h.id),
|
||||
s"Got a new hero update for existing hero ${h.id}"
|
||||
)
|
||||
)
|
||||
gameState.copy(
|
||||
heroes = gameState.heroes ++ newHeroes.map(h => h.id -> h)
|
||||
)
|
||||
}
|
||||
|
||||
def applyChangedHeroes(changedHeroes: Vector[ChangedHeroT], date: Date): GameState =
|
||||
if changedHeroes.isEmpty then gameState
|
||||
else
|
||||
changedHeroes.foldLeft(gameState) {
|
||||
case (gs, ch) =>
|
||||
gs.applyChangedHero(ch, date)
|
||||
}
|
||||
|
||||
def applyChangedHero(ch: ChangedHeroT, date: Date): GameState = ch match {
|
||||
case chc: ChangedHeroC => applyChangedHeroC(chc, date)
|
||||
case _ => throw new EagleInternalException(s"Unknown ChangedHeroT type: ${ch.getClass}")
|
||||
}
|
||||
|
||||
private def applyChangedHeroC(ch: ChangedHeroC, date: Date): GameState = {
|
||||
val existingHero = gameState.heroes(ch.heroId) match {
|
||||
case h: HeroC => h
|
||||
case h => throw new EagleInternalException(s"Unknown HeroT type: ${h.getClass}")
|
||||
}
|
||||
|
||||
val newFactionId = ch.newFactionId.orElse(
|
||||
if ch.clearFactionId then None else existingHero.factionId
|
||||
)
|
||||
|
||||
val newRoundIdJoined =
|
||||
if ch.newFactionId.isDefined then Some(gameState.currentRoundId)
|
||||
else if ch.clearFactionId then None
|
||||
else existingHero.roundIdJoined
|
||||
|
||||
val newVigor = ch.vigorChange match {
|
||||
case StatDelta(d) =>
|
||||
(existingHero.vigor + d).max(0.0).min(existingHero.constitution)
|
||||
case StatAbsolute(va) =>
|
||||
internalRequire(va >= 0, s"Got a negative new vigor of $va")
|
||||
internalRequire(
|
||||
va <= existingHero.constitution,
|
||||
s"Got a new vigor of $va with ${existingHero.constitution} constitution"
|
||||
)
|
||||
va
|
||||
case StatNoChange => existingHero.vigor
|
||||
}
|
||||
|
||||
val newLoyalty = ch.loyaltyChange match {
|
||||
case StatDelta(ld) =>
|
||||
Math.min(100.0, Math.max(0, existingHero.loyalty + ld))
|
||||
case StatAbsolute(la) =>
|
||||
internalRequire(la >= 0, s"Got a negative absolute loyalty of $la")
|
||||
internalRequire(la <= 100.0, s"Got an absolute loyalty of $la")
|
||||
la
|
||||
case StatNoChange => existingHero.loyalty
|
||||
}
|
||||
|
||||
val newBackstoryVersions = existingHero.backstoryVersions ++ ch.newBackstoryTextId.map { textId =>
|
||||
BackstoryVersion(textId = textId, date = date)
|
||||
}
|
||||
|
||||
val newBackstoryEvents =
|
||||
if ch.clearEventsForHeroBackstory then Vector.empty
|
||||
else existingHero.backstoryEvents ++ ch.newEventsForHeroBackstory
|
||||
|
||||
val updatedHero = existingHero.copy(
|
||||
factionId = newFactionId,
|
||||
roundIdJoined = newRoundIdJoined,
|
||||
vigor = newVigor,
|
||||
loyalty = newLoyalty,
|
||||
strengthXp = existingHero.strengthXp + ch.strengthXpDelta.getOrElse(0),
|
||||
agilityXp = existingHero.agilityXp + ch.agilityXpDelta.getOrElse(0),
|
||||
wisdomXp = existingHero.wisdomXp + ch.wisdomXpDelta.getOrElse(0),
|
||||
charismaXp = existingHero.charismaXp + ch.charismaXpDelta.getOrElse(0),
|
||||
constitutionXp = existingHero.constitutionXp + ch.constitutionXpDelta.getOrElse(0),
|
||||
strength = existingHero.strength + ch.strengthDelta.getOrElse(0),
|
||||
agility = existingHero.agility + ch.agilityDelta.getOrElse(0),
|
||||
wisdom = existingHero.wisdom + ch.wisdomDelta.getOrElse(0),
|
||||
charisma = existingHero.charisma + ch.charismaDelta.getOrElse(0),
|
||||
constitution = existingHero.constitution + ch.constitutionDelta.getOrElse(0),
|
||||
profession = ch.newProfession.getOrElse(existingHero.profession),
|
||||
backstoryVersions = newBackstoryVersions,
|
||||
backstoryEvents = newBackstoryEvents
|
||||
)
|
||||
|
||||
gameState.copy(
|
||||
heroes = gameState.heroes.updated(ch.heroId, updatedHero)
|
||||
)
|
||||
}
|
||||
|
||||
def applyRemovedHeroes(removedHeroIds: Set[HeroId]): GameState = {
|
||||
val (removed, remaining) = gameState.heroes.partition {
|
||||
case (k, _) =>
|
||||
removedHeroIds.contains(k)
|
||||
}
|
||||
val killedHeroes = removed.map {
|
||||
case (hid, hero) =>
|
||||
hero match {
|
||||
case h: HeroC => hid -> h.copy(factionId = None)
|
||||
case h => hid -> h
|
||||
}
|
||||
}
|
||||
gameState.copy(
|
||||
killedHeroes = gameState.killedHeroes ++ killedHeroes,
|
||||
heroes = remaining
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package net.eagle0.eagle.library.actions.applier
|
||||
|
||||
import net.eagle0.eagle.*
|
||||
import net.eagle0.eagle.model.action_result.NotificationT
|
||||
import net.eagle0.eagle.model.state.chronicle_entry.ChronicleEntry
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
|
||||
object GameStateMiscExtensions {
|
||||
|
||||
extension (gameState: GameState) {
|
||||
|
||||
def applyAccumulatedDetails(notification: Option[NotificationT]): GameState =
|
||||
notification
|
||||
.map(note => gameState.copy(deferredNotifications = gameState.deferredNotifications :+ note))
|
||||
.getOrElse(gameState)
|
||||
|
||||
def applyNewNotifications(notifications: Vector[NotificationT]): GameState =
|
||||
if notifications.isEmpty then gameState
|
||||
else gameState.copy(deferredNotifications = gameState.deferredNotifications ++ notifications)
|
||||
|
||||
def applyRemovedNotifications(notifications: Vector[NotificationT]): GameState =
|
||||
gameState.copy(
|
||||
deferredNotifications = gameState.deferredNotifications.diff(notifications)
|
||||
)
|
||||
|
||||
def applyNewSeed(newSeed: Option[Long]): GameState =
|
||||
newSeed.map(s => gameState.copy(randomSeed = s)).getOrElse(gameState)
|
||||
|
||||
def applyChronicleEntry(chronicleEntry: Option[ChronicleEntry]): GameState =
|
||||
chronicleEntry.map { ce =>
|
||||
val updatedEntries = gameState.chronicleEntries.indexWhere(_.date == ce.date) match {
|
||||
case -1 => gameState.chronicleEntries :+ ce
|
||||
case idx => gameState.chronicleEntries.updated(idx, ce)
|
||||
}
|
||||
gameState.copy(chronicleEntries = updatedEntries)
|
||||
}
|
||||
.getOrElse(gameState)
|
||||
|
||||
def applyCommandCountUpdate(actingFactionId: Option[FactionId]): GameState =
|
||||
actingFactionId.map { factionId =>
|
||||
val existingCount = gameState.factionCommandCounts.getOrElse(factionId, 0)
|
||||
gameState.copy(
|
||||
factionCommandCounts = gameState.factionCommandCounts.updated(factionId, existingCount + 1)
|
||||
)
|
||||
}
|
||||
.getOrElse(gameState)
|
||||
}
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
package net.eagle0.eagle.library.actions.applier
|
||||
|
||||
import net.eagle0.eagle.*
|
||||
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
|
||||
import net.eagle0.eagle.library.EagleInternalException
|
||||
import net.eagle0.eagle.model.action_result.changed_province.concrete.ChangedProvinceC
|
||||
import net.eagle0.eagle.model.action_result.changed_province.ChangedProvinceT
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.province.concrete.ProvinceC
|
||||
import net.eagle0.eagle.model.state.province.ProvinceT
|
||||
|
||||
object GameStateProvinceExtensions {
|
||||
|
||||
extension (gameState: GameState) {
|
||||
|
||||
def applyNewProvinces(newProvinces: Vector[ProvinceT]): GameState =
|
||||
if newProvinces.isEmpty then gameState
|
||||
else {
|
||||
newProvinces.foreach(p =>
|
||||
internalRequire(
|
||||
!gameState.provinces.contains(p.id),
|
||||
s"Got a new province update for existing province ${p.id}"
|
||||
)
|
||||
)
|
||||
gameState.copy(
|
||||
provinces = gameState.provinces ++ newProvinces.map(p => p.id -> p)
|
||||
)
|
||||
}
|
||||
|
||||
def applyChangedProvinces(changedProvinces: Vector[ChangedProvinceT]): GameState =
|
||||
if changedProvinces.isEmpty then gameState
|
||||
else
|
||||
changedProvinces.foldLeft(gameState) {
|
||||
case (gs, cp) =>
|
||||
gs.applyChangedProvince(cp)
|
||||
}
|
||||
|
||||
def applyChangedProvince(cp: ChangedProvinceT): GameState = cp match {
|
||||
case cpc: ChangedProvinceC => applyChangedProvinceC(cpc)
|
||||
case _ =>
|
||||
throw new EagleInternalException(s"Unknown ChangedProvinceT type: ${cp.getClass}")
|
||||
}
|
||||
|
||||
private def applyChangedProvinceC(cp: ChangedProvinceC): GameState = {
|
||||
val provinceBefore = gameState.provinces(cp.provinceId) match {
|
||||
case p: ProvinceC => p
|
||||
case p => throw new EagleInternalException(s"Unknown ProvinceT type: ${p.getClass}")
|
||||
}
|
||||
|
||||
val factionLeaderIds = gameState.factions.values.flatMap(_.leaderIds).toVector
|
||||
|
||||
// Use helper files to avoid compiler crash
|
||||
val p1 = ProvinceUpdateHelpers.applyRulingAndBattalions(gameState, provinceBefore, cp, factionLeaderIds)
|
||||
val p2 = ProvinceUpdateHelpers.applyArmiesAndResources(p1, cp)
|
||||
val p3 = ProvinceUpdateHelpers2.applyHeroesAndEvents(p2, cp)
|
||||
val provinceApplied = ProvinceUpdateHelpers2.applyMiscFields(gameState, p3, cp)
|
||||
|
||||
val provinceFixedForRuler = ProvinceUpdateHelpers2.fixRulerIfNeeded(gameState, provinceApplied)
|
||||
|
||||
gameState.copy(
|
||||
provinces = gameState.provinces.updated(cp.provinceId, provinceFixedForRuler)
|
||||
)
|
||||
}
|
||||
|
||||
def applyProvinceActed(actedProvince: Option[ProvinceId]): GameState = {
|
||||
internalRequire(!actedProvince.contains(0), "Province acted has id 0")
|
||||
actedProvince.map { id =>
|
||||
val province = gameState.provinces(id) match {
|
||||
case p: ProvinceC => p
|
||||
case p => throw new EagleInternalException(s"Unknown ProvinceT type: ${p.getClass}")
|
||||
}
|
||||
gameState.copy(
|
||||
provinces = gameState.provinces.updated(id, province.copy(hasActed = true))
|
||||
)
|
||||
}
|
||||
.getOrElse(gameState)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package net.eagle0.eagle.library.actions.applier
|
||||
|
||||
import net.eagle0.eagle.*
|
||||
import net.eagle0.eagle.library.util.hero.HeroUtils
|
||||
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
|
||||
import net.eagle0.eagle.model.action_result.changed_province.concrete.{ChangedProvinceC, HostileArmyStatusChange}
|
||||
import net.eagle0.eagle.model.state.{HostileArmyGroup, MovingArmy}
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.province.concrete.ProvinceC
|
||||
|
||||
// Helper object 1 for province updates - split to avoid compiler crash
|
||||
object ProvinceUpdateHelpers {
|
||||
|
||||
def applyRulingAndBattalions(
|
||||
gameState: GameState,
|
||||
p: ProvinceC,
|
||||
cp: ChangedProvinceC,
|
||||
factionLeaderIds: Vector[HeroId]
|
||||
): ProvinceC =
|
||||
p.copy(
|
||||
rulingFactionId =
|
||||
if cp.clearRulingFactionId then None
|
||||
else cp.newRulingFactionId.orElse(p.rulingFactionId),
|
||||
rulingFactionHeroIds = (p.rulingFactionHeroIds
|
||||
.diff(cp.removedRulingFactionHeroIds) ++ cp.newRulingFactionHeroIds)
|
||||
.map(hid => gameState.heroes(hid))
|
||||
.sorted(using HeroUtils.sortOrdering(factionLeaderIds))
|
||||
.map(_.id),
|
||||
battalionIds = p.battalionIds.diff(cp.removedBattalionIds) ++ cp.newBattalionIds,
|
||||
incomingArmies = modifyArmySet(
|
||||
existingArmies = p.incomingArmies,
|
||||
removedIds = cp.removedIncomingArmyIds,
|
||||
addedArmies = cp.newIncomingArmies
|
||||
),
|
||||
hostileArmies = modifyArmyGroupSet(
|
||||
existingGroups = p.hostileArmies,
|
||||
removedFactionIds = cp.removedHostileArmyFactionIds,
|
||||
addedArmies = cp.newHostileArmies,
|
||||
statusChanges = cp.hostileArmyStatusChanges
|
||||
)
|
||||
)
|
||||
|
||||
def applyArmiesAndResources(
|
||||
p: ProvinceC,
|
||||
cp: ChangedProvinceC
|
||||
): ProvinceC = {
|
||||
val incomingShipments = {
|
||||
val holdovers = p.incomingShipments
|
||||
.filterNot(sh => cp.removedIncomingShipmentIds.contains(sh.id))
|
||||
val nextId = holdovers.map(_.id).maxOption.getOrElse(0) + 1
|
||||
holdovers ++ cp.newIncomingShipments.zipWithIndex.map {
|
||||
case (ship, index) =>
|
||||
ship.copy(id = index + nextId)
|
||||
}
|
||||
}
|
||||
|
||||
val defendingArmy =
|
||||
if cp.clearDefendingArmy then None
|
||||
else
|
||||
cp.newDefendingArmy.orElse(
|
||||
p.defendingArmy.map { da =>
|
||||
val newUnits = da.units
|
||||
.filterNot(u => cp.removedRulingFactionHeroIds.contains(u.heroId))
|
||||
.map { u =>
|
||||
u.copy(battalionId = u.battalionId.filterNot(cp.removedBattalionIds.contains))
|
||||
}
|
||||
da.copy(units = newUnits)
|
||||
}
|
||||
)
|
||||
|
||||
val newGold = p.gold + cp.goldDelta.getOrElse(0)
|
||||
internalRequire(newGold >= 0, s"Got a goldDelta of ${cp.goldDelta} but only ${p.gold} available")
|
||||
|
||||
val newFood = p.food + cp.foodDelta.getOrElse(0)
|
||||
internalRequire(newFood >= 0, s"Got a foodDelta of ${cp.foodDelta} but only ${p.food} available")
|
||||
|
||||
p.copy(
|
||||
incomingShipments = incomingShipments,
|
||||
defendingArmy = defendingArmy,
|
||||
gold = newGold,
|
||||
food = newFood,
|
||||
priceIndex = cp.newPriceIndex.getOrElse(p.priceIndex),
|
||||
economy = (p.economy + cp.economyDelta.getOrElse(0.0)).max(0.0).min(100.0),
|
||||
agriculture = (p.agriculture + cp.agricultureDelta.getOrElse(0.0)).max(0.0).min(100.0),
|
||||
infrastructure = (p.infrastructure + cp.infrastructureDelta.getOrElse(0.0)).max(0.0).min(100.0),
|
||||
economyDevastation = (p.economyDevastation + cp.economyDevastationDelta.getOrElse(0.0)).max(0.0).min(p.economy),
|
||||
agricultureDevastation =
|
||||
(p.agricultureDevastation + cp.agricultureDevastationDelta.getOrElse(0.0)).max(0.0).min(p.agriculture),
|
||||
infrastructureDevastation =
|
||||
(p.infrastructureDevastation + cp.infrastructureDevastationDelta.getOrElse(0.0)).max(0.0).min(p.infrastructure),
|
||||
support = (p.support + cp.supportDelta.getOrElse(0.0)).max(0.0).min(100.0)
|
||||
)
|
||||
}
|
||||
|
||||
private def modifyArmySet(
|
||||
existingArmies: Vector[MovingArmy],
|
||||
removedIds: Vector[Int],
|
||||
addedArmies: Vector[MovingArmy]
|
||||
): Vector[MovingArmy] = {
|
||||
val holdovers = existingArmies.filterNot(ma => removedIds.contains(ma.id))
|
||||
val nextId = holdovers.map(_.id).maxOption.getOrElse(0) + 1
|
||||
holdovers ++ addedArmies.zipWithIndex.map {
|
||||
case (army, index) =>
|
||||
army.copy(id = index + nextId)
|
||||
}
|
||||
}
|
||||
|
||||
private def modifyArmyGroupSet(
|
||||
existingGroups: Vector[HostileArmyGroup],
|
||||
removedFactionIds: Vector[FactionId],
|
||||
addedArmies: Vector[HostileArmyGroup],
|
||||
statusChanges: Vector[HostileArmyStatusChange]
|
||||
): Vector[HostileArmyGroup] =
|
||||
statusChanges
|
||||
.foldLeft(existingGroups.filterNot(g => removedFactionIds.contains(g.factionId))) {
|
||||
case (groups, HostileArmyStatusChange(factionId, newStatus)) =>
|
||||
groups.indexWhere(_.factionId == factionId) match {
|
||||
case -1 => groups
|
||||
case index => groups.updated(index, groups(index).copy(status = newStatus))
|
||||
}
|
||||
} ++ addedArmies
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package net.eagle0.eagle.library.actions.applier
|
||||
|
||||
import net.eagle0.eagle.library.EagleInternalException
|
||||
import net.eagle0.eagle.model.action_result.changed_province.concrete.ChangedProvinceC
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.province.{BeastsEvent, ImminentRiotEvent, ProvinceOrderType}
|
||||
import net.eagle0.eagle.model.state.province.concrete.ProvinceC
|
||||
import net.eagle0.eagle.model.state.unaffiliated_hero.{RecruitmentInfo, UnaffiliatedHeroType}
|
||||
import net.eagle0.eagle.model.state.RoundPhase
|
||||
|
||||
// Helper object 2 for province updates - split to avoid compiler crash
|
||||
object ProvinceUpdateHelpers2 {
|
||||
|
||||
def applyHeroesAndEvents(
|
||||
p: ProvinceC,
|
||||
cp: ChangedProvinceC
|
||||
): ProvinceC = {
|
||||
val unaffiliatedHeroes =
|
||||
p.unaffiliatedHeroes
|
||||
.filterNot(uh => cp.removedUnaffiliatedHeroIds.contains(uh.heroId))
|
||||
.filterNot(uh => cp.changedUnaffiliatedHeroes.map(_.heroId).contains(uh.heroId))
|
||||
++ cp.changedUnaffiliatedHeroes ++ cp.newUnaffiliatedHeroes
|
||||
|
||||
val capturedHeroes =
|
||||
p.capturedHeroes
|
||||
.filterNot(ch => cp.removedCapturedHeroIds.contains(ch.heroId))
|
||||
.map(ch =>
|
||||
if cp.recruitmentAtteptedCapturedHeroIds.contains(ch.heroId)
|
||||
then ch.copy(recruitmentAttempted = true)
|
||||
else ch
|
||||
) ++ cp.newCapturedHeroes
|
||||
|
||||
val lockedImprovementType = cp.newLockedImprovementType match {
|
||||
case Some(net.eagle0.eagle.model.action_result.changed_province.concrete.NewLockedImprovementType.None) =>
|
||||
None
|
||||
case Some(net.eagle0.eagle.model.action_result.changed_province.concrete.NewLockedImprovementType.New(v)) =>
|
||||
Some(v)
|
||||
case None => p.lockedImprovementType
|
||||
}
|
||||
|
||||
p.copy(
|
||||
hasActed = cp.setHasActed.getOrElse(p.hasActed),
|
||||
rulerIsTraveling = cp.setRulerIsTraveling.getOrElse(p.rulerIsTraveling),
|
||||
unaffiliatedHeroes = unaffiliatedHeroes,
|
||||
capturedHeroes = capturedHeroes,
|
||||
provinceOrders = cp.newProvinceOrders.getOrElse(p.provinceOrders),
|
||||
activeEvents = cp.newProvinceEvents.getOrElse(p.activeEvents),
|
||||
lockedImprovementType = lockedImprovementType
|
||||
)
|
||||
}
|
||||
|
||||
def applyMiscFields(
|
||||
gameState: GameState,
|
||||
p: ProvinceC,
|
||||
cp: ChangedProvinceC
|
||||
): ProvinceC =
|
||||
p.copy(
|
||||
lastBeastsDate =
|
||||
if cp.newProvinceEvents.exists(_.exists(_.isInstanceOf[BeastsEvent]))
|
||||
then gameState.currentDate
|
||||
else p.lastBeastsDate,
|
||||
lastRiotDate =
|
||||
if cp.newProvinceEvents.exists(_.exists(_.isInstanceOf[ImminentRiotEvent]))
|
||||
then gameState.currentDate
|
||||
else p.lastRiotDate,
|
||||
incomingEndTurnActions = (p.incomingEndTurnActions ++ cp.newIncomingEndTurnActions)
|
||||
.diff(cp.removedIncomingEndTurnActions),
|
||||
deferredChanges = cp.removedDeferredChangeIndex
|
||||
.map(idx => p.deferredChanges.patch(idx, Nil, 1))
|
||||
.getOrElse(p.deferredChanges) ++ cp.newDeferredChange.toVector,
|
||||
battleRevelations = p.battleRevelations.diff(cp.removedBattleRevelations) ++ cp.newBattleRevelations
|
||||
)
|
||||
|
||||
def fixRulerIfNeeded(gameState: GameState, p: ProvinceC): ProvinceC =
|
||||
if gameState.currentPhase != RoundPhase.PlayerCommands &&
|
||||
(p.rulingFactionId.isEmpty || p.rulingFactionHeroIds.isEmpty)
|
||||
then
|
||||
p.copy(
|
||||
rulingFactionId = None,
|
||||
rulingHeroId = None,
|
||||
rulingFactionHeroIds = Vector.empty,
|
||||
support = 0,
|
||||
provinceOrders = ProvinceOrderType.UnknownOrderType,
|
||||
unaffiliatedHeroes = p.unaffiliatedHeroes.map(uh =>
|
||||
uh.copy(
|
||||
recruitmentInfo = uh.unaffiliatedHeroType match {
|
||||
case UnaffiliatedHeroType.Prisoner => RecruitmentInfo.Prisoner
|
||||
case UnaffiliatedHeroType.MovingPrisoner => RecruitmentInfo.MovingPrisoner
|
||||
case UnaffiliatedHeroType.ReturningPrisoner => RecruitmentInfo.NoRulerInProvince
|
||||
case UnaffiliatedHeroType.Outlaw => RecruitmentInfo.Outlaw
|
||||
case UnaffiliatedHeroType.Traveler => RecruitmentInfo.Traveler
|
||||
case UnaffiliatedHeroType.Resident => RecruitmentInfo.NoRulerInProvince
|
||||
case UnaffiliatedHeroType.Unknown =>
|
||||
throw new EagleInternalException("Unknown unaffiliated hero type")
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
else
|
||||
p.copy(
|
||||
rulingHeroId = p.rulingFactionHeroIds.headOption
|
||||
)
|
||||
}
|
||||
+2
-1
@@ -24,6 +24,7 @@ import net.eagle0.eagle.library.actions.impl.command.AvailableCommandTypeMap
|
||||
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
|
||||
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
|
||||
import net.eagle0.eagle.library.util.IncomingArmyUtils
|
||||
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
|
||||
|
||||
object AvailableCommandsFactory {
|
||||
def shouldFollow(
|
||||
@@ -492,7 +493,7 @@ class AvailableCommandsFactory(
|
||||
for {
|
||||
faction <- gs.factions.get(fid)
|
||||
pleaseRecruitMeCommand <- AvailablePleaseRecruitMeCommandFactory
|
||||
.availableCommand(gs, faction.id)
|
||||
.availableCommand(GameStateConverter.fromProto(gs), faction.id)
|
||||
oneProvinceAvailableCommand <- optionalOneProvinceAvailableCommands(
|
||||
gs = gs,
|
||||
0,
|
||||
|
||||
+11
-7
@@ -1,22 +1,26 @@
|
||||
package net.eagle0.eagle.library.actions.availability
|
||||
|
||||
import net.eagle0.eagle.api.available_command.{OneProvincePleaseRecruitMe, PleaseRecruitMeAvailableCommand}
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.internal.province.Province
|
||||
import net.eagle0.eagle.library.settings.MinSupportForTaxes
|
||||
import net.eagle0.eagle.library.util.unaffiliated_hero.LegacyUnaffiliatedHeroUtils
|
||||
import net.eagle0.eagle.library.util.unaffiliated_hero.UnaffiliatedHeroUtils
|
||||
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
|
||||
import net.eagle0.eagle.model.proto_converters.UnaffiliatedHeroConverter
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.province.ProvinceT
|
||||
import net.eagle0.eagle.FactionId
|
||||
|
||||
object AvailablePleaseRecruitMeCommandFactory {
|
||||
|
||||
private def forOneProvince(
|
||||
p: Province,
|
||||
p: ProvinceT,
|
||||
fid: FactionId,
|
||||
gs: GameState
|
||||
): Option[OneProvincePleaseRecruitMe] = {
|
||||
// Convert to proto only for ExpandedUnaffiliatedHeroUtils which still requires proto types
|
||||
val gsProto = GameStateConverter.toProto(gs)
|
||||
val availableHeroes = for {
|
||||
targetUH <- p.unaffiliatedHeroes.filter(uh =>
|
||||
LegacyUnaffiliatedHeroUtils.willPleaseRecruitMe(
|
||||
UnaffiliatedHeroUtils.willPleaseRecruitMe(
|
||||
gameState = gs,
|
||||
factionId = fid,
|
||||
unaffiliatedHero = uh
|
||||
@@ -26,8 +30,8 @@ object AvailablePleaseRecruitMeCommandFactory {
|
||||
// FIXME: We should be generating the LLM request and its textID here instead of earlier in the round
|
||||
ExpandedUnaffiliatedHeroUtils
|
||||
.expandedUnaffiliatedHero(
|
||||
gs = gs,
|
||||
uh = targetUH
|
||||
gs = gsProto,
|
||||
uh = UnaffiliatedHeroConverter.toProto(targetUH)
|
||||
)
|
||||
|
||||
Option.when(availableHeroes.nonEmpty)(
|
||||
|
||||
@@ -63,6 +63,8 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:incoming_army_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils:legacy_faction_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/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -499,7 +501,13 @@ scala_library(
|
||||
"//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/settings:min_support_for_taxes",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/unaffiliated_hero:legacy_unaffiliated_hero_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/unaffiliated_hero",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:unaffiliated_hero_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/quest",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/unaffiliated_hero",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -155,6 +155,8 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library:game_history",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/date",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -195,7 +197,7 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:truce_months_from_returning_leader",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:eagle_require",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/unaffiliated_hero:legacy_unaffiliated_hero_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/unaffiliated_hero",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/view_filters:province_view_filter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
|
||||
@@ -213,19 +215,22 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types:end_aftermath_phase_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:battle_revelation_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:notification_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:unaffiliated_hero_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/date",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/faction",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/hero",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/llm_request",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:battle_revelation",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/faction:faction_relationship",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero:gender",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/quest",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/quest/concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/unaffiliated_hero",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/unaffiliated_hero/concrete",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -275,18 +280,18 @@ scala_library(
|
||||
"//src/test/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:deterministic_single_result_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_simple_action",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:faction_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/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:deterministic_single_result_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_simple_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:eagle_require",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province/concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:action_result_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types:end_battle_request_phase_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -298,17 +303,21 @@ scala_library(
|
||||
"//src/test/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:deterministic_single_result_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_simple_action",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:faction_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/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:deterministic_single_result_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_simple_action",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_faction_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_hero_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:notification_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:action_result_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types:end_resolution_phase_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -399,7 +408,6 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_trait_applier",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_state_trait_sequencer",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:vigor_xp_applier",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:prisoner_escape_chance",
|
||||
@@ -446,19 +454,18 @@ scala_library(
|
||||
"//src/test/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:deterministic_single_result_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_simple_action",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:round_phase_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:faction_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/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:deterministic_single_result_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_simple_action",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province/concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:action_result_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types:end_defense_decision_phase_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:army",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -476,28 +483,13 @@ scala_library(
|
||||
],
|
||||
deps = [
|
||||
":withdrawn_army_returns_home_action",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:faction_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/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:deterministic_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_hero_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:notification_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:action_result_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_battalion_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_hero_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types:end_free_for_all_decision_phase_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero:gender",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -511,7 +503,7 @@ scala_library(
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_random_sequential_results_action",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
|
||||
@@ -521,23 +513,29 @@ scala_library(
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/scala/net/eagle0/common:functional_random",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_trait_applier",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/command:command_factory",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/command:legacy_handle_riot_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/command:handle_riot_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_state_proto_sequencer",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_random_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_state_trait_sequencer",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/province:legacy_province_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_hero_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:notification_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province/concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:action_result_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_battalion_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_hero_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types:end_handle_riots_phase_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
],
|
||||
)
|
||||
@@ -550,19 +548,21 @@ scala_library(
|
||||
"//src/test/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:deterministic_single_result_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_simple_action",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:round_phase_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:faction_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/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:deterministic_single_result_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_simple_action",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_faction_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_hero_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:notification_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:action_result_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types:end_free_for_all_battle_request_phase_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -574,19 +574,21 @@ scala_library(
|
||||
"//src/test/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:deterministic_single_result_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_simple_action",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:round_phase_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:faction_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/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:deterministic_single_result_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_simple_action",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_faction_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_hero_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:notification_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:action_result_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types:end_free_for_all_battle_resolution_phase_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -598,17 +600,23 @@ scala_library(
|
||||
"//src/test/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:deterministic_single_result_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_simple_action",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:faction_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/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:deterministic_single_result_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_simple_action",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_faction_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_hero_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:notification_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:action_result_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:notification_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types:end_please_recruit_me_phase_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -680,6 +688,7 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/date",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/faction",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero:gender",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
@@ -699,21 +708,18 @@ scala_library(
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:t_random_sequential_results_action",
|
||||
],
|
||||
deps = [
|
||||
":check_for_faction_changes_action",
|
||||
":check_for_failed_quests_action",
|
||||
":check_for_fulfilled_quests_action",
|
||||
":hero_backstory_update_action_generator",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:faction_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/scala/net/eagle0/common:functional_random",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_state_proto_sequencer",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_trait_applier",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_state_trait_sequencer",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:t_random_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:eagle_require",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
|
||||
@@ -724,15 +730,17 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_battalion_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_hero_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:action_result_proto_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types:end_vassal_commands_phase_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:battalion_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/date",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/faction",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/hero",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/date",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/faction",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero:gender",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
@@ -882,7 +890,7 @@ scala_library(
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:t_random_sequential_results_action",
|
||||
],
|
||||
deps = [
|
||||
":chronicle_event_generator",
|
||||
@@ -896,8 +904,9 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/common:functional_random",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library:game_history",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_state_proto_sequencer",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_trait_applier",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_state_trait_sequencer",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:t_random_sequential_results_action",
|
||||
"//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",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:faction_bias_multiplier_per_round",
|
||||
@@ -910,9 +919,32 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:eagle_require",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:price_index_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/province:legacy_province_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_faction_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_hero_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:notification_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province/concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:action_result_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_faction_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_hero_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request/chronicle_event",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types:new_round_action_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types/base:action_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:action_result_proto_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:unaffiliated_hero_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/date",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/hero",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/llm_request/chronicle_event",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/chronicle_entry",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/date",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/faction",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/quest",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/unaffiliated_hero",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -924,24 +956,32 @@ scala_library(
|
||||
"//src/test/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:deterministic_single_result_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_simple_action",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:faction_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/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:deterministic_single_result_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_simple_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:ambition_factor_for_loyalty_degradation",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:loyalty_decrease_per_discordance",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:loyalty_discordance_threshold",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:minimum_rounds_before_loyalty_degrades",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:percent_degradation_per_year",
|
||||
"//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/library/util/hero:hero_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_faction_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_hero_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:notification_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province/concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:action_result_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_battalion_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_hero_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types:new_year_action_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -953,20 +993,29 @@ scala_library(
|
||||
"//src/test/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:deterministic_single_result_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_simple_action",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:faction_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/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:deterministic_single_result_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_simple_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:max_battalion_reduction_for_food_shortage",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:legacy_battalion_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/province:legacy_province_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:battalion_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_faction_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_hero_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:notification_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province/concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:action_result_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_battalion_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types:food_consumed_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:army",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -997,8 +1046,6 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_hero_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:notification_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:action_result_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_battalion_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_hero_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:action_result_proto_converter",
|
||||
@@ -1045,16 +1092,25 @@ scala_library(
|
||||
"//src/test/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:deterministic_single_result_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_simple_action",
|
||||
],
|
||||
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/library/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:deterministic_single_result_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_simple_action",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_faction_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_hero_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:notification_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province/concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:action_result_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types:set_up_hostile_armies_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:army",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1114,16 +1170,12 @@ scala_library(
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_random_sequential_results_action",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:province_event_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:round_phase_scala_proto",
|
||||
"//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/common:functional_random",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_random_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:base_beasts_count",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:beasts_duration_months",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:beasts_event_chance",
|
||||
@@ -1158,10 +1210,23 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:months_between_riots_support_multiplier",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:riot_event_chance",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:beast_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:date_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:incoming_army_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:province_event_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/province:legacy_province_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province/concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:action_result_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_battalion_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_faction_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_hero_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types:province_events_changed_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:beast_info",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/date",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/faction",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/quest",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/unaffiliated_hero",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1175,20 +1240,20 @@ scala_library(
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:t_random_sequential_results_action",
|
||||
],
|
||||
deps = [
|
||||
":end_province_move_resolution_phase_action",
|
||||
":friendly_move_action",
|
||||
":shipment_arrived_action",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:round_phase_scala_proto",
|
||||
"//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/common:functional_random",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_trait_applier",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:t_random_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_hero_trait",
|
||||
@@ -1198,13 +1263,14 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_battalion_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_hero_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:action_result_proto_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:army_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:supplies_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/faction",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/hero",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:army",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero:gender",
|
||||
],
|
||||
@@ -1257,20 +1323,35 @@ scala_library(
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:t_random_sequential_results_action",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:round_phase_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:province_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_state_proto_sequencer",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_trait_applier",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_state_trait_sequencer",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:t_random_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:returning_heroes",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/view_filters:province_view_filter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:changed_hero_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:changed_province_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
|
||||
# Transitive deps of action_result_concrete (needed because ActionResultC has fields of these types)
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_hero_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:notification_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province/concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:action_result_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_faction_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:client_text_visibility_extension_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types:end_recon_resolution_phase_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types:recon_succeeded_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:incoming_end_turn_action_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1284,27 +1365,25 @@ scala_library(
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:t_random_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/hero_generator",
|
||||
],
|
||||
deps = [
|
||||
":end_unaffiliated_hero_actions_phase_action",
|
||||
":unaffiliated_hero_appeared_action",
|
||||
":unaffiliated_hero_moved_action",
|
||||
":unaffiliated_hero_rejoined_action",
|
||||
":unaffiliated_heroes_changed_action",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:round_phase_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:unaffiliated_hero_quest_scala_proto",
|
||||
"//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/common:functional_random",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_trait_applier",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:deterministic_single_result_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_state_proto_sequencer",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_state_trait_sequencer",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:t_random_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/name_generation_request",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:free_hero_move_vigor_cost",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:min_vigor_for_free_hero_move",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:new_hero_chance",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:resident_to_traveler_chance",
|
||||
@@ -1312,28 +1391,37 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:traveler_move_chance",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:traveler_to_resident_chance",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:province_distances",
|
||||
"//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_generator",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/hero_generator/hero_generation_response",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/hero_generator/name_info",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/province",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/province:legacy_province_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/unaffiliated_hero:legacy_unaffiliated_hero_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/unaffiliated_hero",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_hero_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:notification_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province/concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:action_result_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_battalion_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_hero_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:notification_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:action_result_proto_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:unaffiliated_hero_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/date",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types:hero_changed_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types:hero_moved_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types:new_quests_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/faction",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/hero",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero:gender",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero/concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/quest",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/quest/concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/unaffiliated_hero",
|
||||
@@ -1351,8 +1439,7 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/command:command_factory",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:deterministic_single_result_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_random_sequential_results_action",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
|
||||
@@ -1363,12 +1450,12 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/common:functional_random",
|
||||
"//src/main/scala/net/eagle0/common:more_option",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_trait_applier",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/command:command_factory",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:deterministic_single_result_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_state_proto_sequencer",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_random_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_state_trait_sequencer",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:command_chooser",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/province:legacy_province_utils",
|
||||
@@ -1376,10 +1463,13 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_hero_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:notification_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:action_result_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_battalion_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_hero_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1391,24 +1481,34 @@ scala_library(
|
||||
"//src/test/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/command:command_factory",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_random_sequential_results_action",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:round_phase_scala_proto",
|
||||
"//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/common:functional_random",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_trait_applier",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/command:command_factory",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_random_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_state_trait_sequencer",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/province:legacy_province_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_hero_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:notification_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:action_result_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_battalion_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_hero_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
@@ -1622,7 +1722,6 @@ scala_library(
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:deterministic_single_result_action",
|
||||
],
|
||||
deps = [
|
||||
":free_for_all_draw_action",
|
||||
@@ -1638,7 +1737,6 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library:eagle_internal_exception",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:deterministic_single_result_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/util:shattered_army_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:battle_agriculture_devastation_delta",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:battle_economy_devastation_delta",
|
||||
@@ -1653,8 +1751,6 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:notification_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province/concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:action_result_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_battalion_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_hero_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:action_result_proto_converter",
|
||||
@@ -1755,54 +1851,37 @@ scala_library(
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:t_random_sequential_results_action",
|
||||
],
|
||||
deps = [
|
||||
":withdrawn_army_returns_home_action",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:round_phase_scala_proto",
|
||||
"//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/common:functional_random",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_trait_applier",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_state_proto_sequencer",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:random_state_trait_sequencer",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:t_random_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils:legacy_faction_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_hero_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:notification_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province/concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:action_result_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_battalion_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_hero_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types:end_truce_turn_back_phase_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types:withdrawal_for_truce_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:army",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "unaffiliated_heroes_changed_action",
|
||||
srcs = ["UnaffiliatedHeroesChangedAction.scala"],
|
||||
visibility = [
|
||||
"//src/main/scala/net/eagle0/eagle/library:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:deterministic_single_result_action",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:round_phase_scala_proto",
|
||||
"//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/library/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:deterministic_single_result_action",
|
||||
],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "unaffiliated_hero_appeared_action",
|
||||
srcs = ["UnaffiliatedHeroAppearedAction.scala"],
|
||||
@@ -1850,7 +1929,6 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/common:functional_random",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:deterministic_single_result_action",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:round_phase_scala_proto",
|
||||
@@ -1859,9 +1937,13 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/common:functional_random",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:deterministic_single_result_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:free_hero_move_vigor_cost",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/unaffiliated_hero:legacy_unaffiliated_hero_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/unaffiliated_hero",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:unaffiliated_hero_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/quest",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/unaffiliated_hero",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1875,7 +1957,6 @@ scala_library(
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:deterministic_single_result_action",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
|
||||
+1
-1
@@ -33,7 +33,6 @@ import net.eagle0.eagle.common.action_result_notification_details.ShatteredArmyD
|
||||
Unrecognized
|
||||
}
|
||||
import net.eagle0.eagle.common.action_result_type.ActionResultType.FACTION_DESTROYED
|
||||
import net.eagle0.eagle.common.date.Date
|
||||
import net.eagle0.eagle.internal.action_result.ActionResult
|
||||
import net.eagle0.eagle.internal.event_for_chronicle.{
|
||||
AllianceAcceptedEvent,
|
||||
@@ -70,6 +69,7 @@ import net.eagle0.eagle.internal.event_for_chronicle.ShatteredArmyEvent.Reason.{
|
||||
}
|
||||
import net.eagle0.eagle.library.actions.impl.common.ActionWithResultingState
|
||||
import net.eagle0.eagle.library.GameHistory
|
||||
import net.eagle0.eagle.model.state.date.Date
|
||||
|
||||
object ChronicleEventGenerator {
|
||||
private def relevantNotifications(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user