mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-29 05:15:44 +00:00
Compare commits
51
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6e4a5798b | ||
|
|
6b39e0632a | ||
|
|
1a6ce2ef2d | ||
|
|
735013e39f | ||
|
|
0b3d02ddf9 | ||
|
|
69e147548f | ||
|
|
72c6bb0122 | ||
|
|
cec62a6abb | ||
|
|
f37b697444 | ||
|
|
736c72845d | ||
|
|
fb3b8caf3b | ||
|
|
36f104c828 | ||
|
|
467dfcebb9 | ||
|
|
1193fb98b1 | ||
|
|
e9aa8242b1 | ||
|
|
569d665626 | ||
|
|
2242f53bce | ||
|
|
75effa4f39 | ||
|
|
091a6ee3ad | ||
|
|
e89e36b8b7 | ||
|
|
b69faa5b2d | ||
|
|
d995b0c949 | ||
|
|
836c975a97 | ||
|
|
aafd622a81 | ||
|
|
f4ef8a949d | ||
|
|
e88b78525d | ||
|
|
9d1c54cb81 | ||
|
|
9e616e5c05 | ||
|
|
76717d660e | ||
|
|
ca9e3e664f | ||
|
|
a0396c8bff | ||
|
|
257d8bed30 | ||
|
|
f23ea0b10c | ||
|
|
0d066b1ce6 | ||
|
|
addc2c13fe | ||
|
|
e22d360410 | ||
|
|
d74fc82df7 | ||
|
|
f91f273539 | ||
|
|
3f5c5e4a10 | ||
|
|
dbf539ff15 | ||
|
|
9875055787 | ||
|
|
7fbf1fb43c | ||
|
|
36c92f7e91 | ||
|
|
d64c8064f6 | ||
|
|
08414206ff | ||
|
|
65689dce38 | ||
|
|
7684e4c218 | ||
|
|
935b9341cd | ||
|
|
59c555a297 | ||
|
|
414537c617 | ||
|
|
75a595e289 |
@@ -30,3 +30,7 @@ common --java_language_version=17
|
||||
common --java_runtime_version=remotejdk_17
|
||||
common --tool_java_language_version=17
|
||||
common --tool_java_runtime_version=remotejdk_17
|
||||
|
||||
build --bes_results_url=https://app.buildbuddy.io/invocation/
|
||||
build --bes_backend=grpcs://remote.buildbuddy.io
|
||||
build --remote_header=x-buildbuddy-api-key=vxkxHhPAglBKl6yKhMy2
|
||||
|
||||
+1
-1
@@ -6,4 +6,4 @@
|
||||
*.bytes filter=lfs diff=lfs merge=lfs -text
|
||||
*.psd filter=lfs diff=lfs merge=lfs -text
|
||||
*.ttf filter=lfs diff=lfs merge=lfs -text
|
||||
*.tsv filter=lfs diff=lfs merge=lfs -text
|
||||
*.herodata filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
@@ -4,12 +4,14 @@ on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
paths:
|
||||
- "src/main/go/**"
|
||||
- "!src/main/go/net/eagle0/web_functions/name-generator/**"
|
||||
- ".github/workflows/client_presigner.yml"
|
||||
- "src/main/go/net/eagle0/client_download/**"
|
||||
- "src/main/go/net/eagle0/util/**"
|
||||
pull_request:
|
||||
paths:
|
||||
- "src/main/go/**"
|
||||
- "!src/main/go/net/eagle0/web_functions/name-generator/**"
|
||||
- ".github/workflows/client_presigner.yml"
|
||||
- "src/main/go/net/eagle0/client_download/**"
|
||||
- "src/main/go/net/eagle0/util/**"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -19,6 +21,10 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- uses: bazel-contrib/setup-bazel@0.15.0
|
||||
with:
|
||||
bazelisk-cache: true
|
||||
disk-cache: true
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: false
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
name: Installer Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
paths:
|
||||
- ".github/workflows/installer_build.yml"
|
||||
- "src/main/csharp/net/eagle0/clients/win/installer/**"
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/installer_build.yml"
|
||||
- "src/main/csharp/net/eagle0/clients/win/installer/**"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build-installer:
|
||||
runs-on: self-hosted
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: false
|
||||
clean: false
|
||||
|
||||
- name: Setup .NET 8
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore src/main/csharp/net/eagle0/clients/win/installer/EagleInstaller/EagleInstaller.csproj
|
||||
|
||||
- name: Build installer
|
||||
run: dotnet publish src/main/csharp/net/eagle0/clients/win/installer/EagleInstaller/EagleInstaller.csproj -c Release -r win-x64 --self-contained true --output ./installer-output
|
||||
|
||||
- name: Archive installer binary
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: eagle-installer
|
||||
path: ./installer-output/EagleInstaller.exe
|
||||
|
||||
- name: Verify installer exists
|
||||
if: success()
|
||||
run: |
|
||||
if [ ! -f "./installer-output/EagleInstaller.exe" ]; then
|
||||
echo "ERROR: EagleInstaller.exe not found at expected location"
|
||||
echo "Directory contents:"
|
||||
ls -la ./installer-output/
|
||||
exit 1
|
||||
fi
|
||||
echo "Installer found at correct location"
|
||||
|
||||
- name: Deploy installer
|
||||
if: success() && github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
env:
|
||||
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
run: |
|
||||
INSTALLER_PATH="$(pwd)/installer-output/EagleInstaller.exe"
|
||||
echo "Using absolute path: $INSTALLER_PATH"
|
||||
bazel run //src/main/go/net/eagle0/build/installer_build_handler:installer_build_handler -- "$INSTALLER_PATH"
|
||||
|
||||
- name: Update unified manifest
|
||||
if: success() && github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
env:
|
||||
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
run: |
|
||||
# Create installer manifest content
|
||||
INSTALLER_SHA=$(sha256sum ./installer-output/EagleInstaller.exe | cut -d' ' -f1)
|
||||
cat > /tmp/installer_manifest.txt << EOF
|
||||
installer_version=1.0.0
|
||||
installer_url=installer/EagleInstaller.exe
|
||||
installer_sha256=$INSTALLER_SHA
|
||||
EOF
|
||||
|
||||
# Update the unified manifest
|
||||
bazel run //src/main/go/net/eagle0/build/manifest_manager:manifest_manager -- installer /tmp/installer_manifest.txt
|
||||
@@ -3,21 +3,15 @@ name: Mac History Editor Build
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
paths-ignore:
|
||||
- "src/main/cpp/**"
|
||||
- "src/main/scala/**"
|
||||
- "src/main/csharp/**"
|
||||
- "src/test/cpp/**"
|
||||
- "src/test/scala/**"
|
||||
- "src/test/csharp/**"
|
||||
paths:
|
||||
- ".github/workflows/mac_history_build.yml"
|
||||
- "src/main/swift/net/eagle0/EagleGameHistoryViewer/**"
|
||||
- "src/main/protobuf/net/eagle0/eagle/**"
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- "src/main/cpp/**"
|
||||
- "src/main/scala/**"
|
||||
- "src/main/csharp/**"
|
||||
- "src/test/cpp/**"
|
||||
- "src/test/scala/**"
|
||||
- "src/test/csharp/**"
|
||||
paths:
|
||||
- ".github/workflows/mac_history_build.yml"
|
||||
- "src/main/swift/net/eagle0/EagleGameHistoryViewer/**"
|
||||
- "src/main/protobuf/net/eagle0/eagle/**"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -6,10 +6,12 @@ on:
|
||||
paths-ignore:
|
||||
- "src/main/csharp/**"
|
||||
- "src/test/csharp/**"
|
||||
- "src/main/go/**"
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- "src/main/csharp/**"
|
||||
- "src/test/csharp/**"
|
||||
- "src/main/go/**"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -40,6 +40,17 @@ jobs:
|
||||
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
run: bazel run //src/main/go/net/eagle0/build/unity3d_windows_build_handler:unity3d_windows_build_handler -- "/tmp/eagle0/eagle0WIN"
|
||||
|
||||
- name: Update unified manifest
|
||||
env:
|
||||
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
run: |
|
||||
# Download the Unity manifest that was just uploaded
|
||||
wget -O /tmp/unity_manifest.txt https://eagle0-windows.sfo3.digitaloceanspaces.com/unity3d/eagle0WIN_manifest.txt
|
||||
|
||||
# Update the unified manifest
|
||||
bazel run //src/main/go/net/eagle0/build/manifest_manager:manifest_manager -- unity3d /tmp/unity_manifest.txt
|
||||
- name: Archive build log
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Eagle0 is a multi-language gaming system combining strategic turn-based gameplay (Eagle) with tactical hex-based combat (Shardok). The system integrates LLM-based narrative generation and supports both human and AI players.
|
||||
|
||||
## Architecture
|
||||
|
||||
**Three-Tier Game System:**
|
||||
- **Unity Client (C#)**: Real-time strategy game client with integrated tactical combat UI
|
||||
- **Eagle (Scala)**: Strategic layer managing turn-based gameplay, diplomacy, hero progression, and province control
|
||||
- **Shardok (C++)**: Tactical layer handling real-time hex-based combat simulation with performance-critical battle resolution
|
||||
|
||||
**Communication Flow:**
|
||||
```
|
||||
Unity Client ↔ Eagle (gRPC streaming) ↔ Shardok (internal gRPC)
|
||||
```
|
||||
|
||||
**Key Entry Points:**
|
||||
- `/src/main/csharp/net/eagle0/clients/unity/eagle0/` - Unity C# game client
|
||||
- `/src/main/scala/net/eagle0/eagle/Main.scala` - Eagle strategic game server
|
||||
- `/src/main/cpp/net/eagle0/shardok/shardok_server_main.cpp` - Shardok tactical server
|
||||
|
||||
**Protocol Buffer Architecture:**
|
||||
- Extensive use of protobuf for type-safe communication
|
||||
- Separate packages: `api/` (client-facing), `internal/` (server state), `views/` (client projections)
|
||||
- Event sourcing pattern with immutable action history
|
||||
|
||||
## Essential Commands
|
||||
|
||||
### Building
|
||||
```bash
|
||||
# Build Eagle server (Scala strategic layer)
|
||||
bazel build //src/main/scala/net/eagle0/eagle:eagle_server_deploy.jar
|
||||
|
||||
# Build Shardok server (C++ tactical layer)
|
||||
bazel build -c opt //src/main/cpp/net/eagle0/shardok:shardok-server
|
||||
|
||||
# Build Unity/C# client
|
||||
./scripts/build_protos.sh # Protocol buffer generation for Unity
|
||||
./scripts/build_plugins.sh # Native plugins for all platforms
|
||||
./scripts/build_windows_plugin.sh # Windows-specific plugin build
|
||||
# Unity builds via CI: ci/github_actions/build_unity.sh
|
||||
```
|
||||
|
||||
### Running Services
|
||||
```bash
|
||||
# Eagle server (port 40032)
|
||||
bazel run //src/main/scala/net/eagle0/eagle:eagle_server -- --eagle-grpc-port 40032
|
||||
# Or: ./scripts/eagle_run.sh
|
||||
|
||||
# Shardok server
|
||||
bazel run //src/main/cpp/net/eagle0/shardok:shardok-server --compilation_mode=opt
|
||||
# Or: ./scripts/shardok_run.sh
|
||||
```
|
||||
|
||||
### Testing
|
||||
```bash
|
||||
# Run all tests
|
||||
bazel test //src/test/... //src/main/go/...
|
||||
|
||||
# Component-specific tests
|
||||
bazel test //src/test/scala/... # Scala Eagle tests
|
||||
bazel test //src/test/cpp/... # C++ Shardok tests
|
||||
```
|
||||
|
||||
### Code Generation
|
||||
```bash
|
||||
bazel run gazelle # Update Go build files
|
||||
./scripts/updateActionResultTypes.sh # Update protocol buffer mappings
|
||||
```
|
||||
|
||||
## Language-Specific Patterns
|
||||
|
||||
**Scala (Strategic Layer):**
|
||||
- Use `EngineImpl.scala` for core game logic modifications
|
||||
- Follow event sourcing pattern - all changes through immutable actions
|
||||
- gRPC streaming for real-time client updates via `EagleServiceImpl.scala`
|
||||
- LLM integration in `/common/llm_integration/` for narrative generation
|
||||
|
||||
**C++ (Tactical Layer):**
|
||||
- Performance-critical combat in `ShardokEngine.hpp/.cpp`
|
||||
- FlatBuffers for efficient serialization in `/flatbuffer/` directory
|
||||
- AI systems in `/ai/` subdirectory with pluggable strategy selectors
|
||||
- Extensive unit testing with Google Test framework
|
||||
|
||||
**Protocol Buffers:**
|
||||
- Three-layer structure: `api/` (client), `internal/` (server), `views/` (projections)
|
||||
- Use `shardok_internal_interface.proto` for Eagle-Shardok communication
|
||||
- Maintain backward compatibility when modifying existing messages
|
||||
|
||||
**C# (Unity Client):**
|
||||
- Located in `/src/main/csharp/net/eagle0/clients/unity/eagle0/`
|
||||
- Uses Unity 6 (6000.0.32f1) with comprehensive protobuf integration (100+ .proto files)
|
||||
- Key components: `EagleConnection.cs` (gRPC client), `EagleGameController.cs` (main game logic)
|
||||
- Real-time bidirectional streaming with server via `PersistentClientConnection.cs`
|
||||
- Strategic map UI in `Assets/Eagle/`, tactical battle UI in `Assets/Shardok/`
|
||||
- Seamless transition between strategic gameplay and hex-based tactical combat
|
||||
|
||||
**Go (Build Tools):**
|
||||
- Build automation and code generation utilities
|
||||
- AWS S3 integration for deployment artifacts
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
- Comprehensive unit tests for both Scala and C++ components
|
||||
- Integration tests for Eagle-Shardok communication
|
||||
- Map validation tests ensure game content integrity
|
||||
- Use `GameSettings_test_utils.cpp` and `ShardokEngineBasedTestData.cpp` for C++ test helpers
|
||||
|
||||
## Game Content
|
||||
|
||||
**Maps:** `.e0mj` files in `/src/main/resources/net/eagle0/shardok/maps/`
|
||||
**Configuration:** Game parameters in `/src/main/resources/net/eagle0/eagle/game_parameters.json`
|
||||
**Data Files:** TSV format for battalions, heroes, and other game data
|
||||
|
||||
## Deployment
|
||||
|
||||
- Bazel handles multi-language builds and dependencies
|
||||
- CI/CD via GitHub Actions with platform-specific build scripts in `/ci/github_actions/`
|
||||
- Docker containerization available via `ci/eagle_run.Dockerfile`
|
||||
@@ -0,0 +1,16 @@
|
||||
actions:
|
||||
- name: "Test all targets"
|
||||
container_image: "ubuntu-22.04"
|
||||
triggers:
|
||||
push:
|
||||
branches:
|
||||
- "main" # <-- replace "main" with your main branch name
|
||||
pull_request:
|
||||
branches:
|
||||
- "*"
|
||||
steps:
|
||||
- run: "bazel build //src/main/go/net/eagle0/client_download"
|
||||
storage:
|
||||
ttl_seconds: 86400 # 1 day
|
||||
disk:
|
||||
root_directory: "/tmp/buildbuddy"
|
||||
Executable
+30
@@ -0,0 +1,30 @@
|
||||
#!/bin/zsh
|
||||
|
||||
echo "***"
|
||||
echo "*** Moving files to workspace"
|
||||
mv /Users/dancrosby/NewInvokeAI/outputs/images/*.png /Users/dancrosby/Downloads/new_heroes/
|
||||
|
||||
echo "***"
|
||||
echo "*** Renaming files"
|
||||
bazel run src/main/go/net/eagle0/util/hero_generation/pngorganizer /Users/dancrosby/Downloads/new_heroes/
|
||||
|
||||
echo "***"
|
||||
echo "*** Moving files to generated"
|
||||
mv /Users/dancrosby/Downloads/new_heroes/generated/*.png /Users/dancrosby/Documents/headshots/generated
|
||||
|
||||
echo "***"
|
||||
echo "*** Syncing to server"
|
||||
./scripts/sync_headshots.sh
|
||||
|
||||
echo "***"
|
||||
echo "*** Checking which new heroes have images and adjusting TSVs"
|
||||
bazel run //src/main/go/net/eagle0/util/hero_generation/imagechecker -- /Users/dancrosby/CodingProjects/github/eagle0/src/main/resources/net/eagle0/eagle/waiting_headshots_heroes.herodata /Users/dancrosby/CodingProjects/github/eagle0/src/main/resources/net/eagle0/eagle/generated_heroes.tsv /Users/dancrosby/Documents/headshots/
|
||||
|
||||
echo "***"
|
||||
echo "*** Deduplicate names"
|
||||
bazel run //src/main/go/net/eagle0/util/hero_generation/namededuplicator /Users/dancrosby/CodingProjects/github/eagle0/src/main/resources/net/eagle0/eagle/generated_heroes.tsv
|
||||
rm src/main/resources/net/eagle0/eagle/generated_heroes.tsv.backup
|
||||
|
||||
echo "***"
|
||||
echo "*** Generating new SD prompts"
|
||||
bazel run src/main/go/net/eagle0/util/hero_generation/heroformatter ${PWD}/src/main/resources/net/eagle0/eagle/waiting_headshots_heroes.herodata ~/samplelines.txt
|
||||
@@ -60,6 +60,7 @@
|
||||
<Analyzer Include="/Applications/Unity/Hub/Editor/6000.0.32f1/Unity.app/Contents/Tools/Unity.SourceGenerators/Unity.UIToolkit.SourceGenerator.dll" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Assets/Eagle/GeneratedTextListener.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/NewRoundNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/common/CommonExtensions.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Switch/SwitchManagerEditor.cs" />
|
||||
@@ -76,6 +77,7 @@
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Slider/SliderManager.cs" />
|
||||
<Compile Include="Assets/common/GUIUtils/TableRowHoverDetector.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/ResolveAllianceCommandSelector.cs" />
|
||||
<Compile Include="Assets/Eagle/Table Rows/DynamicHeroTextUpdater.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/WithdrewForTruceDetailsNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/util/HeroDropdownController.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/PrisonerReturnedNotificationGenerator.cs" />
|
||||
@@ -122,8 +124,8 @@
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Button/ButtonManagerIcon.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/HandleRiotDoNothingCommandSelector.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/PrisonerExecutedDetailsNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/common/AWSResourceFetcher.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandButtonPanelController.cs" />
|
||||
<Compile Include="Assets/Eagle/Table Rows/DynamicFactionTextUpdater.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Demo/DemoListShadow.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Slider/RangeMinSlider.cs" />
|
||||
<Compile Include="Assets/HoveringTooltipTextProvider.cs" />
|
||||
@@ -226,9 +228,11 @@
|
||||
<Compile Include="Assets/Eagle/PanelPositions.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/NotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Switch/SwitchManager.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/DynamicTextNotification.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/UI Manager/UIManagerToggle.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/ProvinceHeldDetailsNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Bluetooth/DieInfo.cs" />
|
||||
<Compile Include="Assets/common/ResourceFetcher.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/UI Manager/UIManagerAnimatedIcon.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Input Field/CustomInputField.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Context Menu/ContextMenuContent.cs" />
|
||||
|
||||
+15
-4
@@ -2,6 +2,8 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -19,9 +21,13 @@ public class AuthInterceptor : Interceptor {
|
||||
private readonly string headerName = "Authorization";
|
||||
private string headerValue;
|
||||
|
||||
public AuthInterceptor(String name, String password) {
|
||||
public static string HeaderValue(string name, string password) {
|
||||
String toEncode = name + ":" + password;
|
||||
headerValue = "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes(toEncode));
|
||||
return Convert.ToBase64String(Encoding.UTF8.GetBytes(toEncode));
|
||||
}
|
||||
|
||||
public AuthInterceptor(String name, String password) {
|
||||
headerValue = "Basic " + HeaderValue(name, password);
|
||||
}
|
||||
public override TResponse BlockingUnaryCall<TRequest, TResponse>(
|
||||
TRequest request,
|
||||
@@ -98,6 +104,7 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber {
|
||||
int fetchedMaxPlayerCount;
|
||||
private EagleConnection eagleConnection;
|
||||
private PersistentClientConnection _persistentClientConnection;
|
||||
private HttpClient _httpClient;
|
||||
private Thread lobbyUpdatesThread;
|
||||
private bool listen = false;
|
||||
private readonly Object pendingReplyLock = new Object();
|
||||
@@ -171,7 +178,7 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber {
|
||||
fetchedMaxPlayerCount = lobbyResponse.NewGameOptions.MaxSupportedPlayers;
|
||||
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
AWSResourceFetcher.headshotFetcher.Prefetch(
|
||||
ResourceFetcher.headshotFetcher.Prefetch(
|
||||
fetchedNewGameLeaders.Select(a => a.ImagePath));
|
||||
|
||||
connectionPanel.gameObject.SetActive(false);
|
||||
@@ -256,7 +263,11 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber {
|
||||
eagleConnection.credentials,
|
||||
EagleConnection.EagleCancellationToken);
|
||||
_persistentClientConnection.Connect();
|
||||
AWSResourceFetcher.SetUpConnection(_persistentClientConnection);
|
||||
|
||||
_httpClient = new HttpClient();
|
||||
_httpClient.DefaultRequestHeaders.Authorization =
|
||||
new AuthenticationHeaderValue("Basic", eagleConnection.authHeader);
|
||||
ResourceFetcher.SetUpConnection(_httpClient);
|
||||
}
|
||||
|
||||
private void _internalCustomBattle() {
|
||||
|
||||
+2
-2
@@ -135,7 +135,7 @@ public class CustomBattleHandler : MonoBehaviour, IClientConnectionSubscriber {
|
||||
var unit = yourUnitsController.ComponentAt<CustomUnitRowController>(i).Unit;
|
||||
unit.Hero.EagleHeroId = nextEagleHeroId;
|
||||
yourSetupInfo.Units.Add(unit);
|
||||
_heroNames[nextEagleHeroId] = unit.Hero.Name;
|
||||
_heroNames[nextEagleHeroId] = unit.Hero.NameTextId;
|
||||
_heroImages[nextEagleHeroId] = "no_profession/male/000107.1333771077.png";
|
||||
nextEagleHeroId++;
|
||||
}
|
||||
@@ -157,7 +157,7 @@ public class CustomBattleHandler : MonoBehaviour, IClientConnectionSubscriber {
|
||||
var unit = aiUnitsController.ComponentAt<CustomUnitRowController>(i).Unit;
|
||||
unit.Hero.EagleHeroId = nextEagleHeroId;
|
||||
aiSetupInfo.Units.Add(unit);
|
||||
_heroNames[nextEagleHeroId] = unit.Hero.Name;
|
||||
_heroNames[nextEagleHeroId] = unit.Hero.NameTextId;
|
||||
nextEagleHeroId++;
|
||||
}
|
||||
|
||||
|
||||
+45
-10
@@ -13,6 +13,7 @@ namespace eagle {
|
||||
using FactionId = Int32;
|
||||
|
||||
public class ApprehendOutlawCommandSelector : CommandSelector {
|
||||
private List<GeneratedTextListener> outlawListeners = new();
|
||||
private HeroId SelectedHeroId => actingHeroDropdownController.SelectedHeroId;
|
||||
private BattalionId? SelectedBattalionId =>
|
||||
actingBattalionDropdown.value == 0
|
||||
@@ -48,13 +49,22 @@ namespace eagle {
|
||||
.OrderBy(batt => batt.Type)
|
||||
.ToList();
|
||||
|
||||
private string outlawDropdownString(ResidentOutlaw outlaw) {
|
||||
string lastFactionString = "";
|
||||
|
||||
if (outlaw.LastFactionId is FactionId fid) {
|
||||
lastFactionString = $" ({_model.FactionName(fid)})";
|
||||
private void ClearOutlawListeners() {
|
||||
foreach (var listener in outlawListeners) {
|
||||
ClientTextProvider.Provider.RemoveListener(listener);
|
||||
}
|
||||
return $"{outlaw.Hero.Name}{lastFactionString}";
|
||||
outlawListeners.Clear();
|
||||
}
|
||||
|
||||
private void SetOutlawText(int index, string text) {
|
||||
if (index >= targetOutlawDropdown.options.Count) return;
|
||||
targetOutlawDropdown.options[index].text = text;
|
||||
targetOutlawDropdown.RefreshShownValue();
|
||||
}
|
||||
|
||||
private string GetFactionSuffix(ResidentOutlaw outlaw) {
|
||||
if (outlaw.LastFactionId is FactionId fid) { return $" ({_model.FactionName(fid)})"; }
|
||||
return "";
|
||||
}
|
||||
|
||||
private TMP_Dropdown.OptionData BattalionOption(BattalionView bv) {
|
||||
@@ -84,11 +94,34 @@ namespace eagle {
|
||||
actingBattalionDropdown.value = Math.Min(1, AvailableBattalions.Count);
|
||||
|
||||
targetOutlawDropdown.ClearOptions();
|
||||
var outlawNames =
|
||||
ApprehendOutlawCommand.Outlaws.Select(opt => outlawDropdownString(opt))
|
||||
.ToList();
|
||||
|
||||
targetOutlawDropdown.AddOptions(outlawNames);
|
||||
ClearOutlawListeners();
|
||||
|
||||
// Create empty options first, text will be populated by listeners
|
||||
var emptyOptions = ApprehendOutlawCommand.Outlaws
|
||||
.Select(
|
||||
_ => new TMP_Dropdown.OptionData { text = "" })
|
||||
.ToList();
|
||||
targetOutlawDropdown.AddOptions(emptyOptions);
|
||||
|
||||
// Set up listeners for each outlaw's name text
|
||||
for (int i = 0; i < ApprehendOutlawCommand.Outlaws.Count; i++) {
|
||||
var outlaw = ApprehendOutlawCommand.Outlaws[i];
|
||||
var suffix = GetFactionSuffix(outlaw);
|
||||
|
||||
if (!string.IsNullOrEmpty(outlaw.Hero.NameTextId)) {
|
||||
var listener = new GeneratedTextListener(
|
||||
outlaw.Hero.NameTextId,
|
||||
"",
|
||||
suffix,
|
||||
i,
|
||||
SetOutlawText);
|
||||
|
||||
outlawListeners.Add(listener);
|
||||
ClientTextProvider.Provider.AddListener(listener);
|
||||
}
|
||||
}
|
||||
|
||||
targetOutlawDropdown.value = 0;
|
||||
}
|
||||
|
||||
@@ -100,5 +133,7 @@ namespace eagle {
|
||||
ActingHeroId = SelectedHeroId
|
||||
}
|
||||
};
|
||||
|
||||
void OnDestroy() { ClearOutlawListeners(); }
|
||||
}
|
||||
}
|
||||
|
||||
+38
-2
@@ -13,6 +13,8 @@ namespace eagle {
|
||||
using HeroId = Int32;
|
||||
|
||||
public class DefendCommandSelector : CommandSelector {
|
||||
private GeneratedTextListener warningTextListener;
|
||||
private string cachedWarningText = "";
|
||||
private int RallyPointProvinceIndex => rallyPointDropdown.value;
|
||||
private List<CombatUnit> SelectedActingUnits => unitSelector.SelectedActingUnits;
|
||||
|
||||
@@ -72,9 +74,11 @@ namespace eagle {
|
||||
if (BadlyConstructedUnits) {
|
||||
var count = unitSelector.HeroIdsLeadingInappropriateUnits.Count;
|
||||
if (count == 1) {
|
||||
txt += $"{_model.Heroes[unitSelector.HeroIdsLeadingInappropriateUnits[0]].Name} is leading a battalion type that would restrict their abilities.\n\n";
|
||||
txt += !string.IsNullOrEmpty(cachedWarningText)
|
||||
? cachedWarningText
|
||||
: "A hero is leading a battalion type that would restrict their abilities.\n\n";
|
||||
} else {
|
||||
txt += $"Some of your heroes are leading battalion types that would restrict their abilities.\n\n";
|
||||
txt += "Some of your heroes are leading battalion types that would restrict their abilities.\n\n";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,6 +151,36 @@ namespace eagle {
|
||||
troopCountText.text = $"{DefendCommand.HostileTroopCount}";
|
||||
|
||||
SetUpUnitSelector();
|
||||
|
||||
SetupWarningTextListener();
|
||||
}
|
||||
|
||||
private void ClearWarningTextListener() {
|
||||
if (warningTextListener != null) {
|
||||
ClientTextProvider.Provider.RemoveListener(warningTextListener);
|
||||
warningTextListener = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetupWarningTextListener() {
|
||||
ClearWarningTextListener();
|
||||
|
||||
// Only set up if there's exactly one hero with inappropriate units
|
||||
if (unitSelector.HeroIdsLeadingInappropriateUnits.Count == 1) {
|
||||
var heroId = unitSelector.HeroIdsLeadingInappropriateUnits[0];
|
||||
var hero = _model.Heroes[heroId];
|
||||
|
||||
if (!string.IsNullOrEmpty(hero.NameTextId)) {
|
||||
warningTextListener = new GeneratedTextListener(
|
||||
hero.NameTextId,
|
||||
"",
|
||||
" is leading a battalion type that would restrict their abilities.\n\n",
|
||||
0,
|
||||
(index, text) => { cachedWarningText = text; });
|
||||
|
||||
ClientTextProvider.Provider.AddListener(warningTextListener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override SelectedCommand Command {
|
||||
@@ -159,5 +193,7 @@ namespace eagle {
|
||||
return cmd;
|
||||
}
|
||||
}
|
||||
|
||||
void OnDestroy() { ClearWarningTextListener(); }
|
||||
}
|
||||
}
|
||||
|
||||
+33
-2
@@ -11,6 +11,8 @@ namespace eagle {
|
||||
using HeroId = Int32;
|
||||
|
||||
public class HeroGiftCommandSelector : CommandSelector {
|
||||
private GeneratedTextListener warningTextListener;
|
||||
private string cachedWarningText = "";
|
||||
public TMP_Dropdown provinceDropdown;
|
||||
public HeroDropdownController heroDropdownController;
|
||||
public Slider goldSlider;
|
||||
@@ -82,8 +84,7 @@ namespace eagle {
|
||||
}
|
||||
}
|
||||
public override bool WarnOnCommitButton => SelectedHero.Loyalty.Stat >= 100;
|
||||
public override string CommitWarningText =>
|
||||
$"{SelectedHero.Name} already has maximum loyalty!";
|
||||
public override string CommitWarningText => cachedWarningText;
|
||||
|
||||
void Awake() {
|
||||
heroDropdownController.ConditionAccessor = hero => hero.Loyalty;
|
||||
@@ -108,6 +109,28 @@ namespace eagle {
|
||||
goldSlider.maxValue = AvailableGold;
|
||||
}
|
||||
|
||||
private void ClearWarningTextListener() {
|
||||
if (warningTextListener != null) {
|
||||
ClientTextProvider.Provider.RemoveListener(warningTextListener);
|
||||
warningTextListener = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetupWarningTextListener() {
|
||||
ClearWarningTextListener();
|
||||
|
||||
if (!string.IsNullOrEmpty(SelectedHero.NameTextId)) {
|
||||
warningTextListener = new GeneratedTextListener(
|
||||
SelectedHero.NameTextId,
|
||||
"",
|
||||
" already has maximum loyalty!",
|
||||
0,
|
||||
(index, text) => { cachedWarningText = text; });
|
||||
|
||||
ClientTextProvider.Provider.AddListener(warningTextListener);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetUpHeroDropdown() {
|
||||
var heroes = AvailableHeroIds.Select(hid => _model.Heroes[hid]).ToList();
|
||||
|
||||
@@ -120,11 +143,17 @@ namespace eagle {
|
||||
}
|
||||
}
|
||||
heroDropdownController.SelectedHeroId = minLoyaltyHero.Id;
|
||||
|
||||
// Set up warning text listener for the selected hero
|
||||
SetupWarningTextListener();
|
||||
}
|
||||
|
||||
public void HeroSelected() {
|
||||
goldSlider.maxValue = AvailableGold;
|
||||
goldSlider.value = Math.Min(goldSlider.value, AvailableGold);
|
||||
|
||||
// Update warning text listener when hero selection changes
|
||||
SetupWarningTextListener();
|
||||
}
|
||||
|
||||
public void GoldPlusClicked() {
|
||||
@@ -151,5 +180,7 @@ namespace eagle {
|
||||
return cmd;
|
||||
}
|
||||
}
|
||||
|
||||
void OnDestroy() { ClearWarningTextListener(); }
|
||||
}
|
||||
}
|
||||
|
||||
+41
-1
@@ -13,6 +13,7 @@ namespace eagle {
|
||||
using ProvinceId = Int32;
|
||||
|
||||
public class IssueOrdersCommandSelector : CommandSelector {
|
||||
private List<GeneratedTextListener> rulingHeroListeners = new();
|
||||
IList<ProvinceOrderType> availableOrderTypes;
|
||||
Dictionary<ProvinceId, ProvinceOrderType> currentOrders;
|
||||
List<ProvinceOrders> newOrders;
|
||||
@@ -63,7 +64,15 @@ namespace eagle {
|
||||
(SelectedFocusProvince != startingFocusProvince);
|
||||
public override string DisabledCommitButtonReason => "No province orders have changed";
|
||||
|
||||
private void ClearRulingHeroListeners() {
|
||||
foreach (var listener in rulingHeroListeners) {
|
||||
ClientTextProvider.Provider.RemoveListener(listener);
|
||||
}
|
||||
rulingHeroListeners.Clear();
|
||||
}
|
||||
|
||||
override protected void SetUpUI() {
|
||||
ClearRulingHeroListeners();
|
||||
availableOrderTypes = IssueOrdersAvailableCommand.AvailableOrders;
|
||||
currentOrders = IssueOrdersAvailableCommand.CurrentOrders.ToDictionary(
|
||||
x => x.ProvinceId,
|
||||
@@ -101,7 +110,36 @@ namespace eagle {
|
||||
var textComponents = newRow.GetComponentsInChildren<TextMeshProUGUI>();
|
||||
textComponents[0].text = province.Name;
|
||||
textComponents[1].text = GUIUtils.ConditionString(province.FullInfo.Support);
|
||||
textComponents[2].text = _model.Heroes[province.FullInfo.RulingHeroId.Value].Name;
|
||||
|
||||
// Set up ruling hero name with listener
|
||||
var rulingHeroTextComponent = textComponents[2];
|
||||
rulingHeroTextComponent.text = ""; // Start empty, will be populated by listener
|
||||
|
||||
if (province.FullInfo.RulingHeroId.HasValue) {
|
||||
var rulingHero = _model.Heroes[province.FullInfo.RulingHeroId.Value];
|
||||
if (!string.IsNullOrEmpty(rulingHero.NameTextId)) {
|
||||
var listener = new GeneratedTextListener(
|
||||
rulingHero.NameTextId,
|
||||
"",
|
||||
"",
|
||||
i,
|
||||
(index, text) => {
|
||||
if (index < scrollContent.transform.childCount) {
|
||||
var rowTransform = scrollContent.transform.GetChild(index);
|
||||
var rowTextComponents =
|
||||
rowTransform
|
||||
.GetComponentsInChildren<TextMeshProUGUI>();
|
||||
if (rowTextComponents.Length > 2) {
|
||||
rowTextComponents[2].text = text;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
rulingHeroListeners.Add(listener);
|
||||
ClientTextProvider.Provider.AddListener(listener);
|
||||
}
|
||||
}
|
||||
|
||||
textComponents[3].text = province.FullInfo.RulingFactionHeroIds.Count.ToString();
|
||||
|
||||
var ordersDropdown = newRow.GetComponentInChildren<TMP_Dropdown>();
|
||||
@@ -141,5 +179,7 @@ namespace eagle {
|
||||
public void ProvinceOrdersValueChanged(int index, Int32 newValue) {
|
||||
newOrders[index].Orders = availableOrderTypes[newValue];
|
||||
}
|
||||
|
||||
void OnDestroy() { ClearRulingHeroListeners(); }
|
||||
}
|
||||
}
|
||||
|
||||
+37
-1
@@ -22,6 +22,8 @@ namespace eagle {
|
||||
}
|
||||
|
||||
public class MarchCommandSelector : CommandSelector {
|
||||
private GeneratedTextListener warningTextListener;
|
||||
private string cachedWarningText = "";
|
||||
private const string NoProvinceSelectedText = "Select...";
|
||||
|
||||
private bool HasNoneOption => toDropdown.options.Last().text == NoProvinceSelectedText;
|
||||
@@ -138,7 +140,9 @@ namespace eagle {
|
||||
if (BadlyConstructedUnits) {
|
||||
var count = unitSelector.HeroIdsLeadingInappropriateUnits.Count;
|
||||
if (count == 1) {
|
||||
txt += $"{_model.Heroes[unitSelector.HeroIdsLeadingInappropriateUnits[0]].Name} is leading a battalion type that would restrict their abilities\n\n";
|
||||
txt += !string.IsNullOrEmpty(cachedWarningText)
|
||||
? cachedWarningText
|
||||
: "A hero is leading a battalion type that would restrict their abilities\n\n";
|
||||
} else {
|
||||
txt += "Some of your heroes are leading battalion types that would restrict their abilities\n\n";
|
||||
}
|
||||
@@ -320,6 +324,36 @@ namespace eagle {
|
||||
|
||||
SetFoodLabels();
|
||||
SetGoldLabels();
|
||||
|
||||
SetupWarningTextListener();
|
||||
}
|
||||
|
||||
private void ClearWarningTextListener() {
|
||||
if (warningTextListener != null) {
|
||||
ClientTextProvider.Provider.RemoveListener(warningTextListener);
|
||||
warningTextListener = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetupWarningTextListener() {
|
||||
ClearWarningTextListener();
|
||||
|
||||
// Only set up if there's exactly one hero with inappropriate units
|
||||
if (unitSelector.HeroIdsLeadingInappropriateUnits.Count == 1) {
|
||||
var heroId = unitSelector.HeroIdsLeadingInappropriateUnits[0];
|
||||
var hero = _model.Heroes[heroId];
|
||||
|
||||
if (!string.IsNullOrEmpty(hero.NameTextId)) {
|
||||
warningTextListener = new GeneratedTextListener(
|
||||
hero.NameTextId,
|
||||
"",
|
||||
" is leading a battalion type that would restrict their abilities\n\n",
|
||||
0,
|
||||
(index, text) => { cachedWarningText = text; });
|
||||
|
||||
ClientTextProvider.Provider.AddListener(warningTextListener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void GoldMinusClicked() {
|
||||
@@ -381,5 +415,7 @@ namespace eagle {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
void OnDestroy() { ClearWarningTextListener(); }
|
||||
}
|
||||
}
|
||||
|
||||
+65
-10
@@ -8,6 +8,8 @@ namespace eagle {
|
||||
using ProvinceId = Int32;
|
||||
|
||||
public class PleaseRecruitMeCommandSelector : PopupPanelController {
|
||||
private List<GeneratedTextListener> heroNameListeners = new();
|
||||
private List<string> cachedHeroNames = new();
|
||||
struct HeroInfo {
|
||||
public ProvinceId provinceId;
|
||||
public ExpandedUnaffiliatedHero hero;
|
||||
@@ -26,9 +28,11 @@ namespace eagle {
|
||||
set {
|
||||
_availableCommand = value;
|
||||
if (value == null) {
|
||||
ClearHeroNameListeners();
|
||||
_offers = new List<HeroInfo>();
|
||||
PopupInfos = new List<PopupInfo>();
|
||||
} else {
|
||||
ClearHeroNameListeners();
|
||||
_offers = _availableCommand.PleaseRecruitMeCommand.AvailableProvinces
|
||||
.SelectMany(
|
||||
p => p.AvailableHeroes.Select(
|
||||
@@ -38,16 +42,18 @@ namespace eagle {
|
||||
}))
|
||||
.ToList();
|
||||
|
||||
PopupInfos =
|
||||
_offers.Select(heroInfo => new PopupInfo {
|
||||
titleText = "Please Recruit Me!",
|
||||
mainText =
|
||||
$"{heroInfo.hero.Hero.Name} wishes to join us in {Model.Provinces[heroInfo.provinceId].Name}.\n\n",
|
||||
mainTextLlmId = heroInfo.hero.PleaseRecruitMeTextId,
|
||||
affectedProvinceIds =
|
||||
new List<ProvinceId>(heroInfo.provinceId)
|
||||
})
|
||||
.ToList();
|
||||
// Initialize cached hero names and set up listeners
|
||||
cachedHeroNames = new List<string>(new string[_offers.Count]);
|
||||
SetupHeroNameListeners();
|
||||
|
||||
PopupInfos = _offers.Select((heroInfo, index) => new PopupInfo {
|
||||
titleText = "Please Recruit Me!",
|
||||
mainText = GetHeroMainText(index, heroInfo.provinceId),
|
||||
mainTextLlmId = heroInfo.hero.PleaseRecruitMeTextId,
|
||||
affectedProvinceIds =
|
||||
new List<ProvinceId>(heroInfo.provinceId)
|
||||
})
|
||||
.ToList();
|
||||
|
||||
SetUpPanel();
|
||||
}
|
||||
@@ -56,6 +62,53 @@ namespace eagle {
|
||||
|
||||
private List<HeroInfo> _offers = new List<HeroInfo>();
|
||||
|
||||
private void ClearHeroNameListeners() {
|
||||
foreach (var listener in heroNameListeners) {
|
||||
ClientTextProvider.Provider.RemoveListener(listener);
|
||||
}
|
||||
heroNameListeners.Clear();
|
||||
}
|
||||
|
||||
private void SetupHeroNameListeners() {
|
||||
for (int i = 0; i < _offers.Count; i++) {
|
||||
var heroInfo = _offers[i];
|
||||
if (!string.IsNullOrEmpty(heroInfo.hero.Hero.NameTextId)) {
|
||||
var listener = new GeneratedTextListener(
|
||||
heroInfo.hero.Hero.NameTextId,
|
||||
"",
|
||||
"",
|
||||
i,
|
||||
(index, text) => {
|
||||
if (index < cachedHeroNames.Count) {
|
||||
cachedHeroNames[index] = text;
|
||||
UpdatePopupInfoMainText(index);
|
||||
}
|
||||
});
|
||||
|
||||
heroNameListeners.Add(listener);
|
||||
ClientTextProvider.Provider.AddListener(listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string GetHeroMainText(int index, ProvinceId provinceId) {
|
||||
var heroName = !string.IsNullOrEmpty(cachedHeroNames[index]) ? cachedHeroNames[index]
|
||||
: "A hero";
|
||||
return $"{heroName} wishes to join us in {Model.Provinces[provinceId].Name}.\n\n";
|
||||
}
|
||||
|
||||
private void UpdatePopupInfoMainText(int index) {
|
||||
if (index < PopupInfos.Count && index < _offers.Count) {
|
||||
var heroInfo = _offers[index];
|
||||
PopupInfos[index] = new PopupInfo {
|
||||
titleText = "Please Recruit Me!",
|
||||
mainText = GetHeroMainText(index, heroInfo.provinceId),
|
||||
mainTextLlmId = heroInfo.hero.PleaseRecruitMeTextId,
|
||||
affectedProvinceIds = new List<ProvinceId>(heroInfo.provinceId)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private SelectedCommand SelectedCommand(bool accept) {
|
||||
return new SelectedCommand {
|
||||
PleaseRecruitMeCommand =
|
||||
@@ -86,5 +139,7 @@ namespace eagle {
|
||||
heroDetailsController.SetHero(_offers[DisplayedIndex].hero.Hero, Model);
|
||||
}
|
||||
}
|
||||
|
||||
void OnDestroy() { ClearHeroNameListeners(); }
|
||||
}
|
||||
}
|
||||
+107
-5
@@ -14,6 +14,8 @@ namespace eagle {
|
||||
using ProvinceId = Int32;
|
||||
|
||||
public class RansomCommandSelector : CommandSelector {
|
||||
private List<GeneratedTextListener> prisonerDropdownListeners = new();
|
||||
private List<GeneratedTextListener> tableListeners = new();
|
||||
private FactionId SelectedFactionId =>
|
||||
AvailableTargetFactionIds[targetFactionDropdown.value];
|
||||
private PrisonerToBeRansomed SelectedPrisonerToBeRansomed =>
|
||||
@@ -188,9 +190,34 @@ namespace eagle {
|
||||
options.Select(opt => opt.RansomOffer.PrisonerToBeRansomed).ToList();
|
||||
|
||||
prisonerToBeRansomedDropdown.ClearOptions();
|
||||
prisonerToBeRansomedDropdown.AddOptions(
|
||||
AvailablePrisonersToBeRansomed.Select(p => _model.Heroes[p.PrisonerHeroId].Name)
|
||||
.ToList());
|
||||
|
||||
ClearPrisonerDropdownListeners();
|
||||
|
||||
// Create empty options first, text will be populated by listeners
|
||||
var emptyOptions = AvailablePrisonersToBeRansomed
|
||||
.Select(
|
||||
_ => new TMP_Dropdown.OptionData { text = "" })
|
||||
.ToList();
|
||||
prisonerToBeRansomedDropdown.AddOptions(emptyOptions);
|
||||
|
||||
// Set up listeners for each prisoner's name text
|
||||
for (int i = 0; i < AvailablePrisonersToBeRansomed.Count; i++) {
|
||||
var prisoner = AvailablePrisonersToBeRansomed[i];
|
||||
var hero = _model.Heroes[prisoner.PrisonerHeroId];
|
||||
|
||||
if (!string.IsNullOrEmpty(hero.NameTextId)) {
|
||||
var listener = new GeneratedTextListener(
|
||||
hero.NameTextId,
|
||||
"",
|
||||
"",
|
||||
i,
|
||||
SetPrisonerDropdownText);
|
||||
|
||||
prisonerDropdownListeners.Add(listener);
|
||||
ClientTextProvider.Provider.AddListener(listener);
|
||||
}
|
||||
}
|
||||
|
||||
prisonerToBeRansomedDropdown.value = 0;
|
||||
|
||||
AvailableHostages = RansomOffer.HostagesOffered.Select(p => p.HeroId).ToList();
|
||||
@@ -199,21 +226,91 @@ namespace eagle {
|
||||
UpdateTables();
|
||||
}
|
||||
|
||||
private void ClearPrisonerDropdownListeners() {
|
||||
foreach (var listener in prisonerDropdownListeners) {
|
||||
ClientTextProvider.Provider.RemoveListener(listener);
|
||||
}
|
||||
prisonerDropdownListeners.Clear();
|
||||
}
|
||||
|
||||
private void ClearTableListeners() {
|
||||
foreach (var listener in tableListeners) {
|
||||
ClientTextProvider.Provider.RemoveListener(listener);
|
||||
}
|
||||
tableListeners.Clear();
|
||||
}
|
||||
|
||||
private void SetPrisonerDropdownText(int index, string text) {
|
||||
if (index >= prisonerToBeRansomedDropdown.options.Count) return;
|
||||
prisonerToBeRansomedDropdown.options[index].text = text;
|
||||
prisonerToBeRansomedDropdown.RefreshShownValue();
|
||||
}
|
||||
|
||||
private void UpdateTables() {
|
||||
ClearTableListeners();
|
||||
|
||||
prisonersTable.RowCount = 0;
|
||||
int prisonerIndex = 0;
|
||||
foreach (HeroId prisonerHid in PrisonerHeroIds) {
|
||||
var row = prisonersTable.AddRowWithComponent<AvailableHeroTableRow>();
|
||||
row.HeroName = _model.Heroes[prisonerHid].Name;
|
||||
var hero = _model.Heroes[prisonerHid];
|
||||
|
||||
// Start with empty name, will be populated by listener
|
||||
row.HeroName = "";
|
||||
row.Selectable = true;
|
||||
row.Selected = false;
|
||||
|
||||
// Set up listener for this table row
|
||||
if (!string.IsNullOrEmpty(hero.NameTextId)) {
|
||||
var listener = new GeneratedTextListener(
|
||||
hero.NameTextId,
|
||||
"",
|
||||
"",
|
||||
prisonerIndex,
|
||||
(index, text) => {
|
||||
if (index < prisonersTable.RowCount) {
|
||||
var tableRow = prisonersTable.GetComponentInChildren<
|
||||
AvailableHeroTableRow>();
|
||||
if (tableRow != null) tableRow.HeroName = text;
|
||||
}
|
||||
});
|
||||
|
||||
tableListeners.Add(listener);
|
||||
ClientTextProvider.Provider.AddListener(listener);
|
||||
}
|
||||
prisonerIndex++;
|
||||
}
|
||||
|
||||
hostagesTable.RowCount = 0;
|
||||
int hostageIndex = 0;
|
||||
foreach (HeroId hostageHid in HostageHeroIds) {
|
||||
var row = hostagesTable.AddRowWithComponent<AvailableHeroTableRow>();
|
||||
row.HeroName = _model.Heroes[hostageHid].Name;
|
||||
var hero = _model.Heroes[hostageHid];
|
||||
|
||||
// Start with empty name, will be populated by listener
|
||||
row.HeroName = "";
|
||||
row.Selectable = true;
|
||||
row.Selected = false;
|
||||
|
||||
// Set up listener for this table row
|
||||
if (!string.IsNullOrEmpty(hero.NameTextId)) {
|
||||
var listener = new GeneratedTextListener(
|
||||
hero.NameTextId,
|
||||
"",
|
||||
"",
|
||||
hostageIndex,
|
||||
(index, text) => {
|
||||
if (index < hostagesTable.RowCount) {
|
||||
var tableRow = hostagesTable.GetComponentInChildren<
|
||||
AvailableHeroTableRow>();
|
||||
if (tableRow != null) tableRow.HeroName = text;
|
||||
}
|
||||
});
|
||||
|
||||
tableListeners.Add(listener);
|
||||
ClientTextProvider.Provider.AddListener(listener);
|
||||
}
|
||||
hostageIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,5 +323,10 @@ namespace eagle {
|
||||
PrisonerHeroIds.RemoveAt(index);
|
||||
UpdateTables();
|
||||
}
|
||||
|
||||
void OnDestroy() {
|
||||
ClearPrisonerDropdownListeners();
|
||||
ClearTableListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+31
-2
@@ -5,13 +5,40 @@ using Net.Eagle0.Eagle.Common;
|
||||
|
||||
namespace eagle {
|
||||
public class ResolveAllianceCommandSelector : ResolveDiplomacyCommandSelector {
|
||||
private GeneratedTextListener messengerTextListener;
|
||||
private string cachedPrefixText = "";
|
||||
protected override IList<DiplomacyOffer> Offers =>
|
||||
_availableCommand?.ResolveAllianceOfferCommand?.Offers.ToList() ??
|
||||
new List<DiplomacyOffer>();
|
||||
|
||||
protected override string PrefixedText(DiplomacyOffer offer) {
|
||||
var messengerName = Model.Heroes[MessengerHeroId].Name;
|
||||
return $"{Model.FactionName(OriginatingFactionId)}'s representative {messengerName}:\n";
|
||||
SetupMessengerTextListener();
|
||||
return !string.IsNullOrEmpty(cachedPrefixText)
|
||||
? cachedPrefixText
|
||||
: $"{Model.FactionName(OriginatingFactionId)}'s representative:\n";
|
||||
}
|
||||
|
||||
private void ClearMessengerTextListener() {
|
||||
if (messengerTextListener != null) {
|
||||
ClientTextProvider.Provider.RemoveListener(messengerTextListener);
|
||||
messengerTextListener = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetupMessengerTextListener() {
|
||||
if (messengerTextListener != null) return; // Already set up
|
||||
|
||||
var messengerHero = Model.Heroes[MessengerHeroId];
|
||||
if (!string.IsNullOrEmpty(messengerHero.NameTextId)) {
|
||||
messengerTextListener = new GeneratedTextListener(
|
||||
messengerHero.NameTextId,
|
||||
$"{Model.FactionName(OriginatingFactionId)}'s representative ",
|
||||
":\n",
|
||||
0,
|
||||
(index, text) => { cachedPrefixText = text; });
|
||||
|
||||
ClientTextProvider.Provider.AddListener(messengerTextListener);
|
||||
}
|
||||
}
|
||||
protected override string TitleText => "Alliance Proposed";
|
||||
|
||||
@@ -22,5 +49,7 @@ namespace eagle {
|
||||
OriginatingFactionId = OriginatingFactionId
|
||||
}
|
||||
};
|
||||
|
||||
void OnDestroy() { ClearMessengerTextListener(); }
|
||||
}
|
||||
}
|
||||
+31
-2
@@ -5,13 +5,40 @@ using Net.Eagle0.Eagle.Common;
|
||||
|
||||
namespace eagle {
|
||||
public class ResolveBreakAllianceCommandSelector : ResolveDiplomacyCommandSelector {
|
||||
private GeneratedTextListener messengerTextListener;
|
||||
private string cachedPrefixText = "";
|
||||
protected override IList<DiplomacyOffer> Offers =>
|
||||
_availableCommand?.ResolveBreakAllianceCommand?.Offers.ToList() ??
|
||||
new List<DiplomacyOffer>();
|
||||
|
||||
protected override string PrefixedText(DiplomacyOffer offer) {
|
||||
var messengerName = Model.Heroes[MessengerHeroId].Name;
|
||||
return $"{Model.FactionName(OriginatingFactionId)}'s representative {messengerName}:\n";
|
||||
SetupMessengerTextListener();
|
||||
return !string.IsNullOrEmpty(cachedPrefixText)
|
||||
? cachedPrefixText
|
||||
: $"{Model.FactionName(OriginatingFactionId)}'s representative:\n";
|
||||
}
|
||||
|
||||
private void ClearMessengerTextListener() {
|
||||
if (messengerTextListener != null) {
|
||||
ClientTextProvider.Provider.RemoveListener(messengerTextListener);
|
||||
messengerTextListener = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetupMessengerTextListener() {
|
||||
if (messengerTextListener != null) return; // Already set up
|
||||
|
||||
var messengerHero = Model.Heroes[MessengerHeroId];
|
||||
if (!string.IsNullOrEmpty(messengerHero.NameTextId)) {
|
||||
messengerTextListener = new GeneratedTextListener(
|
||||
messengerHero.NameTextId,
|
||||
$"{Model.FactionName(OriginatingFactionId)}'s representative ",
|
||||
":\n",
|
||||
0,
|
||||
(index, text) => { cachedPrefixText = text; });
|
||||
|
||||
ClientTextProvider.Provider.AddListener(messengerTextListener);
|
||||
}
|
||||
}
|
||||
protected override string TitleText => "Alliance Broken";
|
||||
|
||||
@@ -22,5 +49,7 @@ namespace eagle {
|
||||
OriginatingFactionId = OriginatingFactionId
|
||||
}
|
||||
};
|
||||
|
||||
void OnDestroy() { ClearMessengerTextListener(); }
|
||||
}
|
||||
}
|
||||
+33
-2
@@ -6,12 +6,41 @@ using Net.Eagle0.Eagle.Common;
|
||||
namespace eagle {
|
||||
|
||||
public class ResolveInvitationCommandSelector : ResolveDiplomacyCommandSelector {
|
||||
private GeneratedTextListener messengerTextListener;
|
||||
private string cachedPrefixText = "";
|
||||
protected override IList<DiplomacyOffer> Offers =>
|
||||
_availableCommand?.ResolveInvitationCommand?.Invitations.ToList() ??
|
||||
new List<DiplomacyOffer>();
|
||||
|
||||
protected override string PrefixedText(DiplomacyOffer offer) =>
|
||||
$"{Model.Heroes[MessengerHeroId].Name} brings an invitation to join {Model.FactionName(OriginatingFactionId)}.\n";
|
||||
protected override string PrefixedText(DiplomacyOffer offer) {
|
||||
SetupMessengerTextListener();
|
||||
return !string.IsNullOrEmpty(cachedPrefixText)
|
||||
? cachedPrefixText
|
||||
: $"A messenger brings an invitation to join {Model.FactionName(OriginatingFactionId)}.\n";
|
||||
}
|
||||
|
||||
private void ClearMessengerTextListener() {
|
||||
if (messengerTextListener != null) {
|
||||
ClientTextProvider.Provider.RemoveListener(messengerTextListener);
|
||||
messengerTextListener = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetupMessengerTextListener() {
|
||||
if (messengerTextListener != null) return; // Already set up
|
||||
|
||||
var messengerHero = Model.Heroes[MessengerHeroId];
|
||||
if (!string.IsNullOrEmpty(messengerHero.NameTextId)) {
|
||||
messengerTextListener = new GeneratedTextListener(
|
||||
messengerHero.NameTextId,
|
||||
"",
|
||||
$" brings an invitation to join {Model.FactionName(OriginatingFactionId)}.\n",
|
||||
0,
|
||||
(index, text) => { cachedPrefixText = text; });
|
||||
|
||||
ClientTextProvider.Provider.AddListener(messengerTextListener);
|
||||
}
|
||||
}
|
||||
protected override string TitleText => "Invited";
|
||||
|
||||
public override SelectedCommand selectedCommand(DiplomacyOfferStatus resolution) =>
|
||||
@@ -22,5 +51,7 @@ namespace eagle {
|
||||
OriginatingFactionId = OriginatingFactionId
|
||||
}
|
||||
};
|
||||
|
||||
void OnDestroy() { ClearMessengerTextListener(); }
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -65,7 +65,7 @@ namespace eagle {
|
||||
var demandingFaction = _model.MaybeDestroyedFaction(SelectedDemand.DemandingFactionId);
|
||||
|
||||
factionDescription.text =
|
||||
$"{DisplayNames.FactionString(demandingFaction, _model)} demands tribute!";
|
||||
$"{DisplayNames.ResolvedFactionString(demandingFaction, _model)} demands tribute!";
|
||||
|
||||
goldAmountLabel.text = SelectedDemand.TributeDemanded.Gold.ToString();
|
||||
foodAmountLabel.text = SelectedDemand.TributeDemanded.Food.ToString();
|
||||
|
||||
+31
-2
@@ -5,13 +5,40 @@ using Net.Eagle0.Eagle.Common;
|
||||
|
||||
namespace eagle {
|
||||
public class ResolveTruceCommandSelector : ResolveDiplomacyCommandSelector {
|
||||
private GeneratedTextListener messengerTextListener;
|
||||
private string cachedPrefixText = "";
|
||||
protected override IList<DiplomacyOffer> Offers =>
|
||||
_availableCommand?.ResolveTruceOfferCommand?.Offers.ToList() ??
|
||||
new List<DiplomacyOffer>();
|
||||
|
||||
protected override string PrefixedText(DiplomacyOffer offer) {
|
||||
var messengerName = Model.Heroes[MessengerHeroId].Name;
|
||||
return $"{Model.FactionName(OriginatingFactionId)}'s representative {messengerName}:\n";
|
||||
SetupMessengerTextListener();
|
||||
return !string.IsNullOrEmpty(cachedPrefixText)
|
||||
? cachedPrefixText
|
||||
: $"{Model.FactionName(OriginatingFactionId)}'s representative:\n";
|
||||
}
|
||||
|
||||
private void ClearMessengerTextListener() {
|
||||
if (messengerTextListener != null) {
|
||||
ClientTextProvider.Provider.RemoveListener(messengerTextListener);
|
||||
messengerTextListener = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetupMessengerTextListener() {
|
||||
if (messengerTextListener != null) return; // Already set up
|
||||
|
||||
var messengerHero = Model.Heroes[MessengerHeroId];
|
||||
if (!string.IsNullOrEmpty(messengerHero.NameTextId)) {
|
||||
messengerTextListener = new GeneratedTextListener(
|
||||
messengerHero.NameTextId,
|
||||
$"{Model.FactionName(OriginatingFactionId)}'s representative ",
|
||||
":\n",
|
||||
0,
|
||||
(index, text) => { cachedPrefixText = text; });
|
||||
|
||||
ClientTextProvider.Provider.AddListener(messengerTextListener);
|
||||
}
|
||||
}
|
||||
|
||||
protected override string TitleText => "Truce Proposed";
|
||||
@@ -23,5 +50,7 @@ namespace eagle {
|
||||
OriginatingFactionId = OriginatingFactionId
|
||||
}
|
||||
};
|
||||
|
||||
void OnDestroy() { ClearMessengerTextListener(); }
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -118,7 +118,7 @@ namespace eagle {
|
||||
private void SetHeadshot() {
|
||||
var imagePath = AvailableHeroes[Dropdown.value].ImagePath;
|
||||
if (!string.IsNullOrEmpty(imagePath)) {
|
||||
AWSResourceFetcher.headshotFetcher.LoadIntoRawImage(HeadshotImage, imagePath);
|
||||
ResourceFetcher.headshotFetcher.LoadIntoRawImage(HeadshotImage, imagePath);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Net.Eagle0.Eagle.Api.Command.Util;
|
||||
using Net.Eagle0.Eagle.Common;
|
||||
using Net.Eagle0.Eagle.Views;
|
||||
@@ -79,128 +78,6 @@ namespace eagle {
|
||||
}
|
||||
}
|
||||
|
||||
public static string ShortQuestString(Quest quest, IGameModel model) {
|
||||
if (quest == null) { return $"There is nothing you can do"; }
|
||||
|
||||
switch (quest.Details.SealedValueCase) {
|
||||
case SealedValueOneofCase.AllianceQuest:
|
||||
return $"Secure an alliance with another faction";
|
||||
|
||||
case SealedValueOneofCase.DefeatFactionQuest:
|
||||
return $"Defeat {model.FactionName(quest.Details.DefeatFactionQuest.TargetFactionId)}";
|
||||
|
||||
case SealedValueOneofCase.AlmsToProvinceQuest: {
|
||||
var province = model.Provinces[quest.Details.AlmsToProvinceQuest.ProvinceId];
|
||||
var desiredFood = quest.Details.AlmsToProvinceQuest.TotalFood;
|
||||
var remainingFood = Math.Max(0, desiredFood - quest.ComponentsFulfilled);
|
||||
var remainingText =
|
||||
quest.ComponentsFulfilled > 0 ? $" ({remainingFood} remaining)" : "";
|
||||
return $"Give {desiredFood} food in alms in {province.Name}{remainingText}";
|
||||
}
|
||||
|
||||
case SealedValueOneofCase.AlmsAcrossRealmQuest: {
|
||||
var desiredFood = quest.Details.AlmsAcrossRealmQuest.TotalFood;
|
||||
var remainingFood = Math.Max(0, desiredFood - quest.ComponentsFulfilled);
|
||||
var remainingText =
|
||||
quest.ComponentsFulfilled > 0 ? $" ({remainingFood} remaining)" : "";
|
||||
return $"Give {desiredFood} food in alms across the realm{remainingText}";
|
||||
}
|
||||
|
||||
case SealedValueOneofCase.GiveToHeroesInProvinceQuest: {
|
||||
var province =
|
||||
model.Provinces[quest.Details.GiveToHeroesInProvinceQuest.ProvinceId];
|
||||
var desiredGold = quest.Details.GiveToHeroesInProvinceQuest.TotalGold;
|
||||
var remainingGold = Math.Max(0, desiredGold - quest.ComponentsFulfilled);
|
||||
var remainingText =
|
||||
quest.ComponentsFulfilled > 0 ? $" ({remainingGold} remaining)" : "";
|
||||
return $"Give {desiredGold} gold to heroes in {province.Name}{remainingText}";
|
||||
}
|
||||
|
||||
case SealedValueOneofCase.GiveToHeroesAcrossRealmQuest: {
|
||||
var desiredGold = quest.Details.GiveToHeroesAcrossRealmQuest.TotalGold;
|
||||
var remainingGold = Math.Max(0, desiredGold - quest.ComponentsFulfilled);
|
||||
var remainingText =
|
||||
quest.ComponentsFulfilled > 0 ? $" ({remainingGold} remaining)" : "";
|
||||
return $"Give {desiredGold} gold to heroes across the realm{remainingText}";
|
||||
}
|
||||
|
||||
case SealedValueOneofCase.ImproveAgricultureQuest:
|
||||
return $"Raise Agriculture to {(int)Math.Ceiling(quest.Details.ImproveAgricultureQuest.DesiredValue)}";
|
||||
|
||||
case SealedValueOneofCase.ImproveEconomyQuest:
|
||||
return $"Raise Economy to {(int)Math.Ceiling(quest.Details.ImproveEconomyQuest.DesiredValue)}";
|
||||
|
||||
case SealedValueOneofCase.ImproveInfrastructureQuest:
|
||||
return $"Raise Infrastructure to {(int)Math.Ceiling(quest.Details.ImproveInfrastructureQuest.DesiredValue)}";
|
||||
|
||||
case SealedValueOneofCase.SpecificExpansionQuest:
|
||||
return $"Expand to {model.Provinces[quest.Details.SpecificExpansionQuest.ProvinceId].Name}";
|
||||
|
||||
case SealedValueOneofCase.DismissSpecificVassalQuest:
|
||||
return $"Dismiss {model.Heroes[quest.Details.DismissSpecificVassalQuest.TargetHeroId].Name}";
|
||||
|
||||
case SealedValueOneofCase.WealthQuest:
|
||||
return $"Acquire {quest.Details.WealthQuest.Gold} gold and {quest.Details.WealthQuest.Food} food";
|
||||
|
||||
case SealedValueOneofCase.UpgradeBattalionQuest:
|
||||
return $"Raise a {LongBattalionTypeNames[quest.Details.UpgradeBattalionQuest.BattalionTypeId]} to {(int)Math.Ceiling(quest.Details.UpgradeBattalionQuest.MinimumTraining)} training and {(int)Math.Ceiling(quest.Details.UpgradeBattalionQuest.MinimumArmament)} armament";
|
||||
|
||||
case SealedValueOneofCase.GrandArmyQuest:
|
||||
return $"Have {quest.Details.GrandArmyQuest.TotalTroopCount} troops";
|
||||
|
||||
case SealedValueOneofCase.TruceWithFactionQuest:
|
||||
return $"Sign a truce with {model.FactionName(quest.Details.TruceWithFactionQuest.TargetFactionId)}";
|
||||
|
||||
case SealedValueOneofCase.TruceCountQuest: {
|
||||
var currentTruceString = "";
|
||||
if (model.PlayerId is int fid) {
|
||||
var currentTruces = model.ActiveFactions[fid].FactionRelationships.Count(
|
||||
fr => fr.RelationshipLevel !=
|
||||
FactionRelationshipView.Types.RelationshipLevel.Hostile);
|
||||
if (currentTruces == 0) {
|
||||
currentTruceString = " (currently have none)";
|
||||
} else {
|
||||
currentTruceString = " (currently have " + currentTruces + ")";
|
||||
}
|
||||
}
|
||||
return $"Have truces with {quest.Details.TruceCountQuest.TruceCount} factions" +
|
||||
currentTruceString;
|
||||
}
|
||||
|
||||
case SealedValueOneofCase.ExecutePrisonerQuest: {
|
||||
var details = quest.Details.ExecutePrisonerQuest;
|
||||
var prisonerName = model.Heroes[details.PrisonerHeroId].Name;
|
||||
var provinceName = model.Provinces[details.ProvinceId].Name;
|
||||
return $"Execute prisoner {prisonerName} in {provinceName}";
|
||||
}
|
||||
|
||||
case SealedValueOneofCase.ExilePrisonerQuest: {
|
||||
var details = quest.Details.ExilePrisonerQuest;
|
||||
var prisonerName = model.Heroes[details.PrisonerHeroId].Name;
|
||||
var provinceName = model.Provinces[details.ProvinceId].Name;
|
||||
return $"Exile prisoner {prisonerName} from {provinceName}";
|
||||
}
|
||||
|
||||
case SealedValueOneofCase.FreePrisonerQuest: {
|
||||
var details = quest.Details.FreePrisonerQuest;
|
||||
var prisonerName = model.Heroes[details.PrisonerHeroId].Name;
|
||||
var provinceName = model.Provinces[details.ProvinceId].Name;
|
||||
return $"Release {prisonerName} from prison in {provinceName}";
|
||||
}
|
||||
|
||||
case SealedValueOneofCase.ReturnPrisonerQuest: {
|
||||
var details = quest.Details.ReturnPrisonerQuest;
|
||||
var prisonerName = model.Heroes[details.PrisonerHeroId].Name;
|
||||
var provinceName = model.Provinces[details.ProvinceId].Name;
|
||||
var factionName = model.FactionName(details.ToFactionId);
|
||||
return $"Return prisoner {prisonerName} from {provinceName} to {factionName}";
|
||||
}
|
||||
|
||||
default:
|
||||
return $"Crosby hasn't gotten to quest of type {quest.Details.SealedValueCase} yet";
|
||||
}
|
||||
}
|
||||
|
||||
public static string RecruitmentStatusString(string name, RecruitmentInfo recruitmentInfo) {
|
||||
switch (recruitmentInfo.Status) {
|
||||
case RecruitmentStatus.LowPrestige:
|
||||
@@ -290,7 +167,7 @@ namespace eagle {
|
||||
return new DateTime(777, month, 1).ToString("MMMM", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
public static string MonthString(Date date) {
|
||||
private static string MonthString(Date date) {
|
||||
return new DateTime(date.Year, date.Month, 1)
|
||||
.ToString("MMMM", CultureInfo.InvariantCulture);
|
||||
}
|
||||
@@ -308,7 +185,7 @@ namespace eagle {
|
||||
case UnaffiliatedHeroType.UnaffiliatedHeroOutlaw: return "Outlaw";
|
||||
|
||||
case UnaffiliatedHeroType.UnaffiliatedHeroUnknown:
|
||||
default: throw new ArgumentOutOfRangeException("Unknown free hero status");
|
||||
default: throw new ArgumentException("Unknown free hero status");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,7 +200,7 @@ namespace eagle {
|
||||
case ControlWeatherType.ControlWeatherEndDrought: return "End Drought";
|
||||
|
||||
case ControlWeatherType.ControlWeatherUnknown:
|
||||
default: throw new ArgumentOutOfRangeException("Unknown control weather type");
|
||||
default: throw new ArgumentException("Unknown control weather type");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -337,14 +214,16 @@ namespace eagle {
|
||||
}
|
||||
}
|
||||
|
||||
public static string
|
||||
MaybeFactionLeaderDescription(FactionView faction, HeroView hero, IGameModel currentModel) {
|
||||
public static string MaybeFactionLeaderDescriptionTemplate(
|
||||
FactionView faction,
|
||||
HeroView hero,
|
||||
IGameModel currentModel) {
|
||||
if (faction.FactionHeadId == hero.Id) {
|
||||
return $"faction leader {hero.Name}";
|
||||
return "faction leader {HeroName}";
|
||||
} else if (faction.Leaders.Contains(hero.Id)) {
|
||||
return $"{FactionString(faction, currentModel)}'s sworn {SiblingDescription(hero.PronounGender)} {hero.Name}";
|
||||
return $"{FactionStringTemplate(faction, currentModel)}'s sworn {SiblingDescription(hero.PronounGender)} {{HeroName}}";
|
||||
} else {
|
||||
return $"{FactionString(faction, currentModel)}'s hero {hero.Name}";
|
||||
return $"{FactionStringTemplate(faction, currentModel)}'s hero {{HeroName}}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,7 +232,7 @@ namespace eagle {
|
||||
else if (str.Length == 1)
|
||||
return str.ToUpper();
|
||||
else
|
||||
return char.ToUpper(str[0]) + str.Substring(1);
|
||||
return char.ToUpper(str[0]) + str[1..];
|
||||
}
|
||||
|
||||
public static string ListString(IList<String> strings) {
|
||||
@@ -364,7 +243,7 @@ namespace eagle {
|
||||
} else {
|
||||
string str = "";
|
||||
for (int i = 0; i < strings.Count - 1; i++) { str += strings[i] + ", "; }
|
||||
str += "and " + strings[strings.Count - 1];
|
||||
str += "and " + strings[^1];
|
||||
|
||||
return str;
|
||||
}
|
||||
@@ -383,18 +262,36 @@ namespace eagle {
|
||||
return ListString(list);
|
||||
}
|
||||
|
||||
private static string
|
||||
ConditionalFactionString(FactionView faction, IGameModel gameModel, bool useFactionName) {
|
||||
private static string ConditionalFactionStringTemplate(
|
||||
FactionView faction,
|
||||
IGameModel gameModel,
|
||||
bool useFactionName) {
|
||||
if (useFactionName) return faction.Name;
|
||||
return gameModel.Heroes[faction.FactionHeadId].Name;
|
||||
return "{FactionLeaderName}";
|
||||
}
|
||||
|
||||
public static string FactionString(FactionView faction, IGameModel gameModel) {
|
||||
return ConditionalFactionString(faction, gameModel, DisplayFactionName);
|
||||
public static string FactionStringTemplate(FactionView faction, IGameModel gameModel) {
|
||||
return ConditionalFactionStringTemplate(faction, gameModel, DisplayFactionName);
|
||||
}
|
||||
|
||||
public static string AlternateFactionString(FactionView faction, IGameModel gameModel) {
|
||||
return ConditionalFactionString(faction, gameModel, !DisplayFactionName);
|
||||
public static string AlternateFactionStringTemplate(
|
||||
FactionView faction,
|
||||
IGameModel gameModel) {
|
||||
return ConditionalFactionStringTemplate(faction, gameModel, !DisplayFactionName);
|
||||
}
|
||||
|
||||
public static string ResolvedFactionString(FactionView faction, IGameModel gameModel) {
|
||||
var template = FactionStringTemplate(faction, gameModel);
|
||||
if (template.Contains("{FactionLeaderName}")) {
|
||||
var hero = gameModel.Heroes[faction.FactionHeadId];
|
||||
var textEntry = ClientTextProvider.Provider.GetTextEntry(hero.NameTextId);
|
||||
if (textEntry != null && !string.IsNullOrEmpty(textEntry.Text)) {
|
||||
return template.Replace("{FactionLeaderName}", textEntry.Text);
|
||||
} else {
|
||||
return template.Replace("{FactionLeaderName}", "Leader");
|
||||
}
|
||||
}
|
||||
return template;
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
-5
@@ -77,6 +77,8 @@ namespace eagle {
|
||||
|
||||
GameModelUpdater ModelUpdater { get; set; }
|
||||
IGameModel Model { get; set; }
|
||||
|
||||
private DynamicFactionTextUpdater factionTextUpdater = new DynamicFactionTextUpdater();
|
||||
private IGameModel _newModel = null;
|
||||
|
||||
public Canvas shardokCanvas;
|
||||
@@ -334,7 +336,7 @@ namespace eagle {
|
||||
}
|
||||
|
||||
private void PrefetchHeadshotForHeroes(List<HeroView> heroViews) {
|
||||
AWSResourceFetcher.headshotFetcher.Prefetch(heroViews.Select(h => h.ImagePath));
|
||||
ResourceFetcher.headshotFetcher.Prefetch(heroViews.Select(h => h.ImagePath));
|
||||
}
|
||||
|
||||
private void PrefetchHeadshots(IGameModel model) {
|
||||
@@ -410,13 +412,18 @@ namespace eagle {
|
||||
void SetHeaderString() {
|
||||
var date = Model.CurrentDate;
|
||||
if (date != null) {
|
||||
string factionLabel = "Observer";
|
||||
string dateString = DisplayNames.DateString(date);
|
||||
if (Model.PlayerId.HasValue) {
|
||||
var faction = Model.MaybeDestroyedFaction(Model.PlayerId.Value);
|
||||
factionLabel =
|
||||
$"{DisplayNames.FactionString(faction, Model)} ({faction.Prestige})";
|
||||
factionTextUpdater.SetFactionTextWithCustomFormat(
|
||||
roundStatusLabel,
|
||||
faction,
|
||||
Model,
|
||||
$"{dateString} - {{FactionLeaderName}} ({{Prestige}})",
|
||||
$"{dateString} - Observer");
|
||||
} else {
|
||||
roundStatusLabel.text = $"{dateString} - Observer";
|
||||
}
|
||||
roundStatusLabel.text = $"{DisplayNames.DateString(date)} - {factionLabel}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -646,5 +653,7 @@ namespace eagle {
|
||||
var textField = gameIdButton.GetComponentInChildren<Text>();
|
||||
textField.text = textField.text == "" ? ModelUpdater.GameId.ToString("X") : "";
|
||||
}
|
||||
|
||||
private void OnDestroy() { factionTextUpdater?.Dispose(); }
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ using Net.Eagle0.Eagle.Api;
|
||||
using Net.Eagle0.Eagle.Common;
|
||||
using Net.Eagle0.Eagle.Views;
|
||||
using Net.Eagle0.Shardok.Common;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityGoDiceInterface;
|
||||
using Logger = common.Logger;
|
||||
@@ -88,7 +89,7 @@ namespace eagle {
|
||||
notification.DisplayedHeroes);
|
||||
}
|
||||
|
||||
private readonly AWSResourceFetcher _awsResourceFetcher;
|
||||
private readonly ResourceFetcher resourceFetcher;
|
||||
|
||||
public long GameId { get; }
|
||||
|
||||
@@ -176,7 +177,17 @@ namespace eagle {
|
||||
return DestroyedFactions[factionId];
|
||||
}
|
||||
public string FactionName(FactionId factionId) {
|
||||
return DisplayNames.FactionString(MaybeDestroyedFaction(factionId), this);
|
||||
return DisplayNames.ResolvedFactionString(MaybeDestroyedFaction(factionId), this);
|
||||
}
|
||||
|
||||
public void SetDynamicFactionText(
|
||||
TMP_Text textComponent,
|
||||
FactionId factionId,
|
||||
DynamicFactionTextUpdater textUpdater,
|
||||
string template = null,
|
||||
string fallbackText = null) {
|
||||
var faction = MaybeDestroyedFaction(factionId);
|
||||
textUpdater.SetFactionText(textComponent, faction, this, template, fallbackText);
|
||||
}
|
||||
|
||||
public RollFetcher RollFetcher { get; set; }
|
||||
@@ -235,7 +246,10 @@ namespace eagle {
|
||||
locationName: _currentModel.Provinces[defenderProvince].Name,
|
||||
month: _currentModel.CurrentDate.Month,
|
||||
players: players,
|
||||
heroNames: _currentModel.Heroes.ToDictionary(kv => kv.Key, kv => kv.Value.Name),
|
||||
heroNames: _currentModel.Heroes.ToDictionary(
|
||||
kv => kv.Key,
|
||||
kv => ClientTextProvider.Provider.GetTextEntry(kv.Value.NameTextId)
|
||||
.Text),
|
||||
heroImages: _currentModel.Heroes.ToDictionary(
|
||||
kv => kv.Key,
|
||||
kv => kv.Value.ImagePath),
|
||||
|
||||
+81
-8
@@ -11,11 +11,14 @@ using FactionId = System.Int32;
|
||||
using ProvinceId = System.Int32;
|
||||
|
||||
namespace eagle {
|
||||
public class FactionsTableController : MonoBehaviour {
|
||||
public class FactionsTableController : MonoBehaviour, System.IDisposable {
|
||||
public GameObject panel;
|
||||
public GameObject factionPopup;
|
||||
public TMP_Text factionPopupText;
|
||||
|
||||
private DynamicHeroTextUpdater textUpdater = new DynamicHeroTextUpdater();
|
||||
private GeneratedTextListener currentListener;
|
||||
|
||||
private ProvinceView _currentProvince;
|
||||
public ProvinceView CurrentProvince {
|
||||
get => _currentProvince;
|
||||
@@ -69,14 +72,47 @@ namespace eagle {
|
||||
|
||||
private string ColoredFactionString(FactionId fid) {
|
||||
var color = Color.Lerp(PlayerColors.LightPlayerColor(fid), Color.black, 0.5f);
|
||||
return GUIUtils.ColoredString(color, Model.FactionName(fid));
|
||||
var factionName = GetResolvedFactionName(fid);
|
||||
return GUIUtils.ColoredString(color, factionName);
|
||||
}
|
||||
|
||||
private string GetResolvedFactionName(FactionId fid) {
|
||||
var faction = Model.MaybeDestroyedFaction(fid);
|
||||
var template = DisplayNames.FactionStringTemplate(faction, Model);
|
||||
|
||||
if (template.Contains("{FactionLeaderName}")) {
|
||||
var hero = Model.Heroes[faction.FactionHeadId];
|
||||
var textEntry = ClientTextProvider.Provider.GetTextEntry(hero.NameTextId);
|
||||
if (textEntry != null && !string.IsNullOrEmpty(textEntry.Text)) {
|
||||
return template.Replace("{FactionLeaderName}", textEntry.Text);
|
||||
} else {
|
||||
return template.Replace("{FactionLeaderName}", "Leader");
|
||||
}
|
||||
}
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
private string GetResolvedAlternateFactionName(FactionView faction) {
|
||||
var template = DisplayNames.AlternateFactionStringTemplate(faction, Model);
|
||||
|
||||
if (template.Contains("{FactionLeaderName}")) {
|
||||
var hero = Model.Heroes[faction.FactionHeadId];
|
||||
var textEntry = ClientTextProvider.Provider.GetTextEntry(hero.NameTextId);
|
||||
if (textEntry != null && !string.IsNullOrEmpty(textEntry.Text)) {
|
||||
return template.Replace("{FactionLeaderName}", textEntry.Text);
|
||||
} else {
|
||||
return template.Replace("{FactionLeaderName}", "Leader");
|
||||
}
|
||||
}
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
private void LongHoveredFactionChanged(FactionId? newFid) {
|
||||
if (newFid is {} fid) {
|
||||
var faction = Model.MaybeDestroyedFaction(fid);
|
||||
var text =
|
||||
$"{ColoredFactionString(fid)} is led by {Model.Heroes[faction.FactionHeadId].Name}.";
|
||||
var baseText = $"{ColoredFactionString(fid)} is led by ";
|
||||
|
||||
string frText = "";
|
||||
foreach (var fr in faction.FactionRelationships) {
|
||||
@@ -95,12 +131,39 @@ namespace eagle {
|
||||
}
|
||||
}
|
||||
|
||||
if (frText != "") { text += "\n" + frText; }
|
||||
var fullText = baseText + "{FactionLeader}.";
|
||||
if (frText != "") { fullText += "\n" + frText; }
|
||||
|
||||
factionPopupText.text = text;
|
||||
if (currentListener != null) {
|
||||
ClientTextProvider.Provider.RemoveListener(currentListener);
|
||||
}
|
||||
|
||||
var hero = Model.Heroes[faction.FactionHeadId];
|
||||
var textEntry = ClientTextProvider.Provider.GetTextEntry(hero.NameTextId);
|
||||
if (textEntry != null && !string.IsNullOrEmpty(textEntry.Text)) {
|
||||
factionPopupText.text = fullText.Replace("{FactionLeader}", textEntry.Text);
|
||||
} else {
|
||||
factionPopupText.text = fullText.Replace("{FactionLeader}", "Leader");
|
||||
|
||||
currentListener = new GeneratedTextListener(
|
||||
hero.NameTextId,
|
||||
baseText,
|
||||
".",
|
||||
0,
|
||||
(index, text) => {
|
||||
var updatedText = baseText + text + ".";
|
||||
if (frText != "") { updatedText += "\n" + frText; }
|
||||
factionPopupText.text = updatedText;
|
||||
});
|
||||
ClientTextProvider.Provider.AddListener(currentListener);
|
||||
}
|
||||
|
||||
factionPopup.SetActive(true);
|
||||
} else {
|
||||
if (currentListener != null) {
|
||||
ClientTextProvider.Provider.RemoveListener(currentListener);
|
||||
currentListener = null;
|
||||
}
|
||||
factionPopup.SetActive(false);
|
||||
}
|
||||
}
|
||||
@@ -160,8 +223,8 @@ namespace eagle {
|
||||
}
|
||||
|
||||
private void SetUpFactionRow(FactionView faction, FactionTableRow row) {
|
||||
row.Faction = DisplayNames.FactionString(faction, Model);
|
||||
row.Tooltip = DisplayNames.AlternateFactionString(faction, Model);
|
||||
row.Faction = GetResolvedFactionName(faction.Id);
|
||||
row.Tooltip = GetResolvedAlternateFactionName(faction);
|
||||
row.FactionColor = PlayerColors.LightPlayerColor(faction.Id);
|
||||
row.ProvinceCount = ProvinceCount(faction);
|
||||
|
||||
@@ -268,5 +331,15 @@ namespace eagle {
|
||||
.ThenBy(fac => ExistingRelationship(fac)?.ResetDate?.Year ?? 0)
|
||||
.ThenBy(fac => ExistingRelationship(fac)?.ResetDate?.Month ?? 0)
|
||||
.ToList();
|
||||
|
||||
public void Dispose() {
|
||||
if (currentListener != null) {
|
||||
ClientTextProvider.Provider.RemoveListener(currentListener);
|
||||
currentListener = null;
|
||||
}
|
||||
textUpdater?.Dispose();
|
||||
}
|
||||
|
||||
private void OnDestroy() { Dispose(); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
|
||||
namespace eagle {
|
||||
public class GeneratedTextListener : IClientTextListener {
|
||||
private readonly string textId;
|
||||
private readonly string prefix;
|
||||
private readonly string suffix;
|
||||
private readonly int index;
|
||||
private readonly Action<int, string> onTextUpdate;
|
||||
|
||||
public void OnTextUpdate(string text, bool completed) {
|
||||
onTextUpdate(index, $"{prefix}{text}{suffix}");
|
||||
}
|
||||
|
||||
public string TextId() { return textId; }
|
||||
|
||||
public GeneratedTextListener(
|
||||
string textId,
|
||||
string prefix,
|
||||
string suffix,
|
||||
int index,
|
||||
Action<int, string> callback) {
|
||||
this.textId = textId;
|
||||
this.prefix = prefix;
|
||||
this.suffix = suffix;
|
||||
this.index = index;
|
||||
this.onTextUpdate = callback;
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7ed95fc26230d428fa783e0be9eb353d
|
||||
+1
-1
@@ -34,7 +34,7 @@ namespace eagle {
|
||||
|
||||
private void SetHeroBasics(HeroView value) {
|
||||
if (value.ImagePath is String imagePath && !string.IsNullOrEmpty(imagePath)) {
|
||||
AWSResourceFetcher.headshotFetcher.LoadIntoRawImage(headshotImage, value.ImagePath);
|
||||
ResourceFetcher.headshotFetcher.LoadIntoRawImage(headshotImage, value.ImagePath);
|
||||
}
|
||||
professionImage.texture =
|
||||
gameObject.GetComponentInParent<EagleCommonTextures>().Profession(
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ namespace eagle {
|
||||
for (int i = 0; i < movingArmy.UnitDetails.Count; i++) {
|
||||
var details = movingArmy.UnitDetails[i];
|
||||
var row = movingArmyPopupTable.ComponentAt<MovingArmyPopupRowController>(i);
|
||||
row.HeroName.text = Model.Heroes[details.HeroId].Name;
|
||||
row.SetHeroText(Model.Heroes[details.HeroId].NameTextId, "Hero");
|
||||
|
||||
var battalion = details.Battalion;
|
||||
if (battalion != null) {
|
||||
|
||||
+16
-9
@@ -27,27 +27,34 @@ namespace eagle.Notifications.ARNNotifications {
|
||||
currentModel.MaybeDestroyedFaction(imprisoningFactionId).FactionHeadId;
|
||||
var imprisoningFactionHead = currentModel.Heroes[imprisoningFactionHeadId];
|
||||
|
||||
var heroPlaceholders = new Dictionary<string, (string nameTextId, string fallback)> {
|
||||
{ "ImprisonedAmbassador", (imprisonedHero.NameTextId, "the ambassador") }
|
||||
};
|
||||
|
||||
if (playerId.HasValue && playerId.Value == imprisoningFactionId) {
|
||||
yield return Notification.StreamingNotification(
|
||||
yield return DynamicTextNotification.StreamingDynamicNotification(
|
||||
title: "Ambassador Imprisoned",
|
||||
prefixedText:
|
||||
$"You have rejected an alliance with {currentModel.FactionName(offeringFactionId)} and imprisoned their ambassador {imprisonedHero.Name}.\n\n",
|
||||
textTemplate:
|
||||
$"You have rejected an alliance with {currentModel.FactionName(offeringFactionId)} and imprisoned their ambassador {{ImprisonedAmbassador}}.\n\n",
|
||||
heroPlaceholders: heroPlaceholders,
|
||||
llmId: notification.LlmId,
|
||||
provinceIds: affectedProvinces,
|
||||
displayedHeroes: new List<HeroView> { offeringFactionHead, imprisonedHero});
|
||||
} else if (playerId.HasValue && playerId.Value == offeringFactionId) {
|
||||
yield return Notification.StreamingNotification(
|
||||
yield return DynamicTextNotification.StreamingDynamicNotification(
|
||||
title: "Ambassador Imprisoned",
|
||||
prefixedText:
|
||||
$"{currentModel.FactionName(imprisoningFactionId)} has imprisoned our ambassador {imprisonedHero.Name}!\n\n",
|
||||
textTemplate:
|
||||
$"{currentModel.FactionName(imprisoningFactionId)} has imprisoned our ambassador {{ImprisonedAmbassador}}!\n\n",
|
||||
heroPlaceholders: heroPlaceholders,
|
||||
llmId: notification.LlmId,
|
||||
provinceIds: affectedProvinces,
|
||||
displayedHeroes: new List<HeroView> { imprisoningFactionHead, imprisonedHero});
|
||||
} else {
|
||||
yield return Notification.StreamingNotification(
|
||||
yield return DynamicTextNotification.StreamingDynamicNotification(
|
||||
title: "Ambassador Imprisoned",
|
||||
prefixedText:
|
||||
$"{currentModel.FactionName(imprisoningFactionId)} has rejected an alliance from {currentModel.FactionName(offeringFactionId)} and imprisoned their ambassador {imprisonedHero.Name}.\n\n",
|
||||
textTemplate:
|
||||
$"{currentModel.FactionName(imprisoningFactionId)} has rejected an alliance from {currentModel.FactionName(offeringFactionId)} and imprisoned their ambassador {{ImprisonedAmbassador}}.\n\n",
|
||||
heroPlaceholders: heroPlaceholders,
|
||||
llmId: notification.LlmId,
|
||||
provinceIds: affectedProvinces,
|
||||
displayedHeroes: new List<HeroView> { imprisoningFactionHead, offeringFactionHead, imprisonedHero });
|
||||
|
||||
+16
-9
@@ -27,27 +27,34 @@ namespace eagle.Notifications.ARNNotifications {
|
||||
currentModel.MaybeDestroyedFaction(imprisoningFactionId).FactionHeadId;
|
||||
var imprisoningFactionHead = currentModel.Heroes[imprisoningFactionHeadId];
|
||||
|
||||
var heroPlaceholders = new Dictionary<string, (string nameTextId, string fallback)> {
|
||||
{ "ImprisonedAmbassador", (imprisonedHero.NameTextId, "the ambassador") }
|
||||
};
|
||||
|
||||
if (playerId.HasValue && playerId.Value == imprisoningFactionId) {
|
||||
yield return Notification.StreamingNotification(
|
||||
yield return DynamicTextNotification.StreamingDynamicNotification(
|
||||
title: "Ambassador Imprisoned",
|
||||
prefixedText:
|
||||
$"{currentModel.FactionName(offeringFactionId)} broke their alliance with us, and we and imprisoned their ambassador {imprisonedHero.Name}.\n\n",
|
||||
textTemplate:
|
||||
$"{currentModel.FactionName(offeringFactionId)} broke their alliance with us, and we and imprisoned their ambassador {{ImprisonedAmbassador}}.\n\n",
|
||||
heroPlaceholders: heroPlaceholders,
|
||||
llmId: notification.LlmId,
|
||||
provinceIds: affectedProvinces,
|
||||
displayedHeroes: new List<HeroView> { offeringFactionHead, imprisonedHero});
|
||||
} else if (playerId.HasValue && playerId.Value == offeringFactionId) {
|
||||
yield return Notification.StreamingNotification(
|
||||
yield return DynamicTextNotification.StreamingDynamicNotification(
|
||||
title: "Ambassador Imprisoned",
|
||||
prefixedText:
|
||||
$"{currentModel.FactionName(imprisoningFactionId)} has imprisoned our ambassador {imprisonedHero.Name} in response to us breaking the alliance!\n\n",
|
||||
textTemplate:
|
||||
$"{currentModel.FactionName(imprisoningFactionId)} has imprisoned our ambassador {{ImprisonedAmbassador}} in response to us breaking the alliance!\n\n",
|
||||
heroPlaceholders: heroPlaceholders,
|
||||
llmId: notification.LlmId,
|
||||
provinceIds: affectedProvinces,
|
||||
displayedHeroes: new List<HeroView> { imprisoningFactionHead, imprisonedHero});
|
||||
} else {
|
||||
yield return Notification.StreamingNotification(
|
||||
yield return DynamicTextNotification.StreamingDynamicNotification(
|
||||
title: "Ambassador Imprisoned",
|
||||
prefixedText:
|
||||
$"{currentModel.FactionName(offeringFactionId)} broke their alliance with {currentModel.FactionName(imprisoningFactionId)}, and {currentModel.FactionName(imprisoningFactionId)} imprisoned their ambassador {imprisonedHero.Name}.\n\n",
|
||||
textTemplate:
|
||||
$"{currentModel.FactionName(offeringFactionId)} broke their alliance with {currentModel.FactionName(imprisoningFactionId)}, and {currentModel.FactionName(imprisoningFactionId)} imprisoned their ambassador {{ImprisonedAmbassador}}.\n\n",
|
||||
heroPlaceholders: heroPlaceholders,
|
||||
llmId: notification.LlmId,
|
||||
provinceIds: affectedProvinces,
|
||||
displayedHeroes: new List<HeroView> { imprisoningFactionHead, offeringFactionHead, imprisonedHero });
|
||||
|
||||
+38
-11
@@ -24,29 +24,56 @@ namespace eagle.Notifications.ARNNotifications {
|
||||
fid && fid == currentModel.PlayerId;
|
||||
|
||||
string noteTitle = "Hero Executed";
|
||||
string noteText;
|
||||
string textTemplate;
|
||||
|
||||
HeroView firstHero = currentModel.Heroes[actingFaction.FactionHeadId];
|
||||
HeroView secondHero = executedHero;
|
||||
|
||||
var heroPlaceholders = new Dictionary<string, (string nameTextId, string fallback)>();
|
||||
|
||||
if (isMyHero) {
|
||||
if (executedHero.IsFactionLeader) {
|
||||
noteText =
|
||||
$"{DisplayNames.FactionString(actingFaction, currentModel)} has executed our sworn {DisplayNames.SiblingDescription(executedHero.PronounGender)} {executedHero.Name} in {province.Name}! They must pay for this atrocity!\n\n";
|
||||
textTemplate =
|
||||
$"{DisplayNames.ResolvedFactionString(actingFaction, currentModel)} has executed our sworn {DisplayNames.SiblingDescription(executedHero.PronounGender)} {{ExecutedHero}} in {province.Name}! They must pay for this atrocity!\n\n";
|
||||
heroPlaceholders["ExecutedHero"] = (executedHero.NameTextId, "our leader");
|
||||
} else {
|
||||
noteText =
|
||||
$"{DisplayNames.FactionString(actingFaction, currentModel)} has executed our loyal vassal, {executedHero.Name} in {province.Name}!\n\n";
|
||||
textTemplate =
|
||||
$"{DisplayNames.ResolvedFactionString(actingFaction, currentModel)} has executed our loyal vassal, {{ExecutedHero}} in {province.Name}!\n\n";
|
||||
heroPlaceholders["ExecutedHero"] = (executedHero.NameTextId, "our vassal");
|
||||
}
|
||||
} else if (isMyAction) {
|
||||
noteText =
|
||||
$"We executed {DisplayNames.MaybeFactionLeaderDescription(victimFaction, executedHero, currentModel)} in {province.Name}.\n\n";
|
||||
string victimDescription;
|
||||
if (victimFaction.FactionHeadId == executedHero.Id) {
|
||||
victimDescription = "faction leader {HeroName}";
|
||||
} else if (victimFaction.Leaders.Contains(executedHero.Id)) {
|
||||
victimDescription =
|
||||
$"{DisplayNames.ResolvedFactionString(victimFaction, currentModel)}'s sworn {DisplayNames.SiblingDescription(executedHero.PronounGender)} {{HeroName}}";
|
||||
} else {
|
||||
victimDescription =
|
||||
$"{DisplayNames.ResolvedFactionString(victimFaction, currentModel)}'s hero {{HeroName}}";
|
||||
}
|
||||
textTemplate = $"We executed {victimDescription} in {province.Name}.\n\n";
|
||||
heroPlaceholders["HeroName"] = (executedHero.NameTextId, "the hero");
|
||||
} else {
|
||||
noteText =
|
||||
$"{DisplayNames.FactionString(actingFaction, currentModel)} executed {DisplayNames.MaybeFactionLeaderDescription(victimFaction, executedHero, currentModel)} in {province.Name}.\n\n";
|
||||
string victimDescription;
|
||||
if (victimFaction.FactionHeadId == executedHero.Id) {
|
||||
victimDescription = "faction leader {HeroName}";
|
||||
} else if (victimFaction.Leaders.Contains(executedHero.Id)) {
|
||||
victimDescription =
|
||||
$"{DisplayNames.ResolvedFactionString(victimFaction, currentModel)}'s sworn {DisplayNames.SiblingDescription(executedHero.PronounGender)} {{HeroName}}";
|
||||
} else {
|
||||
victimDescription =
|
||||
$"{DisplayNames.ResolvedFactionString(victimFaction, currentModel)}'s hero {{HeroName}}";
|
||||
}
|
||||
textTemplate =
|
||||
$"{DisplayNames.ResolvedFactionString(actingFaction, currentModel)} executed {victimDescription} in {province.Name}.\n\n";
|
||||
heroPlaceholders["HeroName"] = (executedHero.NameTextId, "the hero");
|
||||
}
|
||||
|
||||
yield return Notification.StreamingNotification(
|
||||
yield return DynamicTextNotification.StreamingDynamicNotification(
|
||||
title: noteTitle,
|
||||
prefixedText: noteText,
|
||||
textTemplate: textTemplate,
|
||||
heroPlaceholders: heroPlaceholders,
|
||||
llmId: notification.LlmId,
|
||||
provinceIds: new List<ProvinceId> { details.ProvinceId },
|
||||
displayedHeroes: new List<HeroView> { firstHero, secondHero });
|
||||
|
||||
+37
-10
@@ -25,29 +25,56 @@ namespace eagle.Notifications.ARNNotifications {
|
||||
fid && fid == currentModel.PlayerId;
|
||||
|
||||
string noteTitle = "Hero Exiled";
|
||||
string noteText = "";
|
||||
string textTemplate;
|
||||
|
||||
HeroView firstHero = currentModel.Heroes[actingFaction.FactionHeadId];
|
||||
HeroView secondHero = exiledHero;
|
||||
|
||||
var heroPlaceholders = new Dictionary<string, (string nameTextId, string fallback)>();
|
||||
|
||||
if (isMyHero) {
|
||||
if (exiledHero.IsFactionLeader) {
|
||||
noteText =
|
||||
$"{DisplayNames.FactionString(actingFaction, currentModel)} has exiled our sworn {DisplayNames.SiblingDescription(exiledHero.PronounGender)} {exiledHero.Name} from {province.Name}!\n\n";
|
||||
textTemplate =
|
||||
$"{{ActingFactionLeader}} has exiled our sworn {DisplayNames.SiblingDescription(exiledHero.PronounGender)} {{ExiledHero}} from {province.Name}!\n\n";
|
||||
heroPlaceholders["ExiledHero"] = (exiledHero.NameTextId, "our leader");
|
||||
} else {
|
||||
noteText =
|
||||
$"{DisplayNames.FactionString(actingFaction, currentModel)} has exiled our loyal vassal, {exiledHero.Name} from {province.Name}!\n\n";
|
||||
textTemplate =
|
||||
$"{{ActingFactionLeader}} has exiled our loyal vassal, {{ExiledHero}} from {province.Name}!\n\n";
|
||||
heroPlaceholders["ExiledHero"] = (exiledHero.NameTextId, "our vassal");
|
||||
}
|
||||
heroPlaceholders["ActingFactionLeader"] =
|
||||
(currentModel.Heroes[actingFaction.FactionHeadId].NameTextId,
|
||||
actingFaction.Name);
|
||||
} else if (isMyAction) {
|
||||
noteText = $"We exiled {exiledHero.Name} from {province.Name}.\n\n";
|
||||
textTemplate = $"We exiled {{ExiledHero}} from {province.Name}.\n\n";
|
||||
heroPlaceholders["ExiledHero"] = (exiledHero.NameTextId, "the hero");
|
||||
} else {
|
||||
noteText =
|
||||
$"{DisplayNames.FactionString(actingFaction, currentModel)} has exiled {DisplayNames.MaybeFactionLeaderDescription(victimFaction, exiledHero, currentModel)} from {province.Name}.\n\n";
|
||||
string victimDescription;
|
||||
if (victimFaction.FactionHeadId == exiledHero.Id) {
|
||||
victimDescription = "faction leader {HeroName}";
|
||||
} else if (victimFaction.Leaders.Contains(exiledHero.Id)) {
|
||||
victimDescription =
|
||||
$"{{VictimFactionLeader}}'s sworn {DisplayNames.SiblingDescription(exiledHero.PronounGender)} {{HeroName}}";
|
||||
} else {
|
||||
victimDescription = $"{{VictimFactionLeader}}'s hero {{HeroName}}";
|
||||
}
|
||||
textTemplate =
|
||||
$"{{ActingFactionLeader}} has exiled {victimDescription} from {province.Name}.\n\n";
|
||||
heroPlaceholders["HeroName"] = (exiledHero.NameTextId, "the hero");
|
||||
heroPlaceholders["ActingFactionLeader"] =
|
||||
(currentModel.Heroes[actingFaction.FactionHeadId].NameTextId,
|
||||
actingFaction.Name);
|
||||
if (!victimFaction.FactionHeadId.Equals(exiledHero.Id)) {
|
||||
heroPlaceholders["VictimFactionLeader"] =
|
||||
(currentModel.Heroes[victimFaction.FactionHeadId].NameTextId,
|
||||
victimFaction.Name);
|
||||
}
|
||||
}
|
||||
|
||||
yield return Notification.StreamingNotification(
|
||||
yield return DynamicTextNotification.StreamingDynamicNotification(
|
||||
title: noteTitle,
|
||||
prefixedText: noteText,
|
||||
textTemplate: textTemplate,
|
||||
heroPlaceholders: heroPlaceholders,
|
||||
llmId: notification.LlmId,
|
||||
provinceIds: new List<ProvinceId> { details.ProvinceId },
|
||||
displayedHeroes: new List<HeroView> { firstHero, secondHero });
|
||||
|
||||
+51
-11
@@ -24,30 +24,70 @@ namespace eagle.Notifications.ARNNotifications {
|
||||
fid && fid == currentModel.PlayerId;
|
||||
|
||||
string noteTitle = "Hero Imprisoned";
|
||||
string noteText;
|
||||
string textTemplate;
|
||||
|
||||
HeroView firstHero = currentModel.Heroes[actingFaction.FactionHeadId];
|
||||
HeroView secondHero = imprisonedHero;
|
||||
|
||||
var heroPlaceholders = new Dictionary<string, (string nameTextId, string fallback)>();
|
||||
|
||||
if (isMyHero) {
|
||||
if (imprisonedHero.IsFactionLeader) {
|
||||
noteText =
|
||||
$"{DisplayNames.FactionString(actingFaction, currentModel)} has imprisoned our sworn {DisplayNames.SiblingDescription(imprisonedHero.PronounGender)} {imprisonedHero.Name} in {province.Name}!\n\n";
|
||||
textTemplate =
|
||||
$"{{ActingFactionLeader}} has imprisoned our sworn {DisplayNames.SiblingDescription(imprisonedHero.PronounGender)} {{ImprisonedHero}} in {province.Name}!\n\n";
|
||||
heroPlaceholders["ImprisonedHero"] = (imprisonedHero.NameTextId, "our leader");
|
||||
} else {
|
||||
noteText =
|
||||
$"{DisplayNames.FactionString(actingFaction, currentModel)} has imprisoned our loyal vassal, {imprisonedHero.Name} in {province.Name}!\n\n";
|
||||
textTemplate =
|
||||
$"{{ActingFactionLeader}} has imprisoned our loyal vassal, {{ImprisonedHero}} in {province.Name}!\n\n";
|
||||
heroPlaceholders["ImprisonedHero"] = (imprisonedHero.NameTextId, "our vassal");
|
||||
}
|
||||
heroPlaceholders["ActingFactionLeader"] =
|
||||
(currentModel.Heroes[actingFaction.FactionHeadId].NameTextId,
|
||||
actingFaction.Name);
|
||||
} else if (isMyAction) {
|
||||
noteText =
|
||||
$"We imprisoned {DisplayNames.MaybeFactionLeaderDescription(victimFaction, imprisonedHero, currentModel)} in {province.Name}.\n\n";
|
||||
string victimDescription;
|
||||
if (victimFaction.FactionHeadId == imprisonedHero.Id) {
|
||||
victimDescription = "faction leader {HeroName}";
|
||||
} else if (victimFaction.Leaders.Contains(imprisonedHero.Id)) {
|
||||
victimDescription =
|
||||
$"{{VictimFactionLeader}}'s sworn {DisplayNames.SiblingDescription(imprisonedHero.PronounGender)} {{HeroName}}";
|
||||
} else {
|
||||
victimDescription = $"{{VictimFactionLeader}}'s hero {{HeroName}}";
|
||||
}
|
||||
textTemplate = $"We imprisoned {victimDescription} in {province.Name}.\n\n";
|
||||
heroPlaceholders["HeroName"] = (imprisonedHero.NameTextId, "the hero");
|
||||
if (!victimFaction.FactionHeadId.Equals(imprisonedHero.Id)) {
|
||||
heroPlaceholders["VictimFactionLeader"] =
|
||||
(currentModel.Heroes[victimFaction.FactionHeadId].NameTextId,
|
||||
victimFaction.Name);
|
||||
}
|
||||
} else {
|
||||
noteText =
|
||||
$"{DisplayNames.FactionString(actingFaction, currentModel)} has imprisoned {DisplayNames.MaybeFactionLeaderDescription(victimFaction, imprisonedHero, currentModel)} in {province.Name}.\n\n";
|
||||
string victimDescription;
|
||||
if (victimFaction.FactionHeadId == imprisonedHero.Id) {
|
||||
victimDescription = "faction leader {HeroName}";
|
||||
} else if (victimFaction.Leaders.Contains(imprisonedHero.Id)) {
|
||||
victimDescription =
|
||||
$"{{VictimFactionLeader}}'s sworn {DisplayNames.SiblingDescription(imprisonedHero.PronounGender)} {{HeroName}}";
|
||||
} else {
|
||||
victimDescription = $"{{VictimFactionLeader}}'s hero {{HeroName}}";
|
||||
}
|
||||
textTemplate =
|
||||
$"{{ActingFactionLeader}} has imprisoned {victimDescription} in {province.Name}.\n\n";
|
||||
heroPlaceholders["HeroName"] = (imprisonedHero.NameTextId, "the hero");
|
||||
heroPlaceholders["ActingFactionLeader"] =
|
||||
(currentModel.Heroes[actingFaction.FactionHeadId].NameTextId,
|
||||
actingFaction.Name);
|
||||
if (!victimFaction.FactionHeadId.Equals(imprisonedHero.Id)) {
|
||||
heroPlaceholders["VictimFactionLeader"] =
|
||||
(currentModel.Heroes[victimFaction.FactionHeadId].NameTextId,
|
||||
victimFaction.Name);
|
||||
}
|
||||
}
|
||||
|
||||
yield return Notification.StreamingNotification(
|
||||
yield return DynamicTextNotification.StreamingDynamicNotification(
|
||||
title: noteTitle,
|
||||
prefixedText: noteText,
|
||||
textTemplate: textTemplate,
|
||||
heroPlaceholders: heroPlaceholders,
|
||||
llmId: notification.LlmId,
|
||||
provinceIds: new List<ProvinceId> { details.ProvinceId },
|
||||
displayedHeroes: new List<HeroView> { firstHero, secondHero });
|
||||
|
||||
+51
-11
@@ -21,30 +21,70 @@ namespace eagle.Notifications.ARNNotifications {
|
||||
fid && fid == currentModel.PlayerId;
|
||||
|
||||
string noteTitle = "Hero Returned";
|
||||
string noteText = "";
|
||||
string textTemplate;
|
||||
|
||||
HeroView firstHero = currentModel.Heroes[actingFaction.FactionHeadId];
|
||||
HeroView secondHero = imprisonedHero;
|
||||
|
||||
var heroPlaceholders = new Dictionary<string, (string nameTextId, string fallback)>();
|
||||
|
||||
if (isMyHero) {
|
||||
if (imprisonedHero.IsFactionLeader) {
|
||||
noteText =
|
||||
$"{DisplayNames.FactionString(actingFaction, currentModel)} has peacefully returned our sworn {DisplayNames.SiblingDescription(imprisonedHero.PronounGender)} {imprisonedHero.Name} from {province.Name}!";
|
||||
textTemplate =
|
||||
$"{{ActingFactionLeader}} has peacefully returned our sworn {DisplayNames.SiblingDescription(imprisonedHero.PronounGender)} {{ReturnedHero}} from {province.Name}!";
|
||||
heroPlaceholders["ReturnedHero"] = (imprisonedHero.NameTextId, "our leader");
|
||||
} else {
|
||||
noteText =
|
||||
$"{DisplayNames.FactionString(actingFaction, currentModel)} has peacefully returned our loyal vassal, {imprisonedHero.Name} from {province.Name}!";
|
||||
textTemplate =
|
||||
$"{{ActingFactionLeader}} has peacefully returned our loyal vassal, {{ReturnedHero}} from {province.Name}!";
|
||||
heroPlaceholders["ReturnedHero"] = (imprisonedHero.NameTextId, "our vassal");
|
||||
}
|
||||
heroPlaceholders["ActingFactionLeader"] =
|
||||
(currentModel.Heroes[actingFaction.FactionHeadId].NameTextId,
|
||||
actingFaction.Name);
|
||||
} else if (isMyAction) {
|
||||
noteText =
|
||||
$"We returned {DisplayNames.MaybeFactionLeaderDescription(victimFaction, imprisonedHero, currentModel)} from {province.Name}.";
|
||||
string victimDescription;
|
||||
if (victimFaction.FactionHeadId == imprisonedHero.Id) {
|
||||
victimDescription = "faction leader {HeroName}";
|
||||
} else if (victimFaction.Leaders.Contains(imprisonedHero.Id)) {
|
||||
victimDescription =
|
||||
$"{{VictimFactionLeader}}'s sworn {DisplayNames.SiblingDescription(imprisonedHero.PronounGender)} {{HeroName}}";
|
||||
} else {
|
||||
victimDescription = $"{{VictimFactionLeader}}'s hero {{HeroName}}";
|
||||
}
|
||||
textTemplate = $"We returned {victimDescription} from {province.Name}.";
|
||||
heroPlaceholders["HeroName"] = (imprisonedHero.NameTextId, "the hero");
|
||||
if (!victimFaction.FactionHeadId.Equals(imprisonedHero.Id)) {
|
||||
heroPlaceholders["VictimFactionLeader"] =
|
||||
(currentModel.Heroes[victimFaction.FactionHeadId].NameTextId,
|
||||
victimFaction.Name);
|
||||
}
|
||||
} else {
|
||||
noteText =
|
||||
$"{DisplayNames.FactionString(actingFaction, currentModel)} has peacefully returned {DisplayNames.MaybeFactionLeaderDescription(victimFaction, imprisonedHero, currentModel)} from {province.Name}.";
|
||||
string victimDescription;
|
||||
if (victimFaction.FactionHeadId == imprisonedHero.Id) {
|
||||
victimDescription = "faction leader {HeroName}";
|
||||
} else if (victimFaction.Leaders.Contains(imprisonedHero.Id)) {
|
||||
victimDescription =
|
||||
$"{{VictimFactionLeader}}'s sworn {DisplayNames.SiblingDescription(imprisonedHero.PronounGender)} {{HeroName}}";
|
||||
} else {
|
||||
victimDescription = $"{{VictimFactionLeader}}'s hero {{HeroName}}";
|
||||
}
|
||||
textTemplate =
|
||||
$"{{ActingFactionLeader}} has peacefully returned {victimDescription} from {province.Name}.";
|
||||
heroPlaceholders["HeroName"] = (imprisonedHero.NameTextId, "the hero");
|
||||
heroPlaceholders["ActingFactionLeader"] =
|
||||
(currentModel.Heroes[actingFaction.FactionHeadId].NameTextId,
|
||||
actingFaction.Name);
|
||||
if (!victimFaction.FactionHeadId.Equals(imprisonedHero.Id)) {
|
||||
heroPlaceholders["VictimFactionLeader"] =
|
||||
(currentModel.Heroes[victimFaction.FactionHeadId].NameTextId,
|
||||
victimFaction.Name);
|
||||
}
|
||||
}
|
||||
|
||||
yield return new Notification(
|
||||
yield return new DynamicTextNotification(
|
||||
title: noteTitle,
|
||||
text: noteText,
|
||||
textTemplate: textTemplate,
|
||||
heroPlaceholders: heroPlaceholders,
|
||||
singleProvinceId: details.ProvinceId,
|
||||
displayedHeroes: new List<HeroView> { firstHero, secondHero });
|
||||
}
|
||||
|
||||
+21
-6
@@ -18,19 +18,34 @@ namespace eagle.Notifications.ARNNotifications {
|
||||
|
||||
var provinceIds = new List<ProvinceId> { notif.ProvinceId };
|
||||
|
||||
var heroPlaceholders = new Dictionary<string, (string nameTextId, string fallback)> {
|
||||
{ "DepartedHero", (departedHero.NameTextId, "A hero") }
|
||||
};
|
||||
|
||||
if (notif.FromFactionId == currentModel.PlayerId) {
|
||||
yield return new Notification(
|
||||
string textTemplate =
|
||||
$"{{DepartedHero}} has left your service in {province.Name}.\n\n{notification.LlmMessage}";
|
||||
yield return new DynamicTextNotification(
|
||||
title: "Hero Departed",
|
||||
text: $"{departedHero.Name} has left your service in {province.Name}.\n\n{notification.LlmMessage}",
|
||||
textTemplate: textTemplate,
|
||||
heroPlaceholders: heroPlaceholders,
|
||||
singleProvinceId: province.Id,
|
||||
singleDisplayedHero: departedHero);
|
||||
displayedHeroes: new List<HeroView> { departedHero });
|
||||
} else {
|
||||
yield return Notification.StreamingNotification(
|
||||
string textTemplate =
|
||||
$"{{DepartedHero}} has left {{FactionLeader}}'s service in {province.Name}.\n\n";
|
||||
heroPlaceholders["FactionLeader"] =
|
||||
(currentModel.Heroes[faction.FactionHeadId].NameTextId, faction.Name);
|
||||
yield return DynamicTextNotification.StreamingDynamicNotification(
|
||||
title: "Hero Departed",
|
||||
prefixedText: $"{departedHero.Name} has left {DisplayNames.FactionString(faction, currentModel)}'s service in {province.Name}.\n\n",
|
||||
textTemplate: textTemplate,
|
||||
heroPlaceholders: heroPlaceholders,
|
||||
llmId: notification.LlmId,
|
||||
provinceIds: provinceIds,
|
||||
displayedHeroes: new List<HeroView> { departedHero, currentModel.Heroes[faction.FactionHeadId] });
|
||||
displayedHeroes: new List<HeroView> {
|
||||
departedHero,
|
||||
currentModel.Heroes[faction.FactionHeadId]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+16
-9
@@ -19,14 +19,19 @@ namespace eagle.Notifications.ARNNotifications {
|
||||
var affectedProvinces = currentModel.ProvincesForFaction(imprisoningFactionId);
|
||||
affectedProvinces.AddRange(currentModel.ProvincesForFaction(offeringFactionId));
|
||||
|
||||
var heroPlaceholders = new Dictionary<string, (string nameTextId, string fallback)> {
|
||||
{ "ImprisonedAmbassador", (imprisonedHero.NameTextId, "the ambassador") }
|
||||
};
|
||||
|
||||
if (playerId.HasValue && playerId.Value == imprisoningFactionId) {
|
||||
var factionHeadId =
|
||||
currentModel.MaybeDestroyedFaction(offeringFactionId).FactionHeadId;
|
||||
var factionHead = currentModel.Heroes[factionHeadId];
|
||||
yield return Notification.StreamingNotification(
|
||||
yield return DynamicTextNotification.StreamingDynamicNotification(
|
||||
title: "Ambassador Imprisoned",
|
||||
prefixedText:
|
||||
$"You have rejected an invitation from {currentModel.FactionName(offeringFactionId)} and imprisoned their ambassador {imprisonedHero.Name}.\n\n",
|
||||
textTemplate:
|
||||
$"You have rejected an invitation from {currentModel.FactionName(offeringFactionId)} and imprisoned their ambassador {{ImprisonedAmbassador}}.\n\n",
|
||||
heroPlaceholders: heroPlaceholders,
|
||||
llmId: notification.LlmId,
|
||||
provinceIds: affectedProvinces,
|
||||
displayedHeroes: new List<HeroView> { factionHead });
|
||||
@@ -34,10 +39,11 @@ namespace eagle.Notifications.ARNNotifications {
|
||||
var factionHeadId =
|
||||
currentModel.MaybeDestroyedFaction(imprisoningFactionId).FactionHeadId;
|
||||
var factionHead = currentModel.Heroes[factionHeadId];
|
||||
yield return Notification.StreamingNotification(
|
||||
yield return DynamicTextNotification.StreamingDynamicNotification(
|
||||
title: "Ambassador Imprisoned",
|
||||
prefixedText:
|
||||
$"{currentModel.FactionName(imprisoningFactionId)} has imprisoned our ambassador {imprisonedHero.Name}!\n\n",
|
||||
textTemplate:
|
||||
$"{currentModel.FactionName(imprisoningFactionId)} has imprisoned our ambassador {{ImprisonedAmbassador}}!\n\n",
|
||||
heroPlaceholders: heroPlaceholders,
|
||||
llmId: notification.LlmId,
|
||||
provinceIds: affectedProvinces,
|
||||
displayedHeroes: new List<HeroView> { factionHead });
|
||||
@@ -49,10 +55,11 @@ namespace eagle.Notifications.ARNNotifications {
|
||||
currentModel.MaybeDestroyedFaction(offeringFactionId).FactionHeadId;
|
||||
var factionHead2 = currentModel.Heroes[factionHeadId2];
|
||||
|
||||
yield return Notification.StreamingNotification(
|
||||
yield return DynamicTextNotification.StreamingDynamicNotification(
|
||||
title: "Ambassador Imprisoned",
|
||||
prefixedText:
|
||||
$"{currentModel.FactionName(imprisoningFactionId)} has rejected an invitation from {currentModel.FactionName(offeringFactionId)} and imprisoned their ambassador {imprisonedHero.Name}.\n\n",
|
||||
textTemplate:
|
||||
$"{currentModel.FactionName(imprisoningFactionId)} has rejected an invitation from {currentModel.FactionName(offeringFactionId)} and imprisoned their ambassador {{ImprisonedAmbassador}}.\n\n",
|
||||
heroPlaceholders: heroPlaceholders,
|
||||
llmId: notification.LlmId,
|
||||
provinceIds: affectedProvinces,
|
||||
displayedHeroes: new List<HeroView> { factionHead1,
|
||||
|
||||
+9
-4
@@ -17,12 +17,17 @@ namespace eagle.Notifications.ARNNotifications {
|
||||
var apprehended = currentModel.Heroes[details.ApprehendedHeroId];
|
||||
|
||||
string noteTitle = "Outlaw Apprehended";
|
||||
string noteText =
|
||||
$"{factionName} apprehended the outlaw {apprehended.Name} in {province.Name}.";
|
||||
string textTemplate =
|
||||
$"{factionName} apprehended the outlaw {{ApprehendedHero}} in {province.Name}.";
|
||||
|
||||
yield return new Notification(
|
||||
var heroPlaceholders = new Dictionary<string, (string nameTextId, string fallback)> {
|
||||
{ "ApprehendedHero", (apprehended.NameTextId, "an outlaw") }
|
||||
};
|
||||
|
||||
yield return new DynamicTextNotification(
|
||||
title: noteTitle,
|
||||
text: noteText,
|
||||
textTemplate: textTemplate,
|
||||
heroPlaceholders: heroPlaceholders,
|
||||
singleProvinceId: details.ProvinceId,
|
||||
displayedHeroes: new List<HeroView> { factionHead, apprehended });
|
||||
}
|
||||
|
||||
+12
-6
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Net.Eagle0.Eagle.Views;
|
||||
|
||||
namespace eagle.Notifications.ARNNotifications {
|
||||
using FactionId = Int32;
|
||||
@@ -15,21 +16,26 @@ namespace eagle.Notifications.ARNNotifications {
|
||||
var spottedOutlaw = currentModel.Heroes[details.OutlawHeroId];
|
||||
|
||||
string noteTitle = "";
|
||||
string noteText = "";
|
||||
string textTemplate = "";
|
||||
|
||||
var heroPlaceholders = new Dictionary<string, (string nameTextId, string fallback)> {
|
||||
{ "SpottedOutlaw", (spottedOutlaw.NameTextId, "An outlaw") }
|
||||
};
|
||||
|
||||
if (province.RulingFactionId is FactionId fid && fid == currentModel.PlayerId) {
|
||||
noteTitle = "Outlaw Spotted";
|
||||
noteText = $"{spottedOutlaw.Name} has been spotted in {province.Name}!";
|
||||
textTemplate = $"{{SpottedOutlaw}} has been spotted in {province.Name}!";
|
||||
} else {
|
||||
noteTitle = "Outlaw Departed";
|
||||
noteText = $"{spottedOutlaw.Name} has left our territory for {province.Name}.";
|
||||
textTemplate = $"{{SpottedOutlaw}} has left our territory for {province.Name}.";
|
||||
}
|
||||
|
||||
yield return new Notification(
|
||||
yield return new DynamicTextNotification(
|
||||
title: noteTitle,
|
||||
text: noteText,
|
||||
textTemplate: textTemplate,
|
||||
heroPlaceholders: heroPlaceholders,
|
||||
singleProvinceId: province.Id,
|
||||
singleDisplayedHero: spottedOutlaw);
|
||||
displayedHeroes: new List<HeroView> { spottedOutlaw });
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
-4
@@ -18,12 +18,18 @@ namespace eagle.Notifications.ARNNotifications {
|
||||
var hero2Faction = currentModel.FactionName(note.Hero2FactionId);
|
||||
|
||||
string noteTitle = "Prisoners Exchanged";
|
||||
string noteText =
|
||||
$"{hero1.Name} ({hero1Faction}) and {hero2.Name} ({hero2Faction}) have agreed to a prisoner exchange.";
|
||||
string textTemplate =
|
||||
$"{{Hero1}} ({hero1Faction}) and {{Hero2}} ({hero2Faction}) have agreed to a prisoner exchange.";
|
||||
|
||||
yield return new Notification(
|
||||
var heroPlaceholders = new Dictionary<string, (string nameTextId, string fallback)> {
|
||||
{ "Hero1", (hero1.NameTextId, "A hero") },
|
||||
{ "Hero2", (hero2.NameTextId, "A hero") }
|
||||
};
|
||||
|
||||
yield return new DynamicTextNotification(
|
||||
title: noteTitle,
|
||||
text: noteText,
|
||||
textTemplate: textTemplate,
|
||||
heroPlaceholders: heroPlaceholders,
|
||||
provinceIds: new List<
|
||||
ProvinceId> { note.Hero1ProvinceId, note.Hero2ProvinceId },
|
||||
displayedHeroes: new List<HeroView> { hero1, hero2 });
|
||||
|
||||
+21
-6
@@ -18,20 +18,35 @@ namespace eagle.Notifications.ARNNotifications {
|
||||
.FactionHeadId];
|
||||
|
||||
string noteTitle = "Prisoner Executed";
|
||||
string noteText;
|
||||
string textTemplate;
|
||||
|
||||
var heroPlaceholders = new Dictionary<string, (string nameTextId, string fallback)>();
|
||||
|
||||
if (note.LastFactionId is {} lastFactionId) {
|
||||
var lastFaction = currentModel.MaybeDestroyedFaction(lastFactionId);
|
||||
|
||||
noteText =
|
||||
$"{executingFactionName} has executed {DisplayNames.MaybeFactionLeaderDescription(lastFaction, hero, currentModel)}!";
|
||||
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 = $"{executingFactionName} has executed {victimDescription}!";
|
||||
heroPlaceholders["HeroName"] = (hero.NameTextId, "the hero");
|
||||
} else {
|
||||
noteText = $"{executingFactionName} has executed {hero.Name}!";
|
||||
textTemplate = $"{executingFactionName} has executed {{ExecutedHero}}!";
|
||||
heroPlaceholders["ExecutedHero"] = (hero.NameTextId, "the prisoner");
|
||||
}
|
||||
|
||||
yield return new Notification(
|
||||
yield return new DynamicTextNotification(
|
||||
title: noteTitle,
|
||||
text: noteText,
|
||||
textTemplate: textTemplate,
|
||||
heroPlaceholders: heroPlaceholders,
|
||||
singleProvinceId: note.ProvinceId,
|
||||
displayedHeroes: new List<HeroView> { factionLeader, hero });
|
||||
}
|
||||
|
||||
+9
-4
@@ -17,12 +17,17 @@ namespace eagle.Notifications.ARNNotifications {
|
||||
var hero = currentModel.Heroes[details.HeroId];
|
||||
var province = currentModel.Provinces[details.ProvinceId];
|
||||
|
||||
string noteText =
|
||||
$"You failed to complete a {DisplayNames.QuestTypeString(details.FailedQuest)} quest for {hero.Name} in {province.Name}.\n\n";
|
||||
string textTemplate =
|
||||
$"You failed to complete a {DisplayNames.QuestTypeString(details.FailedQuest)} quest for {{QuestHero}} in {province.Name}.\n\n";
|
||||
|
||||
yield return Notification.StreamingNotification(
|
||||
var heroPlaceholders = new Dictionary<string, (string nameTextId, string fallback)> {
|
||||
{ "QuestHero", (hero.NameTextId, "the hero") }
|
||||
};
|
||||
|
||||
yield return DynamicTextNotification.StreamingDynamicNotification(
|
||||
title: noteTitle,
|
||||
prefixedText: noteText,
|
||||
textTemplate: textTemplate,
|
||||
heroPlaceholders: heroPlaceholders,
|
||||
llmId: notification.LlmId,
|
||||
provinceIds: new List<ProvinceId> { province.Id },
|
||||
displayedHeroes: new List<HeroView> { hero });
|
||||
|
||||
+9
-4
@@ -17,12 +17,17 @@ namespace eagle.Notifications.ARNNotifications {
|
||||
var hero = currentModel.Heroes[details.HeroId];
|
||||
var province = currentModel.Provinces[details.ProvinceId];
|
||||
|
||||
string noteText =
|
||||
$"You completed a {DisplayNames.QuestTypeString(details.FulfilledQuest)} quest for {hero.Name} in {province.Name}.\n\n";
|
||||
string textTemplate =
|
||||
$"You completed a {DisplayNames.QuestTypeString(details.FulfilledQuest)} quest for {{QuestHero}} in {province.Name}.\n\n";
|
||||
|
||||
yield return Notification.StreamingNotification(
|
||||
var heroPlaceholders = new Dictionary<string, (string nameTextId, string fallback)> {
|
||||
{ "QuestHero", (hero.NameTextId, "the hero") }
|
||||
};
|
||||
|
||||
yield return DynamicTextNotification.StreamingDynamicNotification(
|
||||
title: noteTitle,
|
||||
prefixedText: noteText,
|
||||
textTemplate: textTemplate,
|
||||
heroPlaceholders: heroPlaceholders,
|
||||
llmId: notification.LlmId,
|
||||
provinceIds: new List<ProvinceId> { province.Id },
|
||||
displayedHeroes: new List<HeroView> { hero });
|
||||
|
||||
+10
-4
@@ -26,19 +26,25 @@ namespace eagle.Notifications.ARNNotifications {
|
||||
var ransomedHeroId = notif.RansomedHeroId;
|
||||
var ransomedHero = currentModel.Heroes[ransomedHeroId];
|
||||
|
||||
var heroPlaceholders = new Dictionary<string, (string nameTextId, string fallback)> {
|
||||
{ "RansomedHero", (ransomedHero.NameTextId, "the hero") }
|
||||
};
|
||||
|
||||
if (playerId.HasValue && playerId.Value == paidToFactionId) {
|
||||
// no notification
|
||||
} else if (playerId.HasValue && playerId.Value == paidByFactionId) {
|
||||
yield return Notification.StreamingNotification(
|
||||
yield return DynamicTextNotification.StreamingDynamicNotification(
|
||||
title: "Ransom Accepted",
|
||||
prefixedText: $"{currentModel.FactionName(paidToFactionId)} has accepted our ransom for {ransomedHero.Name}.\n\n",
|
||||
textTemplate: $"{currentModel.FactionName(paidToFactionId)} has accepted our ransom for {{RansomedHero}}.\n\n",
|
||||
heroPlaceholders: heroPlaceholders,
|
||||
llmId: notification.LlmId,
|
||||
provinceIds: new List<ProvinceId>(),
|
||||
displayedHeroes: new List<HeroView> { acceptingFactionHead, ransomedHero });
|
||||
} else {
|
||||
yield return Notification.StreamingNotification(
|
||||
yield return DynamicTextNotification.StreamingDynamicNotification(
|
||||
title: "Ransom Paid",
|
||||
prefixedText: $"{currentModel.FactionName(paidByFactionId)} paid a ransom to {currentModel.FactionName(paidToFactionId)} for the return of {ransomedHero.Name}.\n\n",
|
||||
textTemplate: $"{currentModel.FactionName(paidByFactionId)} paid a ransom to {currentModel.FactionName(paidToFactionId)} for the return of {{RansomedHero}}.\n\n",
|
||||
heroPlaceholders: heroPlaceholders,
|
||||
llmId: notification.LlmId,
|
||||
provinceIds: new List<ProvinceId>(),
|
||||
displayedHeroes: new List<HeroView> { acceptingFactionHead, offeringFactionHead, ransomedHero });
|
||||
|
||||
+13
-6
@@ -26,24 +26,31 @@ namespace eagle.Notifications.ARNNotifications {
|
||||
var ransomedHeroId = notif.RansomedHeroId;
|
||||
var ransomedHero = currentModel.Heroes[ransomedHeroId];
|
||||
|
||||
var heroPlaceholders = new Dictionary<string, (string nameTextId, string fallback)> {
|
||||
{ "RansomedHero", (ransomedHero.NameTextId, "the hero") }
|
||||
};
|
||||
|
||||
if (playerId is PlayerId pid && pid == refusingFactionId) {
|
||||
yield return Notification.StreamingNotification(
|
||||
yield return DynamicTextNotification.StreamingDynamicNotification(
|
||||
title: "Ransom Rejected",
|
||||
prefixedText: $"We have refused to accept the ransom from {currentModel.FactionName(offeringFactionId)} for {ransomedHero.Name}.\n\n",
|
||||
textTemplate: $"We have refused to accept the ransom from {currentModel.FactionName(offeringFactionId)} for {{RansomedHero}}.\n\n",
|
||||
heroPlaceholders: heroPlaceholders,
|
||||
llmId: notification.LlmId,
|
||||
provinceIds: new List<ProvinceId>(),
|
||||
displayedHeroes: new List<HeroView> { ransomedHero, offeringFactionHead});
|
||||
} else if (playerId.HasValue && playerId.Value == offeringFactionId) {
|
||||
yield return Notification.StreamingNotification(
|
||||
yield return DynamicTextNotification.StreamingDynamicNotification(
|
||||
title: "Ransom Rejected",
|
||||
prefixedText: $"{currentModel.FactionName(refusingFactionId)} has refused to accept our ransom for {ransomedHero.Name}.\n\n",
|
||||
textTemplate: $"{currentModel.FactionName(refusingFactionId)} has refused to accept our ransom for {{RansomedHero}}.\n\n",
|
||||
heroPlaceholders: heroPlaceholders,
|
||||
llmId: notification.LlmId,
|
||||
provinceIds: new List<ProvinceId>(),
|
||||
displayedHeroes: new List<HeroView> { refusingFactionHead, ransomedHero});
|
||||
} else {
|
||||
yield return Notification.StreamingNotification(
|
||||
yield return DynamicTextNotification.StreamingDynamicNotification(
|
||||
title: "Ransom Rejected",
|
||||
prefixedText: $"{currentModel.FactionName(refusingFactionId)} refused to accept a ransom to {currentModel.FactionName(offeringFactionId)} for the return of {ransomedHero.Name}.\n\n",
|
||||
textTemplate: $"{currentModel.FactionName(refusingFactionId)} refused to accept a ransom to {currentModel.FactionName(offeringFactionId)} for the return of {{RansomedHero}}.\n\n",
|
||||
heroPlaceholders: heroPlaceholders,
|
||||
llmId: notification.LlmId,
|
||||
provinceIds: new List<ProvinceId>(),
|
||||
displayedHeroes: new List<HeroView> { refusingFactionHead, offeringFactionHead, ransomedHero });
|
||||
|
||||
+12
-4
@@ -17,13 +17,21 @@ namespace eagle.Notifications.ARNNotifications {
|
||||
|
||||
var affectedProvinces = currentModel.ProvincesForFaction(details.FactionId);
|
||||
|
||||
yield return Notification.StreamingNotification(
|
||||
string textTemplate =
|
||||
$"{{NewBrother}} has sworn brotherhood with {factionName}'s leader {{FactionHead}}.\n\n";
|
||||
|
||||
var heroPlaceholders = new Dictionary<string, (string nameTextId, string fallback)> {
|
||||
{ "NewBrother", (newBrother.NameTextId, "A hero") },
|
||||
{ "FactionHead", (factionHead.NameTextId, "the leader") }
|
||||
};
|
||||
|
||||
yield return DynamicTextNotification.StreamingDynamicNotification(
|
||||
title: "Swore Brotherhood",
|
||||
prefixedText:
|
||||
$"{newBrother.Name} has sworn brotherhood with {factionName}'s leader {factionHead.Name}.\n\n",
|
||||
textTemplate: textTemplate,
|
||||
heroPlaceholders: heroPlaceholders,
|
||||
llmId: notification.LlmId,
|
||||
provinceIds: affectedProvinces,
|
||||
displayedHeroes: new List<HeroView> { newBrother, factionHead});
|
||||
displayedHeroes: new List<HeroView> { newBrother, factionHead });
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
-9
@@ -27,27 +27,34 @@ namespace eagle.Notifications.ARNNotifications {
|
||||
currentModel.MaybeDestroyedFaction(imprisoningFactionId).FactionHeadId;
|
||||
var imprisoningFactionHead = currentModel.Heroes[imprisoningFactionHeadId];
|
||||
|
||||
var heroPlaceholders = new Dictionary<string, (string nameTextId, string fallback)> {
|
||||
{ "ImprisonedAmbassador", (imprisonedHero.NameTextId, "the ambassador") }
|
||||
};
|
||||
|
||||
if (playerId.HasValue && playerId.Value == imprisoningFactionId) {
|
||||
yield return Notification.StreamingNotification(
|
||||
yield return DynamicTextNotification.StreamingDynamicNotification(
|
||||
title: "Ambassador Imprisoned",
|
||||
prefixedText:
|
||||
$"You have rejected a treaty with {currentModel.FactionName(offeringFactionId)} and imprisoned their ambassador {imprisonedHero.Name}.\n\n",
|
||||
textTemplate:
|
||||
$"You have rejected a treaty with {currentModel.FactionName(offeringFactionId)} and imprisoned their ambassador {{ImprisonedAmbassador}}.\n\n",
|
||||
heroPlaceholders: heroPlaceholders,
|
||||
llmId: notification.LlmId,
|
||||
provinceIds: affectedProvinces,
|
||||
displayedHeroes: new List<HeroView> { offeringFactionHead, imprisonedHero});
|
||||
} else if (playerId.HasValue && playerId.Value == offeringFactionId) {
|
||||
yield return Notification.StreamingNotification(
|
||||
yield return DynamicTextNotification.StreamingDynamicNotification(
|
||||
title: "Ambassador Imprisoned",
|
||||
prefixedText:
|
||||
$"{currentModel.FactionName(imprisoningFactionId)} has imprisoned our ambassador {imprisonedHero.Name}!\n\n",
|
||||
textTemplate:
|
||||
$"{currentModel.FactionName(imprisoningFactionId)} has imprisoned our ambassador {{ImprisonedAmbassador}}!\n\n",
|
||||
heroPlaceholders: heroPlaceholders,
|
||||
llmId: notification.LlmId,
|
||||
provinceIds: affectedProvinces,
|
||||
displayedHeroes: new List<HeroView> { imprisoningFactionHead, imprisonedHero});
|
||||
} else {
|
||||
yield return Notification.StreamingNotification(
|
||||
yield return DynamicTextNotification.StreamingDynamicNotification(
|
||||
title: "Ambassador Imprisoned",
|
||||
prefixedText:
|
||||
$"{currentModel.FactionName(imprisoningFactionId)} has rejected a truce offer from {currentModel.FactionName(offeringFactionId)} and imprisoned their ambassador {imprisonedHero.Name}.\n\n",
|
||||
textTemplate:
|
||||
$"{currentModel.FactionName(imprisoningFactionId)} has rejected a truce offer from {currentModel.FactionName(offeringFactionId)} and imprisoned their ambassador {{ImprisonedAmbassador}}.\n\n",
|
||||
heroPlaceholders: heroPlaceholders,
|
||||
llmId: notification.LlmId,
|
||||
provinceIds: affectedProvinces,
|
||||
displayedHeroes: new List<HeroView> { imprisoningFactionHead, offeringFactionHead, imprisonedHero });
|
||||
|
||||
+9
-4
@@ -20,12 +20,17 @@ namespace eagle.Notifications.ARNNotifications {
|
||||
.FactionHeadId];
|
||||
|
||||
string noteTitle = "Vassal Exiled";
|
||||
string noteText =
|
||||
$"{factionName} sent {exiledHero.Name} into exile from {province.Name}\n\n";
|
||||
string textTemplate =
|
||||
$"{factionName} sent {{ExiledHero}} into exile from {province.Name}\n\n";
|
||||
|
||||
yield return Notification.StreamingNotification(
|
||||
var heroPlaceholders = new Dictionary<string, (string nameTextId, string fallback)> {
|
||||
{ "ExiledHero", (exiledHero.NameTextId, "the vassal") }
|
||||
};
|
||||
|
||||
yield return DynamicTextNotification.StreamingDynamicNotification(
|
||||
title: noteTitle,
|
||||
prefixedText: noteText,
|
||||
textTemplate: textTemplate,
|
||||
heroPlaceholders: heroPlaceholders,
|
||||
llmId: notification.LlmId,
|
||||
provinceIds: new List<ProvinceId> { details.ProvinceId },
|
||||
displayedHeroes: new List<HeroView> { factionHead, exiledHero });
|
||||
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Net.Eagle0.Eagle.Views;
|
||||
|
||||
namespace eagle.Notifications {
|
||||
using ProvinceId = Int32;
|
||||
|
||||
public class DynamicTextNotification : Notification, IDisposable {
|
||||
private List<GeneratedTextListener> textListeners = new();
|
||||
private string textTemplate;
|
||||
private Dictionary<string, string> placeholderValues = new();
|
||||
|
||||
public DynamicTextNotification(
|
||||
string title,
|
||||
string textTemplate,
|
||||
Dictionary<string, (string nameTextId, string fallback)> heroPlaceholders,
|
||||
List<ProvinceId> provinceIds,
|
||||
List<HeroView> displayedHeroes,
|
||||
bool shouldAppend = true,
|
||||
string llmId = null)
|
||||
: base(title,
|
||||
BuildInitialText(textTemplate, heroPlaceholders),
|
||||
provinceIds,
|
||||
displayedHeroes,
|
||||
shouldAppend,
|
||||
llmId) {
|
||||
this.textTemplate = textTemplate;
|
||||
SetupTextListeners(heroPlaceholders);
|
||||
}
|
||||
|
||||
public DynamicTextNotification(
|
||||
string title,
|
||||
string textTemplate,
|
||||
Dictionary<string, (string nameTextId, string fallback)> heroPlaceholders,
|
||||
ProvinceId singleProvinceId,
|
||||
List<HeroView> displayedHeroes,
|
||||
bool shouldAppend = true)
|
||||
: this(title,
|
||||
textTemplate,
|
||||
heroPlaceholders,
|
||||
new List<ProvinceId> { singleProvinceId },
|
||||
displayedHeroes,
|
||||
shouldAppend) {}
|
||||
|
||||
private static string BuildInitialText(
|
||||
string template,
|
||||
Dictionary<string, (string nameTextId, string fallback)> heroPlaceholders) {
|
||||
string result = template;
|
||||
foreach (var kvp in heroPlaceholders) {
|
||||
string placeholder = kvp.Key;
|
||||
string fallback = kvp.Value.fallback;
|
||||
|
||||
// Try to get text immediately, fall back if not available
|
||||
string currentText = fallback;
|
||||
if (!string.IsNullOrEmpty(kvp.Value.nameTextId)) {
|
||||
var textEntry = ClientTextProvider.Provider.GetTextEntry(kvp.Value.nameTextId);
|
||||
if (textEntry != null && !string.IsNullOrEmpty(textEntry.Text)) {
|
||||
currentText = textEntry.Text;
|
||||
}
|
||||
}
|
||||
|
||||
result = result.Replace($"{{{placeholder}}}", currentText);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void SetupTextListeners(
|
||||
Dictionary<string, (string nameTextId, string fallback)> heroPlaceholders) {
|
||||
int index = 0;
|
||||
foreach (var kvp in heroPlaceholders) {
|
||||
string placeholder = kvp.Key;
|
||||
string nameTextId = kvp.Value.nameTextId;
|
||||
string fallback = kvp.Value.fallback;
|
||||
|
||||
if (!string.IsNullOrEmpty(nameTextId)) {
|
||||
var listener = new GeneratedTextListener(
|
||||
nameTextId,
|
||||
"",
|
||||
"",
|
||||
index,
|
||||
(listenerIndex, text) => UpdatePlaceholder(placeholder, text));
|
||||
|
||||
textListeners.Add(listener);
|
||||
ClientTextProvider.Provider.AddListener(listener);
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdatePlaceholder(string placeholder, string newText) {
|
||||
placeholderValues[placeholder] = newText;
|
||||
UpdateText();
|
||||
}
|
||||
|
||||
private void UpdateText() {
|
||||
string result = textTemplate;
|
||||
foreach (var kvp in placeholderValues) {
|
||||
result = result.Replace($"{{{kvp.Key}}}", kvp.Value);
|
||||
}
|
||||
|
||||
// Use reflection to update the private setter
|
||||
var textProperty = typeof(Notification).GetProperty("Text");
|
||||
textProperty?.SetValue(this, result);
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
foreach (var listener in textListeners) {
|
||||
ClientTextProvider.Provider.RemoveListener(listener);
|
||||
}
|
||||
textListeners.Clear();
|
||||
}
|
||||
|
||||
public static DynamicTextNotification StreamingDynamicNotification(
|
||||
string title,
|
||||
string textTemplate,
|
||||
Dictionary<string, (string nameTextId, string fallback)> heroPlaceholders,
|
||||
string llmId,
|
||||
List<ProvinceId> provinceIds,
|
||||
List<HeroView> displayedHeroes) {
|
||||
return new DynamicTextNotification(
|
||||
title: title,
|
||||
textTemplate: textTemplate,
|
||||
heroPlaceholders: heroPlaceholders,
|
||||
provinceIds: provinceIds,
|
||||
displayedHeroes: displayedHeroes,
|
||||
shouldAppend: false,
|
||||
llmId: llmId);
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 025d7dcb002bb4e5ca00edcc77689b6d
|
||||
+13
-5
@@ -8,12 +8,20 @@ namespace eagle.Notifications {
|
||||
private static IEnumerable<Notification> GenerateNotification(
|
||||
ActionResultView actionResultView,
|
||||
IGameModel currentModel) {
|
||||
yield return new Notification(
|
||||
var hero = currentModel.Heroes[actionResultView.Leader.Value];
|
||||
string textTemplate =
|
||||
"{DecliningHero} feels you are too different to swear eternal brotherhood.";
|
||||
|
||||
var heroPlaceholders = new Dictionary<string, (string nameTextId, string fallback)> {
|
||||
{ "DecliningHero", (hero.NameTextId, "The hero") }
|
||||
};
|
||||
|
||||
yield return new DynamicTextNotification(
|
||||
title: "Oath Declined",
|
||||
text:
|
||||
$"{currentModel.Heroes[actionResultView.Leader.Value].Name} feels you are too different to swear eternal brotherhood.",
|
||||
singleProvinceId: actionResultView.GameStateDiff
|
||||
.ChangedProvinces[0].Id);
|
||||
textTemplate: textTemplate,
|
||||
heroPlaceholders: heroPlaceholders,
|
||||
singleProvinceId: actionResultView.GameStateDiff.ChangedProvinces[0].Id,
|
||||
displayedHeroes: new List<HeroView> { hero });
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
-4
@@ -22,13 +22,19 @@ namespace eagle.Notifications {
|
||||
var province = currentModel.Provinces[cp.Id];
|
||||
|
||||
string noteTitle = "Prisoner Escaped";
|
||||
string noteText = $"{hero.Name} has escaped from {province.Name}!";
|
||||
string textTemplate = $"{{EscapedHero}} has escaped from {province.Name}!";
|
||||
|
||||
yield return new Notification(
|
||||
var heroPlaceholders =
|
||||
new Dictionary<string, (string nameTextId, string fallback)> {
|
||||
{ "EscapedHero", (hero.NameTextId, "A prisoner") }
|
||||
};
|
||||
|
||||
yield return new DynamicTextNotification(
|
||||
title: noteTitle,
|
||||
text: noteText,
|
||||
textTemplate: textTemplate,
|
||||
heroPlaceholders: heroPlaceholders,
|
||||
singleProvinceId: cp.Id,
|
||||
singleDisplayedHero: hero);
|
||||
displayedHeroes: new List<HeroView> { hero });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+13
-4
@@ -19,12 +19,21 @@ namespace eagle.Notifications {
|
||||
var hero = currentModel.Heroes[ch.Id];
|
||||
|
||||
const string noteTitle = "Prisoner Returned";
|
||||
string noteText =
|
||||
$"{DisplayNames.FactionString(actingFaction, currentModel)} has returned {hero.Name} to us!";
|
||||
string textTemplate =
|
||||
$"{{ActingFactionLeader}} has returned {{ReturnedHero}} to us!";
|
||||
|
||||
yield return new Notification(
|
||||
var heroPlaceholders =
|
||||
new Dictionary<string, (string nameTextId, string fallback)> {
|
||||
{ "ReturnedHero", (hero.NameTextId, "our hero") },
|
||||
{ "ActingFactionLeader",
|
||||
(currentModel.Heroes[actingFaction.FactionHeadId].NameTextId,
|
||||
actingFaction.Name) }
|
||||
};
|
||||
|
||||
yield return new DynamicTextNotification(
|
||||
title: noteTitle,
|
||||
text: noteText,
|
||||
textTemplate: textTemplate,
|
||||
heroPlaceholders: heroPlaceholders,
|
||||
provinceIds: new List<ProvinceId>(),
|
||||
displayedHeroes: new List<HeroView> { actingFactionLeader, hero });
|
||||
}
|
||||
|
||||
+9
-6
@@ -19,15 +19,17 @@ namespace eagle.Notifications {
|
||||
leader = currentModel.Heroes[rulingHid];
|
||||
}
|
||||
string noteTitle = "Riot";
|
||||
string noteText = $"A riot seems to have occurred in {province.Name}.";
|
||||
string baseText = $"A riot seems to have occurred in {province.Name}.";
|
||||
var heroPlaceholders = new Dictionary<string, (string nameTextId, string fallback)>();
|
||||
|
||||
HeroView killedHero = null;
|
||||
if (province.RulingFactionId.HasValue && province.RulingFactionId.Value == playerId) {
|
||||
noteText = $"A terrible riot has occurred in {province.Name}.";
|
||||
baseText = $"A terrible riot has occurred in {province.Name}.";
|
||||
foreach (var removedHeroId in actionResultView.GameStateDiff.RemovedHeroIds) {
|
||||
var hero = currentModel.Heroes[removedHeroId];
|
||||
noteText += "\n";
|
||||
noteText += $"{hero.Name} was killed in the riot!";
|
||||
baseText += "\n";
|
||||
baseText += $"{{KilledHero}} was killed in the riot!";
|
||||
heroPlaceholders["KilledHero"] = (hero.NameTextId, "A hero");
|
||||
killedHero = hero;
|
||||
if (killedHero.Id == leader.Id) killedHero = null;
|
||||
}
|
||||
@@ -36,9 +38,10 @@ namespace eagle.Notifications {
|
||||
var displayedHeroes =
|
||||
new List<HeroView> { leader, killedHero }.Where(x => x != null).ToList();
|
||||
|
||||
yield return new Notification(
|
||||
yield return new DynamicTextNotification(
|
||||
title: noteTitle,
|
||||
text: noteText,
|
||||
textTemplate: baseText,
|
||||
heroPlaceholders: heroPlaceholders,
|
||||
singleProvinceId: province.Id,
|
||||
displayedHeroes: displayedHeroes);
|
||||
}
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ namespace eagle.Notifications {
|
||||
|
||||
string noteTitle = "Army Turned Back";
|
||||
string noteText =
|
||||
$"{DisplayNames.FactionString(faction, currentModel)}'s army approaching {province.Name} was forced to turn back due to harsh weather.";
|
||||
$"{DisplayNames.ResolvedFactionString(faction, currentModel)}'s army approaching {province.Name} was forced to turn back due to harsh weather.";
|
||||
|
||||
yield return new Notification(
|
||||
title: noteTitle,
|
||||
|
||||
-5
@@ -492,11 +492,6 @@ namespace eagle {
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case UpdateStreamResponse.ResponseDetailsOneofCase.HeadshotResponse:
|
||||
AWSResourceFetcher.headshotFetcher.ReceiveHeadshotResponse(
|
||||
current.HeadshotResponse);
|
||||
break;
|
||||
}
|
||||
|
||||
sc = _streamingCall;
|
||||
|
||||
+3
-3
@@ -92,7 +92,7 @@ namespace eagle {
|
||||
if (imagesAndNamesContainer) imagesAndNamesContainer.gameObject.SetActive(true);
|
||||
|
||||
var firstHero = PopupInfos[DisplayedIndex].displayedHeroes[0];
|
||||
AWSResourceFetcher.headshotFetcher.LoadIntoRawImage(
|
||||
ResourceFetcher.headshotFetcher.LoadIntoRawImage(
|
||||
firstImage,
|
||||
firstHero.ImagePath);
|
||||
firstImage.transform.parent.gameObject.SetActive(true);
|
||||
@@ -100,7 +100,7 @@ namespace eagle {
|
||||
|
||||
if (imageCount > 1) {
|
||||
var secondHero = PopupInfos[DisplayedIndex].displayedHeroes[1];
|
||||
AWSResourceFetcher.headshotFetcher.LoadIntoRawImage(
|
||||
ResourceFetcher.headshotFetcher.LoadIntoRawImage(
|
||||
secondImage,
|
||||
secondHero.ImagePath);
|
||||
secondImage.transform.parent.gameObject.SetActive(true);
|
||||
@@ -108,7 +108,7 @@ namespace eagle {
|
||||
|
||||
if (imageCount > 2) {
|
||||
var thirdHero = PopupInfos[DisplayedIndex].displayedHeroes[2];
|
||||
AWSResourceFetcher.headshotFetcher.LoadIntoRawImage(
|
||||
ResourceFetcher.headshotFetcher.LoadIntoRawImage(
|
||||
thirdImage,
|
||||
thirdHero.ImagePath);
|
||||
thirdImage.transform.parent.gameObject.SetActive(true);
|
||||
|
||||
+5
-4
@@ -94,13 +94,14 @@ namespace eagle {
|
||||
public DisclosureTriangle dominionViewToggle;
|
||||
|
||||
private ProvinceView _provinceView;
|
||||
private DynamicFactionTextUpdater factionTextUpdater = new DynamicFactionTextUpdater();
|
||||
|
||||
private void SetHeadshotToHeroId(HeroId hid) {
|
||||
var rulingHero = _model.Heroes[hid];
|
||||
|
||||
if (rulingHero.ImagePath is String path) {
|
||||
HeadshotImage.gameObject.SetActive(true);
|
||||
AWSResourceFetcher.headshotFetcher.LoadIntoRawImage(HeadshotImage, path);
|
||||
ResourceFetcher.headshotFetcher.LoadIntoRawImage(HeadshotImage, path);
|
||||
} else {
|
||||
HeadshotImage.gameObject.SetActive(false);
|
||||
}
|
||||
@@ -314,12 +315,10 @@ namespace eagle {
|
||||
rulingHeroPrefix = "UNKNOWN";
|
||||
}
|
||||
|
||||
var factionLabel = DisplayNames.FactionString(faction, Model);
|
||||
|
||||
ProvinceNameField.text = Province.Name;
|
||||
provinceLeaderField.TextId = rulingHeroNameTextId;
|
||||
provinceLeaderField.PrefixedText = rulingHeroPrefix;
|
||||
provinceFactionField.text = factionLabel;
|
||||
factionTextUpdater.SetFactionText(provinceFactionField, faction, Model);
|
||||
|
||||
var diploStrings = new List<string>();
|
||||
if (!hasFullInfo) {
|
||||
@@ -400,5 +399,7 @@ namespace eagle {
|
||||
dominionViewToggle.isOn = !dominionViewToggle.isOn;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy() { factionTextUpdater?.Dispose(); }
|
||||
}
|
||||
}
|
||||
+8
-2
@@ -4,7 +4,7 @@ using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace eagle {
|
||||
public class CombatUnitRowController : TableRowController {
|
||||
public class CombatUnitRowController : TableRowController, System.IDisposable {
|
||||
public RawImage professionImage;
|
||||
public TMP_Text heroNameLabel;
|
||||
public RawImage battalionImage;
|
||||
@@ -12,11 +12,13 @@ namespace eagle {
|
||||
public TMP_Text trainingLabel;
|
||||
public TMP_Text armamentLabel;
|
||||
|
||||
private DynamicHeroTextUpdater textUpdater = new DynamicHeroTextUpdater();
|
||||
|
||||
public HeroView Hero {
|
||||
set {
|
||||
professionImage.texture = CommonTextures.Profession(value.Profession);
|
||||
professionImage.color = professionImage.texture == null ? Color.clear : Color.white;
|
||||
heroNameLabel.text = value.Name;
|
||||
textUpdater.SetHeroText(heroNameLabel, value.NameTextId, "Hero");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,5 +39,9 @@ namespace eagle {
|
||||
battalionImage.color = battalionImage.texture == null ? Color.clear : Color.white;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() { textUpdater?.Dispose(); }
|
||||
|
||||
private void OnDestroy() { Dispose(); }
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -67,7 +67,7 @@ namespace eagle {
|
||||
public CommonUnit Unit {
|
||||
get {
|
||||
var hero = new CommonHero {
|
||||
Name = heroName.text,
|
||||
NameTextId = heroName.text,
|
||||
Profession = SelectedProfession,
|
||||
Strength = int.Parse(strength.text),
|
||||
Agility = int.Parse(agility.text),
|
||||
@@ -102,7 +102,7 @@ namespace eagle {
|
||||
};
|
||||
}
|
||||
set {
|
||||
heroName.text = value.Hero.Name;
|
||||
heroName.text = value.Hero.NameTextId;
|
||||
professionDropdown.value = value.Hero.Profession == CommonProfession.NoProfession
|
||||
? 0
|
||||
: (int)value.Hero.Profession;
|
||||
|
||||
+9
-2
@@ -12,7 +12,8 @@ namespace eagle {
|
||||
public class DominionTableRowController : TableRowController,
|
||||
IPointerEnterHandler,
|
||||
IPointerExitHandler,
|
||||
IPointerClickHandler {
|
||||
IPointerClickHandler,
|
||||
System.IDisposable {
|
||||
public TMP_Text provinceName;
|
||||
public TMP_Text ruler;
|
||||
public TMP_Text support;
|
||||
@@ -34,6 +35,7 @@ namespace eagle {
|
||||
public GameObject flood;
|
||||
|
||||
private ProvinceId ProvinceId;
|
||||
private DynamicHeroTextUpdater textUpdater = new DynamicHeroTextUpdater();
|
||||
|
||||
private void SetDevastatedValue(TMP_Text textField, double baseValue, double devastation) {
|
||||
if (devastation == 0.0) {
|
||||
@@ -61,7 +63,8 @@ namespace eagle {
|
||||
provinceName.color = Color.black;
|
||||
}
|
||||
|
||||
ruler.text = model.Heroes[province.FullInfo.RulingHeroId.Value].Name;
|
||||
var rulingHero = model.Heroes[province.FullInfo.RulingHeroId.Value];
|
||||
textUpdater.SetHeroText(ruler, rulingHero.NameTextId, "Ruler");
|
||||
support.text = GUIUtils.ConditionString(province.FullInfo.Support);
|
||||
commanderCount.text = province.FullInfo.RulingFactionHeroIds.Count.ToString();
|
||||
battalionCount.text = province.FullInfo.Battalions.Count.ToString();
|
||||
@@ -156,5 +159,9 @@ namespace eagle {
|
||||
public void OnPointerClick(PointerEventData eventData) {
|
||||
GetComponentInParent<DominionPanelController>().RowClicked(ProvinceId);
|
||||
}
|
||||
|
||||
public void Dispose() { textUpdater?.Dispose(); }
|
||||
|
||||
private void OnDestroy() { Dispose(); }
|
||||
}
|
||||
}
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Net.Eagle0.Eagle.Views;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
namespace eagle {
|
||||
public class DynamicFactionTextUpdater : IDisposable {
|
||||
private List<GeneratedTextListener> textListeners = new();
|
||||
private Dictionary<string, TMP_Text> textComponents = new();
|
||||
private Dictionary<string, FactionTextConfig> factionConfigs = new();
|
||||
|
||||
private class FactionTextConfig {
|
||||
public FactionView Faction { get; }
|
||||
public IGameModel GameModel { get; }
|
||||
public string Template { get; }
|
||||
public string FallbackText { get; }
|
||||
|
||||
public FactionTextConfig(
|
||||
FactionView faction,
|
||||
IGameModel gameModel,
|
||||
string template,
|
||||
string fallbackText) {
|
||||
Faction = faction;
|
||||
GameModel = gameModel;
|
||||
Template = template;
|
||||
FallbackText = fallbackText;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetFactionText(
|
||||
TMP_Text textComponent,
|
||||
FactionView faction,
|
||||
IGameModel gameModel,
|
||||
string template = null,
|
||||
string fallbackText = null) {
|
||||
if (!textComponent || faction == null || gameModel == null) return;
|
||||
|
||||
string componentKey = textComponent.GetInstanceID().ToString();
|
||||
textComponents[componentKey] = textComponent;
|
||||
|
||||
// Use the provided template or get the default faction string template
|
||||
string actualTemplate =
|
||||
template ?? DisplayNames.FactionStringTemplate(faction, gameModel);
|
||||
string actualFallback = fallbackText ?? faction.Name;
|
||||
|
||||
factionConfigs[componentKey] =
|
||||
new FactionTextConfig(faction, gameModel, actualTemplate, actualFallback);
|
||||
|
||||
// Set initial text
|
||||
UpdateFactionText(componentKey);
|
||||
|
||||
// If the template contains {FactionLeaderName}, subscribe to leader name updates
|
||||
if (actualTemplate.Contains("{FactionLeaderName}")) {
|
||||
var leader = gameModel.Heroes[faction.FactionHeadId];
|
||||
if (!string.IsNullOrEmpty(leader.NameTextId)) {
|
||||
var textEntry = ClientTextProvider.Provider.GetTextEntry(leader.NameTextId);
|
||||
if (textEntry == null || string.IsNullOrEmpty(textEntry.Text)) {
|
||||
// Create listener for dynamic updates
|
||||
var listener = new GeneratedTextListener(
|
||||
leader.NameTextId,
|
||||
"",
|
||||
"",
|
||||
0,
|
||||
(index, text) => UpdateFactionText(componentKey));
|
||||
|
||||
textListeners.Add(listener);
|
||||
ClientTextProvider.Provider.AddListener(listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SetFactionTextWithCustomFormat(
|
||||
TMP_Text textComponent,
|
||||
FactionView faction,
|
||||
IGameModel gameModel,
|
||||
string customFormat,
|
||||
string fallbackText = null) {
|
||||
if (!textComponent || faction == null || gameModel == null ||
|
||||
string.IsNullOrEmpty(customFormat))
|
||||
return;
|
||||
|
||||
string componentKey = textComponent.GetInstanceID().ToString();
|
||||
textComponents[componentKey] = textComponent;
|
||||
|
||||
string actualFallback = fallbackText ?? faction.Name;
|
||||
factionConfigs[componentKey] =
|
||||
new FactionTextConfig(faction, gameModel, customFormat, actualFallback);
|
||||
|
||||
// Set initial text
|
||||
UpdateFactionTextWithCustomFormat(componentKey, customFormat);
|
||||
|
||||
// If the custom format contains {FactionLeaderName}, subscribe to leader name updates
|
||||
if (customFormat.Contains("{FactionLeaderName}")) {
|
||||
var leader = gameModel.Heroes[faction.FactionHeadId];
|
||||
if (!string.IsNullOrEmpty(leader.NameTextId)) {
|
||||
var textEntry = ClientTextProvider.Provider.GetTextEntry(leader.NameTextId);
|
||||
if (textEntry == null || string.IsNullOrEmpty(textEntry.Text)) {
|
||||
// Create listener for dynamic updates
|
||||
var listener = new GeneratedTextListener(
|
||||
leader.NameTextId,
|
||||
"",
|
||||
"",
|
||||
0,
|
||||
(index, text) => UpdateFactionTextWithCustomFormat(
|
||||
componentKey,
|
||||
customFormat));
|
||||
|
||||
textListeners.Add(listener);
|
||||
ClientTextProvider.Provider.AddListener(listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateFactionText(string componentKey) {
|
||||
if (!textComponents.TryGetValue(componentKey, out var component) ||
|
||||
!factionConfigs.TryGetValue(componentKey, out var config) || component == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var template = config.Template;
|
||||
if (template.Contains("{FactionLeaderName}")) {
|
||||
var leader = config.GameModel.Heroes[config.Faction.FactionHeadId];
|
||||
var textEntry = ClientTextProvider.Provider.GetTextEntry(leader.NameTextId);
|
||||
if (textEntry != null && !string.IsNullOrEmpty(textEntry.Text)) {
|
||||
component.text = template.Replace("{FactionLeaderName}", textEntry.Text);
|
||||
} else {
|
||||
component.text = template.Replace("{FactionLeaderName}", "Leader");
|
||||
}
|
||||
} else {
|
||||
component.text = template;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateFactionTextWithCustomFormat(string componentKey, string customFormat) {
|
||||
if (!textComponents.TryGetValue(componentKey, out var component) ||
|
||||
!factionConfigs.TryGetValue(componentKey, out var config) || component == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
string resolvedText = customFormat;
|
||||
if (customFormat.Contains("{FactionLeaderName}")) {
|
||||
var leader = config.GameModel.Heroes[config.Faction.FactionHeadId];
|
||||
var textEntry = ClientTextProvider.Provider.GetTextEntry(leader.NameTextId);
|
||||
if (textEntry != null && !string.IsNullOrEmpty(textEntry.Text)) {
|
||||
resolvedText = resolvedText.Replace("{FactionLeaderName}", textEntry.Text);
|
||||
} else {
|
||||
resolvedText = resolvedText.Replace("{FactionLeaderName}", "Leader");
|
||||
}
|
||||
}
|
||||
|
||||
if (customFormat.Contains("{FactionName}")) {
|
||||
resolvedText = resolvedText.Replace("{FactionName}", config.Faction.Name);
|
||||
}
|
||||
|
||||
if (customFormat.Contains("{Prestige}")) {
|
||||
resolvedText =
|
||||
resolvedText.Replace("{Prestige}", config.Faction.Prestige.ToString());
|
||||
}
|
||||
|
||||
component.text = resolvedText;
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
foreach (var listener in textListeners) {
|
||||
ClientTextProvider.Provider.RemoveListener(listener);
|
||||
}
|
||||
textListeners.Clear();
|
||||
textComponents.Clear();
|
||||
factionConfigs.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 73a4160390d2f45938cfcd0c2cc05b21
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
namespace eagle {
|
||||
public class DynamicHeroTextUpdater : IDisposable {
|
||||
private List<GeneratedTextListener> textListeners = new();
|
||||
private Dictionary<string, TMP_Text> textComponents = new();
|
||||
private Dictionary<string, string> fallbackTexts = new();
|
||||
|
||||
public void SetHeroText(TMP_Text textComponent, string nameTextId, string fallbackText) {
|
||||
if (!textComponent) return;
|
||||
|
||||
string componentKey = textComponent.GetInstanceID().ToString();
|
||||
textComponents[componentKey] = textComponent;
|
||||
fallbackTexts[componentKey] = fallbackText;
|
||||
|
||||
// Set initial text
|
||||
if (!string.IsNullOrEmpty(nameTextId)) {
|
||||
var textEntry = ClientTextProvider.Provider.GetTextEntry(nameTextId);
|
||||
if (textEntry != null && !string.IsNullOrEmpty(textEntry.Text)) {
|
||||
textComponent.text = textEntry.Text;
|
||||
} else {
|
||||
textComponent.text = fallbackText;
|
||||
|
||||
// Create listener for dynamic updates
|
||||
var listener = new GeneratedTextListener(
|
||||
nameTextId,
|
||||
"",
|
||||
"",
|
||||
0,
|
||||
(index, text) => UpdateComponentText(componentKey, text));
|
||||
|
||||
textListeners.Add(listener);
|
||||
ClientTextProvider.Provider.AddListener(listener);
|
||||
}
|
||||
} else {
|
||||
textComponent.text = fallbackText;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateComponentText(string componentKey, string newText) {
|
||||
if (textComponents.TryGetValue(componentKey, out var component) && component != null) {
|
||||
component.text = newText;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
foreach (var listener in textListeners) {
|
||||
ClientTextProvider.Provider.RemoveListener(listener);
|
||||
}
|
||||
textListeners.Clear();
|
||||
textComponents.Clear();
|
||||
fallbackTexts.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6efb8db8ffa154bdaac3cb4297833938
|
||||
+8
-2
@@ -4,7 +4,7 @@ using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace eagle {
|
||||
public class ExchangedHeroRowController : TableRowController {
|
||||
public class ExchangedHeroRowController : TableRowController, System.IDisposable {
|
||||
public RawImage professionImage;
|
||||
public TMP_Text heroNameLabel;
|
||||
public TMP_Text strLabel;
|
||||
@@ -19,6 +19,8 @@ namespace eagle {
|
||||
public RawImage prisonerImage;
|
||||
public TMP_Text lastFactionLabel;
|
||||
|
||||
private DynamicHeroTextUpdater textUpdater = new DynamicHeroTextUpdater();
|
||||
|
||||
public void SetHostage(HeroView hero, string factionName) {
|
||||
SetHero(hero, factionName);
|
||||
prisonerImage.gameObject.SetActive(false);
|
||||
@@ -38,7 +40,7 @@ namespace eagle {
|
||||
} else {
|
||||
professionImage.color = Color.white;
|
||||
}
|
||||
heroNameLabel.text = hero.Name;
|
||||
textUpdater.SetHeroText(heroNameLabel, hero.NameTextId, "Hero");
|
||||
|
||||
if (hero.Strength > 0) {
|
||||
strLabel.text = hero.Strength.ToString();
|
||||
@@ -59,5 +61,9 @@ namespace eagle {
|
||||
|
||||
lastFactionLabel.text = factionName;
|
||||
}
|
||||
|
||||
public void Dispose() { textUpdater?.Dispose(); }
|
||||
|
||||
private void OnDestroy() { Dispose(); }
|
||||
}
|
||||
}
|
||||
+9
-3
@@ -5,7 +5,7 @@ using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace eagle {
|
||||
public class HeroRowController : TableRowController {
|
||||
public class HeroRowController : TableRowController, System.IDisposable {
|
||||
public RawImage professionImage;
|
||||
public TMP_Text heroNameLabel;
|
||||
public TMP_Text strengthLabel;
|
||||
@@ -23,17 +23,19 @@ namespace eagle {
|
||||
public RawImage archeryImage;
|
||||
public RawImage fireImage;
|
||||
|
||||
private DynamicHeroTextUpdater textUpdater = new DynamicHeroTextUpdater();
|
||||
|
||||
public HeroView Hero {
|
||||
set {
|
||||
professionImage.texture =
|
||||
gameObject.GetComponentInParent<EagleCommonTextures>().Profession(
|
||||
value.Profession);
|
||||
if (professionImage.texture == null) {
|
||||
if (!professionImage.texture) {
|
||||
professionImage.color = Color.clear;
|
||||
} else {
|
||||
professionImage.color = Color.white;
|
||||
}
|
||||
heroNameLabel.text = value.Name;
|
||||
textUpdater.SetHeroText(heroNameLabel, value.NameTextId, "Hero");
|
||||
|
||||
// not ideal -- this is just so we don't show 0s for scouted heroes
|
||||
if (value.Strength != 0) {
|
||||
@@ -70,5 +72,9 @@ namespace eagle {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() { textUpdater?.Dispose(); }
|
||||
|
||||
private void OnDestroy() { Dispose(); }
|
||||
}
|
||||
}
|
||||
+11
-1
@@ -2,9 +2,19 @@ using TMPro;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace eagle {
|
||||
public class MovingArmyPopupRowController : TableRowController {
|
||||
public class MovingArmyPopupRowController : TableRowController, System.IDisposable {
|
||||
public TMP_Text HeroName;
|
||||
public RawImage BattalionImage;
|
||||
public TMP_Text BattalionSize;
|
||||
|
||||
private DynamicHeroTextUpdater textUpdater = new DynamicHeroTextUpdater();
|
||||
|
||||
public void SetHeroText(string nameTextId, string fallbackText) {
|
||||
textUpdater.SetHeroText(HeroName, nameTextId, fallbackText);
|
||||
}
|
||||
|
||||
public void Dispose() { textUpdater?.Dispose(); }
|
||||
|
||||
private void OnDestroy() { Dispose(); }
|
||||
}
|
||||
}
|
||||
+8
-2
@@ -5,7 +5,7 @@ using UnityEngine.Events;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace eagle {
|
||||
public class SelectedUnitRowController : TableRowController {
|
||||
public class SelectedUnitRowController : TableRowController, System.IDisposable {
|
||||
public RawImage professionImage;
|
||||
public RawImage archeryImage;
|
||||
public RawImage fireImage;
|
||||
@@ -19,6 +19,8 @@ namespace eagle {
|
||||
public RawImage armamentImage;
|
||||
public TMP_Text battalionNameLabel;
|
||||
|
||||
private DynamicHeroTextUpdater textUpdater = new DynamicHeroTextUpdater();
|
||||
|
||||
public HeroView Hero {
|
||||
set {
|
||||
archeryImage.gameObject.SetActive(false);
|
||||
@@ -31,7 +33,7 @@ namespace eagle {
|
||||
professionImage.texture =
|
||||
gameObject.GetComponentInParent<EagleCommonTextures>().Profession(
|
||||
value.Profession);
|
||||
heroNameLabel.text = value.Name;
|
||||
textUpdater.SetHeroText(heroNameLabel, value.NameTextId, "Hero");
|
||||
archeryImage.gameObject.SetActive(value.ArcheryCapable);
|
||||
fireImage.gameObject.SetActive(value.StartFireCapable);
|
||||
}
|
||||
@@ -107,5 +109,9 @@ namespace eagle {
|
||||
armamentImage.GetComponentInChildren<TableRowClickDetector>().selectable = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() { textUpdater?.Dispose(); }
|
||||
|
||||
private void OnDestroy() { Dispose(); }
|
||||
}
|
||||
}
|
||||
+183
-5
@@ -1,11 +1,14 @@
|
||||
using Net.Eagle0.Eagle.Common;
|
||||
using System;
|
||||
using Net.Eagle0.Eagle.Common;
|
||||
using Net.Eagle0.Eagle.Views;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using static Net.Eagle0.Eagle.Common.QuestDetails;
|
||||
using System.Linq;
|
||||
|
||||
namespace eagle {
|
||||
public class UnaffiliatedHeroRowController : TableRowController {
|
||||
public class UnaffiliatedHeroRowController : TableRowController, System.IDisposable {
|
||||
public RawImage professionImage;
|
||||
public TMP_Text heroNameLabel;
|
||||
public TMP_Text questLabel;
|
||||
@@ -18,6 +21,9 @@ namespace eagle {
|
||||
public RawImage archeryImage;
|
||||
public RawImage fireImage;
|
||||
|
||||
private DynamicHeroTextUpdater textUpdater = new DynamicHeroTextUpdater();
|
||||
private GeneratedTextListener questTextListener;
|
||||
|
||||
public void SetUnaffiliatedHero(UnaffiliatedHeroBasics uh, IGameModel model) {
|
||||
professionImage.texture =
|
||||
gameObject.GetComponentInParent<EagleCommonTextures>().Profession(
|
||||
@@ -27,12 +33,13 @@ namespace eagle {
|
||||
} else {
|
||||
professionImage.color = Color.white;
|
||||
}
|
||||
heroNameLabel.text = uh.Name;
|
||||
|
||||
// Use the NameTextId from UnaffiliatedHeroBasics
|
||||
textUpdater.SetHeroText(heroNameLabel, uh.NameTextId, "Hero");
|
||||
|
||||
switch (uh.RecruitmentInfo.Status) {
|
||||
case RecruitmentStatus.HasQuest:
|
||||
questLabel.text =
|
||||
DisplayNames.ShortQuestString(uh.RecruitmentInfo.Quest, model);
|
||||
SetQuestText(uh.RecruitmentInfo.Quest, model);
|
||||
break;
|
||||
default:
|
||||
questLabel.text = DisplayNames.ShortRecruitmentStatusString(uh.RecruitmentInfo);
|
||||
@@ -60,5 +67,176 @@ namespace eagle {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
textUpdater?.Dispose();
|
||||
if (questTextListener != null) {
|
||||
ClientTextProvider.Provider.RemoveListener(questTextListener);
|
||||
questTextListener = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy() { Dispose(); }
|
||||
|
||||
private void SetQuestText(Quest quest, IGameModel model) {
|
||||
if (quest == null) {
|
||||
questLabel.text = "There is nothing you can do";
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle the special case of DismissSpecificVassalQuest with dynamic text
|
||||
if (quest.Details.SealedValueCase == SealedValueOneofCase.DismissSpecificVassalQuest) {
|
||||
var targetHeroId = quest.Details.DismissSpecificVassalQuest.TargetHeroId;
|
||||
var targetHero = model.Heroes[targetHeroId];
|
||||
|
||||
// Check if the name is available immediately
|
||||
var textEntry = ClientTextProvider.Provider.GetTextEntry(targetHero.NameTextId);
|
||||
if (textEntry != null && !string.IsNullOrEmpty(textEntry.Text)) {
|
||||
questLabel.text = $"Dismiss {textEntry.Text}";
|
||||
} else {
|
||||
// Set fallback text and create listener for dynamic updates
|
||||
questLabel.text = "Dismiss vassal";
|
||||
|
||||
// Clean up any existing listener
|
||||
if (questTextListener != null) {
|
||||
ClientTextProvider.Provider.RemoveListener(questTextListener);
|
||||
}
|
||||
|
||||
// Create new listener that prefixes "Dismiss " to the hero name
|
||||
questTextListener = new GeneratedTextListener(
|
||||
targetHero.NameTextId,
|
||||
"",
|
||||
"",
|
||||
0,
|
||||
(index, heroName) => {
|
||||
if (questLabel != null) { questLabel.text = $"Dismiss {heroName}"; }
|
||||
});
|
||||
ClientTextProvider.Provider.AddListener(questTextListener);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// For all other quests, use the static method
|
||||
questLabel.text = ShortQuestString(quest, model);
|
||||
}
|
||||
|
||||
static string ShortQuestString(Quest quest, IGameModel model) {
|
||||
if (quest == null) { return $"There is nothing you can do"; }
|
||||
|
||||
switch (quest.Details.SealedValueCase) {
|
||||
case SealedValueOneofCase.AllianceQuest:
|
||||
return $"Secure an alliance with another faction";
|
||||
|
||||
case SealedValueOneofCase.DefeatFactionQuest:
|
||||
return $"Defeat {model.FactionName(quest.Details.DefeatFactionQuest.TargetFactionId)}";
|
||||
|
||||
case SealedValueOneofCase.AlmsToProvinceQuest: {
|
||||
var province = model.Provinces[quest.Details.AlmsToProvinceQuest.ProvinceId];
|
||||
var desiredFood = quest.Details.AlmsToProvinceQuest.TotalFood;
|
||||
var remainingFood = Math.Max(0, desiredFood - quest.ComponentsFulfilled);
|
||||
var remainingText =
|
||||
quest.ComponentsFulfilled > 0 ? $" ({remainingFood} remaining)" : "";
|
||||
return $"Give {desiredFood} food in alms in {province.Name}{remainingText}";
|
||||
}
|
||||
|
||||
case SealedValueOneofCase.AlmsAcrossRealmQuest: {
|
||||
var desiredFood = quest.Details.AlmsAcrossRealmQuest.TotalFood;
|
||||
var remainingFood = Math.Max(0, desiredFood - quest.ComponentsFulfilled);
|
||||
var remainingText =
|
||||
quest.ComponentsFulfilled > 0 ? $" ({remainingFood} remaining)" : "";
|
||||
return $"Give {desiredFood} food in alms across the realm{remainingText}";
|
||||
}
|
||||
|
||||
case SealedValueOneofCase.GiveToHeroesInProvinceQuest: {
|
||||
var province =
|
||||
model.Provinces[quest.Details.GiveToHeroesInProvinceQuest.ProvinceId];
|
||||
var desiredGold = quest.Details.GiveToHeroesInProvinceQuest.TotalGold;
|
||||
var remainingGold = Math.Max(0, desiredGold - quest.ComponentsFulfilled);
|
||||
var remainingText =
|
||||
quest.ComponentsFulfilled > 0 ? $" ({remainingGold} remaining)" : "";
|
||||
return $"Give {desiredGold} gold to heroes in {province.Name}{remainingText}";
|
||||
}
|
||||
|
||||
case SealedValueOneofCase.GiveToHeroesAcrossRealmQuest: {
|
||||
var desiredGold = quest.Details.GiveToHeroesAcrossRealmQuest.TotalGold;
|
||||
var remainingGold = Math.Max(0, desiredGold - quest.ComponentsFulfilled);
|
||||
var remainingText =
|
||||
quest.ComponentsFulfilled > 0 ? $" ({remainingGold} remaining)" : "";
|
||||
return $"Give {desiredGold} gold to heroes across the realm{remainingText}";
|
||||
}
|
||||
|
||||
case SealedValueOneofCase.ImproveAgricultureQuest:
|
||||
return $"Raise Agriculture to {(int)Math.Ceiling(quest.Details.ImproveAgricultureQuest.DesiredValue)}";
|
||||
|
||||
case SealedValueOneofCase.ImproveEconomyQuest:
|
||||
return $"Raise Economy to {(int)Math.Ceiling(quest.Details.ImproveEconomyQuest.DesiredValue)}";
|
||||
|
||||
case SealedValueOneofCase.ImproveInfrastructureQuest:
|
||||
return $"Raise Infrastructure to {(int)Math.Ceiling(quest.Details.ImproveInfrastructureQuest.DesiredValue)}";
|
||||
|
||||
case SealedValueOneofCase.SpecificExpansionQuest:
|
||||
return $"Expand to {model.Provinces[quest.Details.SpecificExpansionQuest.ProvinceId].Name}";
|
||||
|
||||
case SealedValueOneofCase.DismissSpecificVassalQuest:
|
||||
// This case is now handled by SetQuestText with dynamic updates
|
||||
return "Dismiss vassal";
|
||||
|
||||
case SealedValueOneofCase.WealthQuest:
|
||||
return $"Acquire {quest.Details.WealthQuest.Gold} gold and {quest.Details.WealthQuest.Food} food";
|
||||
|
||||
case SealedValueOneofCase.UpgradeBattalionQuest:
|
||||
return $"Raise a {DisplayNames.LongBattalionTypeNames[quest.Details.UpgradeBattalionQuest.BattalionTypeId]} to {(int)Math.Ceiling(quest.Details.UpgradeBattalionQuest.MinimumTraining)} training and {(int)Math.Ceiling(quest.Details.UpgradeBattalionQuest.MinimumArmament)} armament";
|
||||
|
||||
case SealedValueOneofCase.GrandArmyQuest:
|
||||
return $"Have {quest.Details.GrandArmyQuest.TotalTroopCount} troops";
|
||||
|
||||
case SealedValueOneofCase.TruceWithFactionQuest:
|
||||
return $"Sign a truce with {model.FactionName(quest.Details.TruceWithFactionQuest.TargetFactionId)}";
|
||||
|
||||
case SealedValueOneofCase.TruceCountQuest: {
|
||||
var currentTruceString = "";
|
||||
if (model.PlayerId is int fid) {
|
||||
var currentTruces = model.ActiveFactions[fid].FactionRelationships.Count(
|
||||
fr => fr.RelationshipLevel !=
|
||||
FactionRelationshipView.Types.RelationshipLevel.Hostile);
|
||||
if (currentTruces == 0) {
|
||||
currentTruceString = " (currently have none)";
|
||||
} else {
|
||||
currentTruceString = " (currently have " + currentTruces + ")";
|
||||
}
|
||||
}
|
||||
return $"Have truces with {quest.Details.TruceCountQuest.TruceCount} factions" +
|
||||
currentTruceString;
|
||||
}
|
||||
|
||||
case SealedValueOneofCase.ExecutePrisonerQuest: {
|
||||
var details = quest.Details.ExecutePrisonerQuest;
|
||||
var provinceName = model.Provinces[details.ProvinceId].Name;
|
||||
return $"Execute prisoner in {provinceName}";
|
||||
}
|
||||
|
||||
case SealedValueOneofCase.ExilePrisonerQuest: {
|
||||
var details = quest.Details.ExilePrisonerQuest;
|
||||
var provinceName = model.Provinces[details.ProvinceId].Name;
|
||||
return $"Exile prisoner from {provinceName}";
|
||||
}
|
||||
|
||||
case SealedValueOneofCase.FreePrisonerQuest: {
|
||||
var details = quest.Details.FreePrisonerQuest;
|
||||
var provinceName = model.Provinces[details.ProvinceId].Name;
|
||||
return $"Release prisoner from prison in {provinceName}";
|
||||
}
|
||||
|
||||
case SealedValueOneofCase.ReturnPrisonerQuest: {
|
||||
var details = quest.Details.ReturnPrisonerQuest;
|
||||
var provinceName = model.Provinces[details.ProvinceId].Name;
|
||||
var factionName = model.FactionName(details.ToFactionId);
|
||||
return $"Return prisoner from {provinceName} to {factionName}";
|
||||
}
|
||||
|
||||
default:
|
||||
return $"Crosby hasn't gotten to quest of type {quest.Details.SealedValueCase} yet";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
-2
@@ -3,22 +3,28 @@ using TMPro;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace eagle {
|
||||
public class UnitSelectorHeroRowController : TableRowController {
|
||||
public class UnitSelectorHeroRowController : TableRowController, System.IDisposable {
|
||||
public RawImage professionImage;
|
||||
public RawImage archeryImage;
|
||||
public RawImage fireImage;
|
||||
public TMP_Text heroNameLabel;
|
||||
|
||||
private DynamicHeroTextUpdater textUpdater = new DynamicHeroTextUpdater();
|
||||
|
||||
public HeroView Hero {
|
||||
set {
|
||||
professionImage.texture =
|
||||
gameObject.GetComponentInParent<EagleCommonTextures>().Profession(
|
||||
value.Profession);
|
||||
heroNameLabel.text = value.Name;
|
||||
textUpdater.SetHeroText(heroNameLabel, value.NameTextId, "Hero");
|
||||
archeryImage.gameObject.SetActive(value.ArcheryCapable);
|
||||
fireImage.gameObject.SetActive(value.StartFireCapable);
|
||||
professionImage.gameObject.SetActive(professionImage.texture != null);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() { textUpdater?.Dispose(); }
|
||||
|
||||
private void OnDestroy() { Dispose(); }
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ using Net.Eagle0.Eagle.Api;
|
||||
public class EagleConnection : IDisposable {
|
||||
public readonly string playerName;
|
||||
public readonly Metadata credentials;
|
||||
public readonly string authHeader;
|
||||
private const double KeepAliveSeconds = 45.0;
|
||||
|
||||
public static CancellationToken EagleCancellationToken =>
|
||||
@@ -44,6 +45,7 @@ public class EagleConnection : IDisposable {
|
||||
public EagleConnection(string playerName, string password, string url) {
|
||||
this.playerName = playerName;
|
||||
credentials = new Metadata { { "user", playerName } };
|
||||
authHeader = AuthInterceptor.HeaderValue(playerName, password);
|
||||
|
||||
EagleGrpcClient = new Eagle.EagleClient(MakeInvoker(playerName, password, url));
|
||||
}
|
||||
|
||||
+2
-2
@@ -191,7 +191,7 @@ namespace Shardok {
|
||||
unitsById[unit.CommandingUnitId.Value];
|
||||
nameLabel.text = $"[[{commandingUnit.Name ?? "??"}]]";
|
||||
if (!string.IsNullOrEmpty(commandingUnit.HeadshotPath)) {
|
||||
AWSResourceFetcher.headshotFetcher.LoadIntoRawImage(
|
||||
ResourceFetcher.headshotFetcher.LoadIntoRawImage(
|
||||
headshotImage,
|
||||
commandingUnit.HeadshotPath);
|
||||
}
|
||||
@@ -205,7 +205,7 @@ namespace Shardok {
|
||||
} else {
|
||||
HeroView hero = unit.AttachedHero;
|
||||
if (!string.IsNullOrEmpty(unit.HeadshotPath)) {
|
||||
AWSResourceFetcher.headshotFetcher.LoadIntoRawImage(
|
||||
ResourceFetcher.headshotFetcher.LoadIntoRawImage(
|
||||
headshotImage,
|
||||
unit.HeadshotPath);
|
||||
}
|
||||
|
||||
+36
-43
@@ -1,32 +1,35 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using eagle;
|
||||
using Google.Protobuf;
|
||||
using Net.Eagle0.Eagle.Api;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace common {
|
||||
public class AWSResourceFetcher {
|
||||
public static AWSResourceFetcher headshotFetcher = null;
|
||||
public class ResourceFetcher {
|
||||
public static ResourceFetcher headshotFetcher = null;
|
||||
|
||||
public static void SetUpConnection(PersistentClientConnection persistentConnection) {
|
||||
headshotFetcher = new("headshots", persistentConnection);
|
||||
public static void SetUpConnection(HttpClient httpClient) {
|
||||
headshotFetcher = new("headshots", httpClient);
|
||||
}
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
private readonly PersistentClientConnection _persistentClientConnection;
|
||||
private static readonly Uri BaseUri = new("https://eagle0.net/assets/headshots/");
|
||||
|
||||
AWSResourceFetcher(
|
||||
string relativeLocalPath,
|
||||
PersistentClientConnection persistentClientConnection) {
|
||||
ResourceFetcher(string relativeLocalPath, HttpClient httpClient) {
|
||||
_localPath = Path.Combine(
|
||||
Application.persistentDataPath,
|
||||
"eagle0",
|
||||
"Resources",
|
||||
relativeLocalPath);
|
||||
Directory.CreateDirectory(_localPath);
|
||||
_persistentClientConnection = persistentClientConnection;
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
private readonly string _localPath;
|
||||
@@ -59,12 +62,14 @@ namespace common {
|
||||
}
|
||||
}
|
||||
|
||||
private void FetchRemote(string path) {
|
||||
var headshotRequest = new HeadshotRequest();
|
||||
headshotRequest.Paths.Add(new HeadshotRequest.Types.SingleRequest { Path = path });
|
||||
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)
|
||||
});
|
||||
|
||||
_persistentClientConnection.SendUpdateStreamRequestAsync(
|
||||
new UpdateStreamRequest { HeadshotRequest = headshotRequest });
|
||||
return await ReceiveHeadshot(response.Content, path);
|
||||
}
|
||||
|
||||
private class ImageLoadQueueItem {
|
||||
@@ -74,24 +79,23 @@ namespace common {
|
||||
|
||||
readonly List<ImageLoadQueueItem> _imageLoadQueue = new();
|
||||
|
||||
public void ReceiveHeadshotResponse(HeadshotResponse response) {
|
||||
foreach (var singleResponse in response.HeadshotResponses) {
|
||||
SaveLocal(singleResponse.Path, singleResponse.ImageData);
|
||||
private async Task<bool> ReceiveHeadshot(HttpContent response, string path) {
|
||||
var buf = await response.ReadAsByteArrayAsync();
|
||||
SaveLocal(path, ByteString.CopyFrom(buf));
|
||||
|
||||
lock (_imageLoadQueue) {
|
||||
if (_imageLoadQueue.Count > 0) {
|
||||
var elt =
|
||||
_imageLoadQueue.FirstOrDefault(x => x.path == singleResponse.Path);
|
||||
lock (_imageLoadQueue) {
|
||||
if (_imageLoadQueue.Count > 0) {
|
||||
var elt = _imageLoadQueue.FirstOrDefault(x => x.path == path);
|
||||
|
||||
if (elt != null) {
|
||||
_imageLoadQueue.Remove(elt);
|
||||
if (elt != null) {
|
||||
_imageLoadQueue.Remove(elt);
|
||||
|
||||
MainQueue.Q.Enqueue(
|
||||
() => LoadIntoRawImage(elt.image, singleResponse.Path));
|
||||
}
|
||||
MainQueue.Q.Enqueue(() => LoadIntoRawImage(elt.image, path));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// This must be called from the main thread
|
||||
@@ -126,26 +130,15 @@ namespace common {
|
||||
if (pathToFetch != null) { FetchRemote(pathToFetch); }
|
||||
}
|
||||
|
||||
private void PrefetchBatch(IEnumerable<string> paths) {
|
||||
var request = new HeadshotRequest();
|
||||
request.Paths.AddRange(
|
||||
paths.Select(path => new HeadshotRequest.Types.SingleRequest { Path = path }));
|
||||
|
||||
_persistentClientConnection.SendUpdateStreamRequestAsync(
|
||||
new UpdateStreamRequest { HeadshotRequest = request });
|
||||
}
|
||||
|
||||
const int MaxFetchCount = 10;
|
||||
public void Prefetch(IEnumerable<string> paths) {
|
||||
var toFetch = paths.Where(path => !File.Exists(FullLocalPath(path))).ToList();
|
||||
|
||||
if (toFetch.Any()) {
|
||||
if (toFetch.Count() <= MaxFetchCount) {
|
||||
PrefetchBatch(toFetch);
|
||||
} else {
|
||||
PrefetchBatch(toFetch.Take(MaxFetchCount));
|
||||
Prefetch(toFetch.Skip(MaxFetchCount));
|
||||
foreach (var path in toFetch) {
|
||||
if (string.IsNullOrEmpty(path)) {
|
||||
Debug.LogWarning("ResourceFetcher: Attempted to prefetch an empty path.");
|
||||
}
|
||||
|
||||
FetchRemote(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
# Eagle0 Installer Credential System
|
||||
|
||||
The installer has been updated to use dynamic user credentials instead of hardcoded authentication. This improves security and provides a better user experience.
|
||||
|
||||
## Key Features
|
||||
|
||||
### 1. **Dynamic Credential Management**
|
||||
- **No hardcoded passwords** in configuration files
|
||||
- **User-prompted login** when credentials are needed
|
||||
- **Secure credential storage** using Windows Data Protection API
|
||||
- **Automatic credential sharing** with Unity client
|
||||
|
||||
### 2. **User Experience**
|
||||
- **Saved credentials** are reused automatically
|
||||
- **Login dialog** appears only when needed
|
||||
- **Remember credentials** option for convenience
|
||||
- **401 error handling** with automatic re-prompting
|
||||
|
||||
### 3. **Security Features**
|
||||
- **Encrypted password storage** using DPAPI (user-scoped)
|
||||
- **Registry storage** for username/server (non-sensitive)
|
||||
- **Automatic credential clearing** on authentication failure
|
||||
- **Unity client compatibility** for shared login state
|
||||
|
||||
## How It Works
|
||||
|
||||
### **First Run Flow**
|
||||
1. User launches installer
|
||||
2. No saved credentials found
|
||||
3. Login dialog appears
|
||||
4. User enters username/password/server
|
||||
5. Credentials saved securely (if requested)
|
||||
6. Installer proceeds with updates
|
||||
|
||||
### **Subsequent Runs**
|
||||
1. User launches installer
|
||||
2. Saved credentials loaded automatically
|
||||
3. Installer proceeds with updates
|
||||
4. No user interaction needed
|
||||
|
||||
### **Authentication Failure Flow**
|
||||
1. Installer detects 401 Unauthorized response
|
||||
2. Saved credentials cleared automatically
|
||||
3. Login dialog appears with error message
|
||||
4. User enters corrected credentials
|
||||
5. Installer retries with new credentials
|
||||
|
||||
### **Changing Valid Credentials**
|
||||
Users can change working credentials through multiple methods:
|
||||
|
||||
**Method 1: Interactive Prompt (Default)**
|
||||
1. Launch installer normally
|
||||
2. See message: "Using saved credentials for user: john@example.com"
|
||||
3. Press 'C' within 3 seconds to change credentials
|
||||
4. Login dialog appears with current credentials pre-filled
|
||||
|
||||
**Method 2: Command Line Flag**
|
||||
```bash
|
||||
EagleInstaller.exe --login
|
||||
EagleInstaller.exe --change-credentials
|
||||
```
|
||||
|
||||
**Method 3: Clear and Re-enter**
|
||||
```bash
|
||||
EagleInstaller.exe --clear-credentials
|
||||
EagleInstaller.exe # Next run will prompt for new credentials
|
||||
```
|
||||
|
||||
### **Credential Sharing with Unity Client**
|
||||
- Launcher saves credentials in Unity PlayerPrefs format
|
||||
- Unity client can access same username/password/server
|
||||
- Seamless transition from launcher to game
|
||||
|
||||
## File Structure
|
||||
|
||||
### **New Files Added**
|
||||
- `CredentialManager.cs` - Secure credential storage/retrieval
|
||||
- `LoginDialog.cs` - Windows Forms login dialog
|
||||
|
||||
### **Modified Files**
|
||||
- `Program.cs` - Integrated credential prompting and error handling
|
||||
- `EagleUpdater.cs` - Dynamic authentication and 401 error detection
|
||||
- `EagleInstaller.csproj` - Added Windows Forms support
|
||||
- `configuration.txt` - Removed hardcoded credentials
|
||||
|
||||
### **Credential Storage Locations**
|
||||
- **Username/Server**: `HKCU\SOFTWARE\Eagle0\Launcher`
|
||||
- **Password**: `%LocalAppData%\eagle0\eagle0_credentials.dat` (encrypted)
|
||||
- **Unity Sharing**: `HKCU\SOFTWARE\Unity\UnityEditor\CompanyName\ProductName`
|
||||
|
||||
## Configuration
|
||||
|
||||
The `configuration.txt` file now only contains non-sensitive settings:
|
||||
|
||||
```
|
||||
manifest_name = eagle0WIN_manifest.txt
|
||||
remote_manifest_prefix = assets/unity3d/
|
||||
remote_asset_prefix = assets/unity3d/win/
|
||||
```
|
||||
|
||||
Server URL, username, and password are now provided by the user through the login dialog.
|
||||
|
||||
## Command Line Options
|
||||
|
||||
The installer supports several command line flags for credential management:
|
||||
|
||||
```bash
|
||||
EagleInstaller.exe # Normal operation
|
||||
EagleInstaller.exe --help # Show help message
|
||||
EagleInstaller.exe --login # Force credential dialog
|
||||
EagleInstaller.exe --change-credentials # Same as --login
|
||||
EagleInstaller.exe --clear-credentials # Clear saved credentials
|
||||
```
|
||||
|
||||
During normal operation, users see a 3-second prompt to change credentials:
|
||||
```
|
||||
Using saved credentials for user: john@example.com
|
||||
Press 'C' within 3 seconds to change credentials, or any other key to continue...
|
||||
```
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Security**: No more plaintext passwords in config files
|
||||
2. **Flexibility**: Users can connect to different servers/accounts
|
||||
3. **User Experience**: Credentials remembered between sessions
|
||||
4. **Easy Changes**: Multiple ways to update credentials
|
||||
5. **Error Recovery**: Automatic handling of expired/invalid credentials
|
||||
6. **Integration**: Shared credentials with Unity game client
|
||||
7. **Self-Updating**: Maintains credential system through installer updates
|
||||
|
||||
## Technical Implementation
|
||||
|
||||
### **Two-Stage Approach**
|
||||
The installer maintains both the two-stage installer update system AND the new credential system:
|
||||
|
||||
**Stage 1**: Check installer version (with authentication)
|
||||
**Stage 2**: Update game files (with same credentials)
|
||||
|
||||
Both stages use the same credential system and handle 401 errors consistently.
|
||||
|
||||
### **Error Handling**
|
||||
- `AuthenticationFailedException` for 401 responses
|
||||
- Automatic credential retry loop
|
||||
- Graceful fallback on credential prompt cancellation
|
||||
- Backup/restore for failed credential saves
|
||||
|
||||
This system provides enterprise-grade credential management while maintaining the simple user experience of the original installer.
|
||||
@@ -0,0 +1,144 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace EagleInstaller {
|
||||
public class Credentials {
|
||||
public string Username { get; set; }
|
||||
public string Password { get; set; }
|
||||
public string ServerUrl { get; set; }
|
||||
}
|
||||
|
||||
public class CredentialManager {
|
||||
private const string RegistryKeyPath = @"SOFTWARE\Eagle0\Launcher";
|
||||
private const string UsernameValueName = "Username";
|
||||
private const string ServerUrlValueName = "ServerUrl";
|
||||
private const string CredentialFileName = "eagle0_credentials.dat";
|
||||
|
||||
private static readonly string CredentialFilePath = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"eagle0",
|
||||
CredentialFileName);
|
||||
|
||||
/// <summary>
|
||||
/// Loads saved credentials. Returns null if no valid credentials are found.
|
||||
/// </summary>
|
||||
public static Credentials LoadCredentials() {
|
||||
try {
|
||||
// Try to load from registry (username and server URL)
|
||||
string username = null;
|
||||
string serverUrl = null;
|
||||
|
||||
using (var key = Registry.CurrentUser.OpenSubKey(RegistryKeyPath, false)) {
|
||||
if (key != null) {
|
||||
username = key.GetValue(UsernameValueName) as string;
|
||||
serverUrl = key.GetValue(ServerUrlValueName) as string;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(username)) { return null; }
|
||||
|
||||
// Try to load encrypted password from file
|
||||
string password = LoadEncryptedPassword();
|
||||
if (string.IsNullOrEmpty(password)) { return null; }
|
||||
|
||||
return new Credentials {
|
||||
Username = username,
|
||||
Password = password,
|
||||
ServerUrl = serverUrl ?? "https://eagle0.net"
|
||||
};
|
||||
} catch (Exception) {
|
||||
// If anything fails, return null to prompt for credentials
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves credentials to registry (username/server) and encrypted file (password).
|
||||
/// </summary>
|
||||
public static void SaveCredentials(Credentials credentials) {
|
||||
try {
|
||||
// Save username and server URL to registry
|
||||
using (var key = Registry.CurrentUser.CreateSubKey(RegistryKeyPath)) {
|
||||
key.SetValue(UsernameValueName, credentials.Username);
|
||||
key.SetValue(ServerUrlValueName, credentials.ServerUrl);
|
||||
}
|
||||
|
||||
// Save encrypted password to file
|
||||
SaveEncryptedPassword(credentials.Password);
|
||||
} catch (Exception ex) {
|
||||
throw new Exception($"Failed to save credentials: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all saved credentials.
|
||||
/// </summary>
|
||||
public static void ClearCredentials() {
|
||||
try {
|
||||
// Clear registry entries
|
||||
using (var key = Registry.CurrentUser.OpenSubKey(RegistryKeyPath, true)) {
|
||||
if (key != null) {
|
||||
key.DeleteValue(UsernameValueName, false);
|
||||
key.DeleteValue(ServerUrlValueName, false);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete password file
|
||||
if (File.Exists(CredentialFilePath)) { File.Delete(CredentialFilePath); }
|
||||
} catch (Exception) {
|
||||
// Ignore errors when clearing
|
||||
}
|
||||
}
|
||||
|
||||
private static string LoadEncryptedPassword() {
|
||||
try {
|
||||
if (!File.Exists(CredentialFilePath)) { return null; }
|
||||
|
||||
byte[] encryptedData = File.ReadAllBytes(CredentialFilePath);
|
||||
byte[] decryptedData = ProtectedData.Unprotect(
|
||||
encryptedData,
|
||||
null,
|
||||
DataProtectionScope.CurrentUser);
|
||||
return Encoding.UTF8.GetString(decryptedData);
|
||||
} catch (Exception) { return null; }
|
||||
}
|
||||
|
||||
private static void SaveEncryptedPassword(string password) {
|
||||
try {
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(CredentialFilePath));
|
||||
|
||||
byte[] passwordBytes = Encoding.UTF8.GetBytes(password);
|
||||
byte[] encryptedData =
|
||||
ProtectedData.Protect(passwordBytes, null, DataProtectionScope.CurrentUser);
|
||||
File.WriteAllBytes(CredentialFilePath, encryptedData);
|
||||
} catch (Exception ex) {
|
||||
throw new Exception($"Failed to save encrypted password: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves credentials in the same format used by the Unity client for compatibility.
|
||||
/// </summary>
|
||||
public static void SaveCredentialsForUnityClient(Credentials credentials) {
|
||||
try {
|
||||
// Unity uses PlayerPrefs which on Windows maps to registry entries
|
||||
// We'll save to the same location the Unity client expects
|
||||
const string unityRegistryPath =
|
||||
@"SOFTWARE\Unity\UnityEditor\CompanyName\ProductName";
|
||||
|
||||
using (var key = Registry.CurrentUser.CreateSubKey(unityRegistryPath)) {
|
||||
key.SetValue("urlKey", credentials.ServerUrl);
|
||||
key.SetValue("nameKey", credentials.Username);
|
||||
key.SetValue("passwordKey", credentials.Password);
|
||||
}
|
||||
} catch (Exception) {
|
||||
// If Unity credential sharing fails, continue anyway
|
||||
// The main launcher credentials are still saved
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+9
-5
@@ -2,10 +2,16 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
<PublishTrimmed>false</PublishTrimmed>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
|
||||
<PublishReadyToRun>true</PublishReadyToRun>
|
||||
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
||||
<IncludeAllContentForSelfExtract>true</IncludeAllContentForSelfExtract>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<EnableWindowsTargeting>true</EnableWindowsTargeting>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -13,8 +19,6 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Remove="configuration.txt" />
|
||||
<Content Include="configuration.txt">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<EmbeddedResource Include="configuration.txt" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Reflection;
|
||||
@@ -9,30 +10,36 @@ using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
|
||||
namespace EagleInstaller {
|
||||
internal class UpdaterFailureException : Exception {
|
||||
public UpdaterFailureException(string message) : base(message) {}
|
||||
internal class UpdaterFailureException
|
||||
(string message) : Exception(message);
|
||||
|
||||
internal class AuthenticationFailedException
|
||||
(string message) : Exception(message);
|
||||
|
||||
public class InstallerUpdateInfo {
|
||||
public bool InstallerNeedsUpdate { get; set; }
|
||||
public string NewInstallerVersion { get; set; }
|
||||
public string NewInstallerUrl { get; set; }
|
||||
}
|
||||
|
||||
class EagleUpdater {
|
||||
private static readonly Dictionary<string, string> ConfigFileEntries = ReadConfiguration();
|
||||
private static readonly Uri ServiceUrl = new Uri(ConfigFileEntries["service_url"]);
|
||||
private const string CurrentInstallerVersion = "1.0.0";
|
||||
|
||||
private static readonly Dictionary<string, string> ConfigFileEntries = ReadConfiguration();
|
||||
private static readonly string ManifestName = ConfigFileEntries["manifest_name"];
|
||||
private static readonly string RemoteManifestName =
|
||||
ConfigFileEntries["remote_manifest_prefix"] + ManifestName;
|
||||
|
||||
private const int DownloadSlots = 4;
|
||||
private const int RetryCount = 3;
|
||||
|
||||
private static readonly string Username = ConfigFileEntries["username"];
|
||||
|
||||
private static readonly string Password = ConfigFileEntries["password"];
|
||||
|
||||
private static readonly string RemotePrefix = ConfigFileEntries["remote_asset_prefix"];
|
||||
|
||||
private readonly HttpClient _httpClient;
|
||||
private const int DownloadSlots = 8;
|
||||
private const int RetryCount = 3;
|
||||
|
||||
private static Semaphore _pool = new Semaphore(DownloadSlots, DownloadSlots);
|
||||
private readonly HttpClient _httpClient;
|
||||
private static readonly SemaphoreSlim Pool = new(DownloadSlots, DownloadSlots);
|
||||
|
||||
// Current credentials and service URL
|
||||
private Credentials CurrentCredentials { get; set; }
|
||||
private Uri ServiceUrl { get; set; }
|
||||
|
||||
private struct FetchAttempt {
|
||||
public bool Success;
|
||||
@@ -40,10 +47,11 @@ namespace EagleInstaller {
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> ReadConfiguration() {
|
||||
string path = Path.Combine(
|
||||
Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location),
|
||||
@"configuration.txt");
|
||||
var configText = File.ReadAllText(path);
|
||||
var assembly = System.Reflection.Assembly.GetExecutingAssembly();
|
||||
using var stream =
|
||||
assembly.GetManifestResourceStream("EagleInstaller.configuration.txt");
|
||||
using var reader = new StreamReader(stream);
|
||||
var configText = reader.ReadToEnd();
|
||||
|
||||
if (string.IsNullOrEmpty(configText)) { return new Dictionary<string, string>(); }
|
||||
|
||||
@@ -59,10 +67,27 @@ namespace EagleInstaller {
|
||||
return dict;
|
||||
}
|
||||
|
||||
public EagleUpdater()
|
||||
{
|
||||
public EagleUpdater(Credentials credentials) {
|
||||
CurrentCredentials =
|
||||
credentials ?? throw new ArgumentNullException(nameof(credentials));
|
||||
ServiceUrl = new Uri(CurrentCredentials.ServerUrl);
|
||||
|
||||
_httpClient = new HttpClient();
|
||||
_httpClient.DefaultRequestHeaders.Authorization = AuthenticationHeader(Username, Password);
|
||||
SetAuthentication(CurrentCredentials.Username, CurrentCredentials.Password);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the authentication credentials for future requests.
|
||||
/// </summary>
|
||||
public void UpdateCredentials(Credentials newCredentials) {
|
||||
CurrentCredentials = newCredentials;
|
||||
ServiceUrl = new Uri(CurrentCredentials.ServerUrl);
|
||||
SetAuthentication(CurrentCredentials.Username, CurrentCredentials.Password);
|
||||
}
|
||||
|
||||
private void SetAuthentication(string username, string password) {
|
||||
_httpClient.DefaultRequestHeaders.Authorization =
|
||||
AuthenticationHeader(username, password);
|
||||
}
|
||||
|
||||
private List<string> Discrepancies(
|
||||
@@ -75,17 +100,53 @@ namespace EagleInstaller {
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task<InstallerUpdateInfo> CheckForInstallerUpdate() {
|
||||
try {
|
||||
Logger.WriteLine("Checking for installer updates...");
|
||||
string manifestText = await RemoteManifestText();
|
||||
|
||||
// Parse installer version from manifest comments
|
||||
var lines = manifestText.Split('\n');
|
||||
var versionLine =
|
||||
lines.FirstOrDefault(l => l.TrimStart().StartsWith("# installer_version="));
|
||||
var urlLine =
|
||||
lines.FirstOrDefault(l => l.TrimStart().StartsWith("# installer_url="));
|
||||
|
||||
if (versionLine != null && urlLine != null) {
|
||||
var remoteVersion = versionLine.Split('=') [1].Trim();
|
||||
var installerUrl = urlLine.Split('=') [1].Trim();
|
||||
|
||||
if (remoteVersion != CurrentInstallerVersion) {
|
||||
Logger.WriteLine(
|
||||
$"New installer version available: {remoteVersion} (current: {CurrentInstallerVersion})");
|
||||
return new InstallerUpdateInfo {
|
||||
InstallerNeedsUpdate = true,
|
||||
NewInstallerVersion = remoteVersion,
|
||||
NewInstallerUrl = installerUrl
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Logger.WriteLine("Installer is up to date");
|
||||
return new InstallerUpdateInfo { InstallerNeedsUpdate = false };
|
||||
} catch (Exception e) {
|
||||
Logger.WriteError($"Failed to check for installer updates: {e.Message}");
|
||||
// If we can't check for updates, assume no update needed
|
||||
return new InstallerUpdateInfo { InstallerNeedsUpdate = false };
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> MaybePerformUpdateWithRetries(string localPath) {
|
||||
string existingManifestPath = Path.Combine(localPath, ManifestName);
|
||||
|
||||
Console.WriteLine("Loading old manifest");
|
||||
Logger.UpdateStatus("Loading old manifest...");
|
||||
string existingText = await LocalManifestText(existingManifestPath);
|
||||
|
||||
Console.WriteLine("Fetching new manifest...");
|
||||
Logger.UpdateStatus("Fetching new manifest...");
|
||||
string newText = await RemoteManifestText();
|
||||
|
||||
if (existingText != null && existingText.Equals(newText)) {
|
||||
Console.WriteLine("We already match!");
|
||||
Logger.UpdateStatus("Files are up to date!");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -95,34 +156,33 @@ namespace EagleInstaller {
|
||||
List<string> discrepancies = Discrepancies(desiredFiles, currentFiles);
|
||||
|
||||
for (int retry = 0; retry < RetryCount; retry++) {
|
||||
Console.WriteLine($"Attempt {retry + 1}");
|
||||
Logger.UpdateStatus($"Update attempt {retry + 1} of {RetryCount}...");
|
||||
var newFiles = await AttemptUpdate(localPath, desiredFiles, currentFiles);
|
||||
|
||||
List<string> newDiscrepancies = Discrepancies(desiredFiles, newFiles);
|
||||
if (newDiscrepancies.Count == 0) {
|
||||
Console.WriteLine(
|
||||
$"Current state matches manifest. Writing downloaded manifest...");
|
||||
Logger.UpdateStatus("Update completed successfully!");
|
||||
Logger.WriteLine("Writing downloaded manifest...");
|
||||
File.WriteAllText(existingManifestPath, newText);
|
||||
return true;
|
||||
} else {
|
||||
Console.WriteLine("There are still some discrepancies.");
|
||||
Logger.WriteLine("There are still some discrepancies.");
|
||||
if (newDiscrepancies.Count == discrepancies.Count) {
|
||||
Console.WriteLine("We don't seem to be making progress.");
|
||||
Logger.WriteLine("We don't seem to be making progress.");
|
||||
} else {
|
||||
Console.WriteLine(
|
||||
"We seem to have made some progress. Discrepancies are now:");
|
||||
newDiscrepancies.ForEach(s => Console.WriteLine($" {s}"));
|
||||
Logger.WriteLine("Some progress made. Remaining discrepancies:");
|
||||
newDiscrepancies.ForEach(s => Logger.WriteLine($" {s}"));
|
||||
discrepancies = newDiscrepancies;
|
||||
}
|
||||
|
||||
Console.WriteLine("Writing out the current state.");
|
||||
Logger.WriteLine("Writing out the current state.");
|
||||
File.WriteAllText(existingManifestPath, TextFromShas(newFiles));
|
||||
currentFiles = newFiles;
|
||||
}
|
||||
}
|
||||
|
||||
// sadness
|
||||
Console.WriteLine($"Exhausted {RetryCount} attempts. Giving up.");
|
||||
Logger.WriteError($"Exhausted {RetryCount} attempts. Update failed.");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -138,7 +198,7 @@ namespace EagleInstaller {
|
||||
|
||||
IEnumerable<Task<FetchAttempt>> updateTasks =
|
||||
pathsToUpdate.Select(remotePathAndSha => {
|
||||
Console.WriteLine($"Need to update file {remotePathAndSha.Key}");
|
||||
Logger.WriteLine($"Downloading: {remotePathAndSha.Key}");
|
||||
return FetchAndWriteOne(
|
||||
remotePathAndSha.Key,
|
||||
localPath,
|
||||
@@ -151,7 +211,7 @@ namespace EagleInstaller {
|
||||
|
||||
pathsToDelete.ToList().ForEach(path => {
|
||||
var localDeletePath = WritePathFromRemotePath(path, localPath);
|
||||
Console.WriteLine($"Deleting {localDeletePath}...");
|
||||
Logger.WriteLine($"Deleting: {localDeletePath}");
|
||||
File.Delete(localDeletePath);
|
||||
existingFiles.Remove(path);
|
||||
});
|
||||
@@ -162,7 +222,7 @@ namespace EagleInstaller {
|
||||
existingFiles[newItem.Key].Equals(newItem.Value))
|
||||
select newItem.Key;
|
||||
|
||||
pathsOk.ToList().ForEach(path => Console.WriteLine($"Up to date at {path}"));
|
||||
pathsOk.ToList().ForEach(path => Logger.WriteLine($"Up to date: {path}"));
|
||||
|
||||
var fetchAttemptResults = await Task.WhenAll(updateTasks);
|
||||
|
||||
@@ -173,11 +233,11 @@ namespace EagleInstaller {
|
||||
.ForEach(fa => existingFiles[fa.Key] = fa.Value);
|
||||
|
||||
if (fetchAttemptResults.ToList().TrueForAll(fa => fa.Success)) {
|
||||
Console.WriteLine("All download tasks succeeded.");
|
||||
Logger.WriteLine("All downloads completed successfully.");
|
||||
} else {
|
||||
Console.WriteLine("Failed to fetch all files!");
|
||||
Logger.WriteError("Some file downloads failed!");
|
||||
}
|
||||
} catch (Exception e) { Console.WriteLine($"Caught unexpected exception {e}"); }
|
||||
} catch (Exception e) { Logger.WriteError($"Unexpected exception: {e}"); }
|
||||
return existingFiles;
|
||||
}
|
||||
|
||||
@@ -199,6 +259,13 @@ namespace EagleInstaller {
|
||||
async Task<string> RemoteManifestText() {
|
||||
using HttpResponseMessage response =
|
||||
await _httpClient.GetAsync(new Uri(ServiceUrl, RemoteManifestName));
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.Unauthorized) {
|
||||
throw new AuthenticationFailedException(
|
||||
"Authentication failed while fetching manifest. Invalid username or password.");
|
||||
}
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
await using Stream responseStream = await response.Content.ReadAsStreamAsync();
|
||||
using StreamReader reader = new StreamReader(responseStream);
|
||||
return await reader.ReadToEndAsync();
|
||||
@@ -234,26 +301,31 @@ namespace EagleInstaller {
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(writePath));
|
||||
|
||||
_pool.WaitOne();
|
||||
await Pool.WaitAsync();
|
||||
|
||||
try {
|
||||
using HttpResponseMessage response =
|
||||
await _httpClient.GetAsync(new Uri(ServiceUrl, RemotePrefix + remotePath));
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.Unauthorized) {
|
||||
throw new AuthenticationFailedException(
|
||||
$"Authentication failed while downloading {remotePath}. Invalid username or password.");
|
||||
}
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
await using Stream responseStream = await response.Content.ReadAsStreamAsync();
|
||||
await using FileStream fileStream = File.Create(writePath);
|
||||
using MemoryStream memStream = new MemoryStream();
|
||||
await responseStream.CopyToAsync(memStream);
|
||||
memStream.Position = 0;
|
||||
|
||||
_pool.Release();
|
||||
|
||||
var sha = System.Security.Cryptography.SHA256.Create();
|
||||
string computedSha = BitConverter.ToString(sha.ComputeHash(memStream))
|
||||
.Replace("-", string.Empty)
|
||||
.ToLower();
|
||||
|
||||
if (computedSha.Equals(expectedSha)) {
|
||||
Console.WriteLine($"Verified SHA256 for {remotePath}; writing...");
|
||||
Logger.WriteLine($"✓ Verified and saved: {remotePath}");
|
||||
memStream.Position = 0;
|
||||
await memStream.CopyToAsync(fileStream);
|
||||
return new FetchAttempt {
|
||||
@@ -261,18 +333,67 @@ namespace EagleInstaller {
|
||||
Kvp = new KeyValuePair<String, String>(remotePath, computedSha)
|
||||
};
|
||||
} else {
|
||||
Console.WriteLine($"SHAs do not match for {remotePath}");
|
||||
Logger.WriteError($"✗ SHA mismatch for {remotePath}");
|
||||
return new FetchAttempt() { Success = false };
|
||||
}
|
||||
} catch (TaskCanceledException e) {
|
||||
Console.WriteLine($"Caught ObjectDisposedException {e}");
|
||||
Logger.WriteError($"Download cancelled: {e.Message}");
|
||||
return new FetchAttempt() { Success = false };
|
||||
} catch (HttpRequestException e) {
|
||||
Console.WriteLine($"Caught HttpRequestException {e}");
|
||||
Logger.WriteError($"Network error: {e.Message}");
|
||||
return new FetchAttempt() { Success = false };
|
||||
} catch (Exception e) {
|
||||
Console.WriteLine($"Caught Exception {e}");
|
||||
Logger.WriteError($"Download failed: {e.Message}");
|
||||
return new FetchAttempt() { Success = false };
|
||||
} finally { Pool.Release(); }
|
||||
}
|
||||
|
||||
public async Task<bool> DownloadAndLaunchNewInstaller(string installerUrl) {
|
||||
try {
|
||||
Logger.UpdateStatus("Downloading new installer...");
|
||||
|
||||
// Create temp directory for new installer
|
||||
string tempDir = Path.Combine(Path.GetTempPath(), "eagle0_installer_update");
|
||||
Directory.CreateDirectory(tempDir);
|
||||
|
||||
string newInstallerPath = Path.Combine(tempDir, "EagleInstaller_new.exe");
|
||||
|
||||
// Download new installer
|
||||
using HttpResponseMessage response =
|
||||
await _httpClient.GetAsync(new Uri(ServiceUrl, installerUrl));
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.Unauthorized) {
|
||||
throw new AuthenticationFailedException(
|
||||
"Authentication failed while downloading new installer. Invalid username or password.");
|
||||
}
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
await using Stream responseStream = await response.Content.ReadAsStreamAsync();
|
||||
await using FileStream fileStream = File.Create(newInstallerPath);
|
||||
await responseStream.CopyToAsync(fileStream);
|
||||
|
||||
Logger.WriteLine($"New installer downloaded successfully");
|
||||
|
||||
// Get current installer path
|
||||
string currentInstallerPath =
|
||||
Environment.ProcessPath ??
|
||||
Path.Combine(AppContext.BaseDirectory, "EagleInstaller.exe");
|
||||
|
||||
// Launch new installer with replacement arguments
|
||||
var processInfo = new System.Diagnostics.ProcessStartInfo {
|
||||
FileName = newInstallerPath,
|
||||
Arguments = $"--replace-installer \"{currentInstallerPath}\"",
|
||||
UseShellExecute = true
|
||||
};
|
||||
|
||||
Logger.UpdateStatus("Launching new installer...");
|
||||
System.Diagnostics.Process.Start(processInfo);
|
||||
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
Logger.WriteError($"Failed to download or launch new installer: {e}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
|
||||
namespace EagleInstaller {
|
||||
public static class Logger {
|
||||
private static MainForm _mainForm;
|
||||
|
||||
public static void Initialize(MainForm mainForm) { _mainForm = mainForm; }
|
||||
|
||||
public static void WriteLine(string message) {
|
||||
// Write to console (for when running from command line)
|
||||
Console.WriteLine(message);
|
||||
|
||||
// Also write to GUI if available
|
||||
if (_mainForm != null) { _mainForm.AppendProgress(message); }
|
||||
}
|
||||
|
||||
public static void UpdateStatus(string status) {
|
||||
// Write to console
|
||||
Console.WriteLine($"STATUS: {status}");
|
||||
|
||||
// Update GUI status if available
|
||||
if (_mainForm != null) { _mainForm.UpdateStatus(status); }
|
||||
}
|
||||
|
||||
public static void WriteError(string message) { WriteLine($"ERROR: {message}"); }
|
||||
|
||||
public static void WriteWarning(string message) { WriteLine($"WARNING: {message}"); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace EagleInstaller {
|
||||
public partial class LoginDialog : Form {
|
||||
public Credentials Credentials { get; private set; }
|
||||
public bool SaveCredentials { get; private set; }
|
||||
|
||||
private TextBox usernameTextBox;
|
||||
private TextBox passwordTextBox;
|
||||
private TextBox serverUrlTextBox;
|
||||
private CheckBox saveCredentialsCheckBox;
|
||||
private Button okButton;
|
||||
private Button cancelButton;
|
||||
private Label titleLabel;
|
||||
private Label usernameLabel;
|
||||
private Label passwordLabel;
|
||||
private Label serverLabel;
|
||||
|
||||
public LoginDialog(string title = "Eagle0 Login", Credentials existingCredentials = null) {
|
||||
InitializeComponent();
|
||||
this.Text = title;
|
||||
titleLabel.Text = title;
|
||||
|
||||
if (existingCredentials != null) {
|
||||
usernameTextBox.Text = existingCredentials.Username ?? "";
|
||||
passwordTextBox.Text = existingCredentials.Password ?? "";
|
||||
serverUrlTextBox.Text = existingCredentials.ServerUrl ?? "https://eagle0.net";
|
||||
} else {
|
||||
serverUrlTextBox.Text = "https://eagle0.net";
|
||||
}
|
||||
|
||||
// Default to saving credentials
|
||||
saveCredentialsCheckBox.Checked = true;
|
||||
}
|
||||
|
||||
private void InitializeComponent() {
|
||||
// Form properties
|
||||
this.Size = new Size(400, 280);
|
||||
this.StartPosition = FormStartPosition.CenterScreen;
|
||||
this.FormBorderStyle = FormBorderStyle.FixedDialog;
|
||||
this.MaximizeBox = false;
|
||||
this.MinimizeBox = false;
|
||||
this.ShowInTaskbar = false;
|
||||
|
||||
// Title label
|
||||
titleLabel = new Label {
|
||||
Text = "Eagle0 Login",
|
||||
Font = new Font("Microsoft Sans Serif", 12F, FontStyle.Bold),
|
||||
Location = new Point(20, 20),
|
||||
Size = new Size(350, 25),
|
||||
TextAlign = ContentAlignment.MiddleCenter
|
||||
};
|
||||
|
||||
// Username label and textbox
|
||||
usernameLabel = new Label {
|
||||
Text = "Username:",
|
||||
Location = new Point(20, 60),
|
||||
Size = new Size(80, 23),
|
||||
TextAlign = ContentAlignment.MiddleLeft
|
||||
};
|
||||
|
||||
usernameTextBox = new TextBox {
|
||||
Location = new Point(110, 60),
|
||||
Size = new Size(250, 23),
|
||||
TabIndex = 0
|
||||
};
|
||||
|
||||
// Password label and textbox
|
||||
passwordLabel = new Label {
|
||||
Text = "Password:",
|
||||
Location = new Point(20, 90),
|
||||
Size = new Size(80, 23),
|
||||
TextAlign = ContentAlignment.MiddleLeft
|
||||
};
|
||||
|
||||
passwordTextBox = new TextBox {
|
||||
Location = new Point(110, 90),
|
||||
Size = new Size(250, 23),
|
||||
UseSystemPasswordChar = true,
|
||||
TabIndex = 1
|
||||
};
|
||||
|
||||
// Server URL label and textbox
|
||||
serverLabel = new Label {
|
||||
Text = "Server:",
|
||||
Location = new Point(20, 120),
|
||||
Size = new Size(80, 23),
|
||||
TextAlign = ContentAlignment.MiddleLeft
|
||||
};
|
||||
|
||||
serverUrlTextBox = new TextBox {
|
||||
Location = new Point(110, 120),
|
||||
Size = new Size(250, 23),
|
||||
TabIndex = 2
|
||||
};
|
||||
|
||||
// Save credentials checkbox
|
||||
saveCredentialsCheckBox = new CheckBox {
|
||||
Text = "Remember credentials",
|
||||
Location = new Point(20, 160),
|
||||
Size = new Size(200, 23),
|
||||
TabIndex = 3
|
||||
};
|
||||
|
||||
// OK button
|
||||
okButton = new Button {
|
||||
Text = "OK",
|
||||
Location = new Point(200, 200),
|
||||
Size = new Size(80, 30),
|
||||
TabIndex = 4,
|
||||
DialogResult = DialogResult.OK
|
||||
};
|
||||
okButton.Click += OkButton_Click;
|
||||
|
||||
// Cancel button
|
||||
cancelButton = new Button {
|
||||
Text = "Cancel",
|
||||
Location = new Point(290, 200),
|
||||
Size = new Size(80, 30),
|
||||
TabIndex = 5,
|
||||
DialogResult = DialogResult.Cancel
|
||||
};
|
||||
|
||||
// Add controls to form
|
||||
this.Controls.AddRange(new Control[] {
|
||||
titleLabel,
|
||||
usernameLabel,
|
||||
usernameTextBox,
|
||||
passwordLabel,
|
||||
passwordTextBox,
|
||||
serverLabel,
|
||||
serverUrlTextBox,
|
||||
saveCredentialsCheckBox,
|
||||
okButton,
|
||||
cancelButton
|
||||
});
|
||||
|
||||
// Set default button and cancel button
|
||||
this.AcceptButton = okButton;
|
||||
this.CancelButton = cancelButton;
|
||||
|
||||
// Focus on username field
|
||||
this.ActiveControl = usernameTextBox;
|
||||
}
|
||||
|
||||
private void OkButton_Click(object sender, EventArgs e) {
|
||||
// Validate input
|
||||
if (string.IsNullOrWhiteSpace(usernameTextBox.Text)) {
|
||||
MessageBox.Show(
|
||||
"Username is required.",
|
||||
"Validation Error",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Warning);
|
||||
usernameTextBox.Focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(passwordTextBox.Text)) {
|
||||
MessageBox.Show(
|
||||
"Password is required.",
|
||||
"Validation Error",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Warning);
|
||||
passwordTextBox.Focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(serverUrlTextBox.Text)) {
|
||||
MessageBox.Show(
|
||||
"Server URL is required.",
|
||||
"Validation Error",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Warning);
|
||||
serverUrlTextBox.Focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// Create credentials object
|
||||
Credentials = new Credentials {
|
||||
Username = usernameTextBox.Text.Trim(),
|
||||
Password = passwordTextBox.Text,
|
||||
ServerUrl = serverUrlTextBox.Text.Trim()
|
||||
};
|
||||
|
||||
// Normalize server URL
|
||||
if (!Credentials.ServerUrl.StartsWith("http://") &&
|
||||
!Credentials.ServerUrl.StartsWith("https://")) {
|
||||
Credentials.ServerUrl = "https://" + Credentials.ServerUrl;
|
||||
}
|
||||
|
||||
SaveCredentials = saveCredentialsCheckBox.Checked;
|
||||
|
||||
this.DialogResult = DialogResult.OK;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows the login dialog and returns the entered credentials, or null if cancelled.
|
||||
/// </summary>
|
||||
public static (Credentials credentials, bool saveCredentials) ShowLoginDialog(
|
||||
string title = "Eagle0 Login",
|
||||
Credentials existingCredentials = null) {
|
||||
using (var dialog = new LoginDialog(title, existingCredentials)) {
|
||||
if (dialog.ShowDialog() == DialogResult.OK) {
|
||||
return (dialog.Credentials, dialog.SaveCredentials);
|
||||
}
|
||||
return (null, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace EagleInstaller {
|
||||
public partial class MainForm : Form {
|
||||
private Panel loginPanel;
|
||||
private Panel progressPanel;
|
||||
private TextBox progressTextBox;
|
||||
private Label statusLabel;
|
||||
private Button changeCredentialsButton;
|
||||
private Button exitButton;
|
||||
|
||||
// Login controls
|
||||
private TextBox usernameTextBox;
|
||||
private TextBox passwordTextBox;
|
||||
private TextBox serverUrlTextBox;
|
||||
private CheckBox saveCredentialsCheckBox;
|
||||
private Button loginButton;
|
||||
private Button cancelButton;
|
||||
|
||||
public Credentials EnteredCredentials { get; private set; }
|
||||
public bool SaveCredentials { get; private set; }
|
||||
public bool LoginCancelled { get; private set; }
|
||||
|
||||
public MainForm() {
|
||||
InitializeComponent();
|
||||
ShowLoginView();
|
||||
}
|
||||
|
||||
private void InitializeComponent() {
|
||||
// Form properties
|
||||
this.Text = "Eagle0 Installer";
|
||||
this.Size = new Size(600, 450);
|
||||
this.StartPosition = FormStartPosition.CenterScreen;
|
||||
this.FormBorderStyle = FormBorderStyle.FixedSingle;
|
||||
this.MaximizeBox = false;
|
||||
this.MinimizeBox = true;
|
||||
|
||||
// Create login panel
|
||||
CreateLoginPanel();
|
||||
|
||||
// Create progress panel
|
||||
CreateProgressPanel();
|
||||
|
||||
// Add panels to form
|
||||
this.Controls.Add(loginPanel);
|
||||
this.Controls.Add(progressPanel);
|
||||
}
|
||||
|
||||
private void CreateLoginPanel() {
|
||||
loginPanel = new Panel { Dock = DockStyle.Fill, Padding = new Padding(20) };
|
||||
|
||||
// Title
|
||||
var titleLabel = new Label {
|
||||
Text = "Eagle0 Login",
|
||||
Font = new Font("Microsoft Sans Serif", 14F, FontStyle.Bold),
|
||||
Location = new Point(20, 20),
|
||||
Size = new Size(540, 30),
|
||||
TextAlign = ContentAlignment.MiddleCenter
|
||||
};
|
||||
|
||||
// Username
|
||||
var usernameLabel = new Label {
|
||||
Text = "Username:",
|
||||
Location = new Point(50, 80),
|
||||
Size = new Size(100, 23)
|
||||
};
|
||||
usernameTextBox = new TextBox {
|
||||
Location = new Point(160, 80),
|
||||
Size = new Size(300, 23),
|
||||
TabIndex = 0
|
||||
};
|
||||
|
||||
// Password
|
||||
var passwordLabel = new Label {
|
||||
Text = "Password:",
|
||||
Location = new Point(50, 110),
|
||||
Size = new Size(100, 23)
|
||||
};
|
||||
passwordTextBox = new TextBox {
|
||||
Location = new Point(160, 110),
|
||||
Size = new Size(300, 23),
|
||||
UseSystemPasswordChar = true,
|
||||
TabIndex = 1
|
||||
};
|
||||
|
||||
// Server
|
||||
var serverLabel = new Label {
|
||||
Text = "Server:",
|
||||
Location = new Point(50, 140),
|
||||
Size = new Size(100, 23)
|
||||
};
|
||||
serverUrlTextBox = new TextBox {
|
||||
Location = new Point(160, 140),
|
||||
Size = new Size(300, 23),
|
||||
TabIndex = 2
|
||||
};
|
||||
|
||||
// Save credentials checkbox
|
||||
saveCredentialsCheckBox = new CheckBox {
|
||||
Text = "Remember credentials",
|
||||
Location = new Point(50, 180),
|
||||
Size = new Size(200, 23),
|
||||
Checked = true,
|
||||
TabIndex = 3
|
||||
};
|
||||
|
||||
// Buttons
|
||||
loginButton = new Button {
|
||||
Text = "Login",
|
||||
Location = new Point(280, 220),
|
||||
Size = new Size(80, 30),
|
||||
TabIndex = 4
|
||||
};
|
||||
loginButton.Click += LoginButton_Click;
|
||||
|
||||
cancelButton = new Button {
|
||||
Text = "Cancel",
|
||||
Location = new Point(380, 220),
|
||||
Size = new Size(80, 30),
|
||||
TabIndex = 5
|
||||
};
|
||||
cancelButton.Click += CancelButton_Click;
|
||||
|
||||
// Add controls to login panel
|
||||
loginPanel.Controls.AddRange(new Control[] {
|
||||
titleLabel,
|
||||
usernameLabel,
|
||||
usernameTextBox,
|
||||
passwordLabel,
|
||||
passwordTextBox,
|
||||
serverLabel,
|
||||
serverUrlTextBox,
|
||||
saveCredentialsCheckBox,
|
||||
loginButton,
|
||||
cancelButton
|
||||
});
|
||||
|
||||
// Set form defaults
|
||||
this.AcceptButton = loginButton;
|
||||
this.CancelButton = cancelButton;
|
||||
}
|
||||
|
||||
private void CreateProgressPanel() {
|
||||
progressPanel =
|
||||
new Panel { Dock = DockStyle.Fill, Padding = new Padding(10), Visible = false };
|
||||
|
||||
// Title and status
|
||||
var titleLabel = new Label {
|
||||
Text = "Eagle0 Installer",
|
||||
Font = new Font("Microsoft Sans Serif", 14F, FontStyle.Bold),
|
||||
Location = new Point(10, 10),
|
||||
Size = new Size(200, 30)
|
||||
};
|
||||
|
||||
statusLabel = new Label {
|
||||
Text = "Initializing...",
|
||||
Location = new Point(10, 45),
|
||||
Size = new Size(560, 23),
|
||||
ForeColor = Color.Blue
|
||||
};
|
||||
|
||||
// Progress text box with scrollbar
|
||||
progressTextBox = new TextBox {
|
||||
Location = new Point(10, 75),
|
||||
Size = new Size(560, 280),
|
||||
Multiline = true,
|
||||
ReadOnly = true,
|
||||
ScrollBars = ScrollBars.Vertical,
|
||||
Font = new Font("Consolas", 9F),
|
||||
BackColor = Color.Black,
|
||||
ForeColor = Color.LightGreen,
|
||||
TabIndex = 0
|
||||
};
|
||||
|
||||
// Control buttons
|
||||
changeCredentialsButton = new Button {
|
||||
Text = "Change Credentials",
|
||||
Location = new Point(350, 370),
|
||||
Size = new Size(130, 30),
|
||||
Enabled = false
|
||||
};
|
||||
changeCredentialsButton.Click += ChangeCredentialsButton_Click;
|
||||
|
||||
exitButton = new Button {
|
||||
Text = "Exit",
|
||||
Location = new Point(490, 370),
|
||||
Size = new Size(80, 30)
|
||||
};
|
||||
exitButton.Click += ExitButton_Click;
|
||||
|
||||
// Add controls to progress panel
|
||||
progressPanel.Controls.AddRange(new Control[] {
|
||||
titleLabel,
|
||||
statusLabel,
|
||||
progressTextBox,
|
||||
changeCredentialsButton,
|
||||
exitButton
|
||||
});
|
||||
}
|
||||
|
||||
public void ShowLoginView() {
|
||||
loginPanel.Visible = true;
|
||||
progressPanel.Visible = false;
|
||||
this.ActiveControl = usernameTextBox;
|
||||
}
|
||||
|
||||
public void ShowProgressView() {
|
||||
loginPanel.Visible = false;
|
||||
progressPanel.Visible = true;
|
||||
changeCredentialsButton.Enabled = true;
|
||||
}
|
||||
|
||||
public void SetLoginCredentials(Credentials credentials) {
|
||||
if (credentials != null) {
|
||||
usernameTextBox.Text = credentials.Username ?? "";
|
||||
passwordTextBox.Text = credentials.Password ?? "";
|
||||
serverUrlTextBox.Text = credentials.ServerUrl ?? "https://eagle0.net";
|
||||
} else {
|
||||
serverUrlTextBox.Text = "https://eagle0.net";
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateStatus(string status) {
|
||||
if (statusLabel.InvokeRequired) {
|
||||
statusLabel.Invoke(new Action<string>(UpdateStatus), status);
|
||||
return;
|
||||
}
|
||||
statusLabel.Text = status;
|
||||
}
|
||||
|
||||
public void AppendProgress(string message) {
|
||||
if (progressTextBox.InvokeRequired) {
|
||||
progressTextBox.Invoke(new Action<string>(AppendProgress), message);
|
||||
return;
|
||||
}
|
||||
|
||||
progressTextBox.AppendText($"[{DateTime.Now:HH:mm:ss}] {message}\r\n");
|
||||
progressTextBox.SelectionStart = progressTextBox.Text.Length;
|
||||
progressTextBox.ScrollToCaret();
|
||||
}
|
||||
|
||||
private void LoginButton_Click(object sender, EventArgs e) {
|
||||
// Validate input
|
||||
if (string.IsNullOrWhiteSpace(usernameTextBox.Text)) {
|
||||
MessageBox.Show(
|
||||
"Username is required.",
|
||||
"Validation Error",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Warning);
|
||||
usernameTextBox.Focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(passwordTextBox.Text)) {
|
||||
MessageBox.Show(
|
||||
"Password is required.",
|
||||
"Validation Error",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Warning);
|
||||
passwordTextBox.Focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(serverUrlTextBox.Text)) {
|
||||
MessageBox.Show(
|
||||
"Server URL is required.",
|
||||
"Validation Error",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Warning);
|
||||
serverUrlTextBox.Focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// Create credentials
|
||||
var serverUrl = serverUrlTextBox.Text.Trim();
|
||||
if (!serverUrl.StartsWith("http://") && !serverUrl.StartsWith("https://")) {
|
||||
serverUrl = "https://" + serverUrl;
|
||||
}
|
||||
|
||||
EnteredCredentials = new Credentials {
|
||||
Username = usernameTextBox.Text.Trim(),
|
||||
Password = passwordTextBox.Text,
|
||||
ServerUrl = serverUrl
|
||||
};
|
||||
|
||||
SaveCredentials = saveCredentialsCheckBox.Checked;
|
||||
LoginCancelled = false;
|
||||
|
||||
ShowProgressView();
|
||||
}
|
||||
|
||||
private void CancelButton_Click(object sender, EventArgs e) {
|
||||
LoginCancelled = true;
|
||||
this.Close();
|
||||
}
|
||||
|
||||
private void ChangeCredentialsButton_Click(object sender, EventArgs e) { ShowLoginView(); }
|
||||
|
||||
private void ExitButton_Click(object sender, EventArgs e) { this.Close(); }
|
||||
|
||||
public static MainForm CreateMainForm(
|
||||
string title = "Eagle0 Login",
|
||||
Credentials existingCredentials = null) {
|
||||
var form = new MainForm();
|
||||
form.Text = title;
|
||||
form.SetLoginCredentials(existingCredentials);
|
||||
return form;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace EagleInstaller {
|
||||
class Program {
|
||||
@@ -11,17 +12,278 @@ namespace EagleInstaller {
|
||||
private static readonly string ExeLocation = Path.Combine(FileLocation, "eagle0.exe");
|
||||
|
||||
static async Task Main(string[] args) {
|
||||
EagleUpdater updater = new EagleUpdater();
|
||||
// Enable visual styles for Windows Forms
|
||||
Application.EnableVisualStyles();
|
||||
Application.SetCompatibleTextRenderingDefault(false);
|
||||
|
||||
bool success = await updater.MaybePerformUpdateWithRetries(FileLocation);
|
||||
|
||||
if (success) {
|
||||
Console.WriteLine("Launching application");
|
||||
System.Diagnostics.Process.Start(ExeLocation);
|
||||
} else {
|
||||
Console.WriteLine("Failed to update!");
|
||||
Thread.Sleep(int.MaxValue);
|
||||
// Handle installer replacement if launched with --replace-installer flag
|
||||
if (args.Length > 0 && args[0] == "--replace-installer") {
|
||||
await HandleInstallerReplacement(args);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for credential management flags
|
||||
bool forceLogin =
|
||||
args.Length > 0 && (args[0] == "--login" || args[0] == "--change-credentials");
|
||||
bool clearCredentials = args.Length > 0 && args[0] == "--clear-credentials";
|
||||
bool showHelp = args.Length > 0 && (args[0] == "--help" || args[0] == "-h");
|
||||
|
||||
// Show help if requested
|
||||
if (showHelp) {
|
||||
ShowHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle clear credentials flag
|
||||
if (clearCredentials) {
|
||||
Console.WriteLine("Clearing saved credentials...");
|
||||
CredentialManager.ClearCredentials();
|
||||
Console.WriteLine("Credentials cleared successfully.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create main form
|
||||
var mainForm = MainForm.CreateMainForm();
|
||||
Logger.Initialize(mainForm);
|
||||
|
||||
// Show the form and ensure handle is created
|
||||
mainForm.Show();
|
||||
mainForm.CreateControl();
|
||||
|
||||
// Start the installer logic in the background
|
||||
var installerTask = RunInstallerLogic(mainForm, forceLogin);
|
||||
|
||||
// Run the application with the main form
|
||||
Application.Run(mainForm);
|
||||
|
||||
// Wait for installer logic to complete
|
||||
await installerTask;
|
||||
}
|
||||
|
||||
private static async Task RunInstallerLogic(MainForm mainForm, bool forceLogin) {
|
||||
try {
|
||||
// Get credentials (load saved or prompt user)
|
||||
Credentials credentials = await GetValidCredentials(mainForm, forceLogin);
|
||||
if (credentials == null) {
|
||||
Logger.WriteError("No valid credentials provided. Exiting.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Switch to progress view
|
||||
SafeInvoke(mainForm, () => mainForm.ShowProgressView());
|
||||
|
||||
EagleUpdater updater = new EagleUpdater(credentials);
|
||||
|
||||
bool credentialsAreValid = false;
|
||||
while (!credentialsAreValid) {
|
||||
try {
|
||||
// Stage 1: Check if installer itself needs update
|
||||
var installerUpdateInfo = await updater.CheckForInstallerUpdate();
|
||||
if (installerUpdateInfo.InstallerNeedsUpdate) {
|
||||
Logger.UpdateStatus("Installer update required...");
|
||||
bool downloadSuccess = await updater.DownloadAndLaunchNewInstaller(
|
||||
installerUpdateInfo.NewInstallerUrl);
|
||||
if (downloadSuccess) {
|
||||
Logger.UpdateStatus(
|
||||
"New installer launched. Exiting current version.");
|
||||
return; // Exit this version
|
||||
} else {
|
||||
Logger.WriteError(
|
||||
"Failed to download new installer, continuing with current version...");
|
||||
}
|
||||
}
|
||||
|
||||
// Stage 2: Proceed with normal game update
|
||||
bool success = await updater.MaybePerformUpdateWithRetries(FileLocation);
|
||||
|
||||
if (success) {
|
||||
Logger.UpdateStatus("Launching Eagle0...");
|
||||
System.Diagnostics.Process.Start(ExeLocation);
|
||||
credentialsAreValid = true;
|
||||
} else {
|
||||
Logger.WriteError("Update failed!");
|
||||
credentialsAreValid = true; // Don't retry on non-auth failures
|
||||
}
|
||||
} catch (AuthenticationFailedException ex) {
|
||||
Logger.WriteError($"Authentication failed: {ex.Message}");
|
||||
|
||||
// Clear saved credentials since they're invalid
|
||||
CredentialManager.ClearCredentials();
|
||||
|
||||
// Show login view again
|
||||
SafeInvoke(mainForm, () => mainForm.ShowLoginView());
|
||||
|
||||
// Wait for new credentials
|
||||
credentials = await GetValidCredentials(
|
||||
mainForm,
|
||||
true,
|
||||
"Authentication Failed - Please re-enter credentials");
|
||||
if (credentials == null) {
|
||||
Logger.WriteError("No valid credentials provided. Exiting.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Switch back to progress view
|
||||
SafeInvoke(mainForm, () => mainForm.ShowProgressView());
|
||||
|
||||
// Update updater with new credentials
|
||||
updater.UpdateCredentials(credentials);
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) { Logger.WriteError($"Unexpected error: {ex.Message}"); }
|
||||
}
|
||||
|
||||
private static async Task HandleInstallerReplacement(string[] args) {
|
||||
if (args.Length < 2) {
|
||||
Console.WriteLine("Error: --replace-installer requires path to old installer");
|
||||
return;
|
||||
}
|
||||
|
||||
string oldInstallerPath = args[1];
|
||||
string currentInstallerPath =
|
||||
Environment.ProcessPath ?? System.AppContext.BaseDirectory;
|
||||
|
||||
Console.WriteLine($"Replacing installer: {oldInstallerPath}");
|
||||
|
||||
try {
|
||||
// Wait a moment for the old process to exit
|
||||
await Task.Delay(2000);
|
||||
|
||||
// Backup old installer
|
||||
string backupPath = oldInstallerPath + ".backup";
|
||||
if (File.Exists(oldInstallerPath)) { File.Move(oldInstallerPath, backupPath); }
|
||||
|
||||
// Copy new installer to old location
|
||||
File.Copy(currentInstallerPath, oldInstallerPath, true);
|
||||
|
||||
Console.WriteLine("Installer replaced successfully. Launching new version...");
|
||||
|
||||
// Launch the newly installed version
|
||||
var processInfo = new System.Diagnostics.ProcessStartInfo {
|
||||
FileName = oldInstallerPath,
|
||||
UseShellExecute = true
|
||||
};
|
||||
System.Diagnostics.Process.Start(processInfo);
|
||||
|
||||
// Clean up backup after successful replacement
|
||||
if (File.Exists(backupPath)) { File.Delete(backupPath); }
|
||||
|
||||
} catch (Exception e) {
|
||||
Console.WriteLine($"Failed to replace installer: {e.Message}");
|
||||
|
||||
// Restore backup if replacement failed
|
||||
string backupPath = oldInstallerPath + ".backup";
|
||||
if (File.Exists(backupPath) && !File.Exists(oldInstallerPath)) {
|
||||
File.Move(backupPath, oldInstallerPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<Credentials> GetValidCredentials(
|
||||
MainForm mainForm,
|
||||
bool forceLogin = false,
|
||||
string title = "Eagle0 Login") {
|
||||
Credentials savedCredentials = null;
|
||||
|
||||
// Try to load saved credentials first (unless forced to login)
|
||||
if (!forceLogin) {
|
||||
savedCredentials = CredentialManager.LoadCredentials();
|
||||
|
||||
if (savedCredentials != null) {
|
||||
Logger.WriteLine(
|
||||
$"Found saved credentials for user: {savedCredentials.Username}");
|
||||
// Pre-fill the form with saved credentials but let user click Login
|
||||
SafeInvoke(mainForm, () => mainForm.SetLoginCredentials(savedCredentials));
|
||||
}
|
||||
}
|
||||
|
||||
// If no saved credentials or force login, show login form
|
||||
if (savedCredentials == null || forceLogin) {
|
||||
if (savedCredentials == null) {
|
||||
Logger.WriteLine(
|
||||
"No saved credentials found. Please enter your login information.");
|
||||
} else {
|
||||
Logger.WriteLine("Please enter new credentials.");
|
||||
}
|
||||
|
||||
// Set the form title
|
||||
SafeInvoke(mainForm, () => mainForm.Text = title);
|
||||
}
|
||||
|
||||
// Wait for user to enter credentials through the form
|
||||
return await WaitForCredentials(mainForm);
|
||||
}
|
||||
|
||||
private static async Task<Credentials> WaitForCredentials(MainForm mainForm) {
|
||||
var tcs = new TaskCompletionSource<Credentials>();
|
||||
|
||||
// Set up event handler to capture when login is completed
|
||||
EventHandler loginCompleted = null;
|
||||
loginCompleted = (sender, e) => {
|
||||
var form = sender as MainForm;
|
||||
if (form != null && form.EnteredCredentials != null) {
|
||||
var credentials = form.EnteredCredentials;
|
||||
var saveCredentials = form.SaveCredentials;
|
||||
|
||||
if (saveCredentials) {
|
||||
try {
|
||||
CredentialManager.SaveCredentials(credentials);
|
||||
CredentialManager.SaveCredentialsForUnityClient(credentials);
|
||||
Logger.WriteLine("Credentials saved successfully.");
|
||||
} catch (Exception ex) {
|
||||
Logger.WriteError($"Failed to save credentials: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
tcs.SetResult(credentials);
|
||||
} else if (form != null && form.LoginCancelled) {
|
||||
tcs.SetResult(null);
|
||||
}
|
||||
};
|
||||
|
||||
// Monitor for form changes (this is a simplified approach)
|
||||
var monitorTask = Task.Run(async () => {
|
||||
while (!tcs.Task.IsCompleted) {
|
||||
await Task.Delay(100);
|
||||
if (mainForm.EnteredCredentials != null || mainForm.LoginCancelled) {
|
||||
loginCompleted(mainForm, EventArgs.Empty);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return await tcs.Task;
|
||||
}
|
||||
|
||||
private static void SafeInvoke(Control control, Action action) {
|
||||
if (control.IsHandleCreated && control.InvokeRequired) {
|
||||
control.Invoke(action);
|
||||
} else if (control.IsHandleCreated) {
|
||||
action();
|
||||
}
|
||||
}
|
||||
|
||||
private static void ShowHelp() {
|
||||
Console.WriteLine("Eagle0 Installer");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("USAGE:");
|
||||
Console.WriteLine(" EagleInstaller.exe [OPTIONS]");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("OPTIONS:");
|
||||
Console.WriteLine(" --help, -h Show this help message");
|
||||
Console.WriteLine(" --login, --change-credentials Force credential input dialog");
|
||||
Console.WriteLine(" --clear-credentials Clear saved credentials and exit");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("NORMAL OPERATION:");
|
||||
Console.WriteLine(" Run without arguments to start the normal update process.");
|
||||
Console.WriteLine(
|
||||
" If credentials are saved, you'll see a 3-second prompt to change them.");
|
||||
Console.WriteLine(" Press 'C' during this prompt to change credentials.");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("EXAMPLES:");
|
||||
Console.WriteLine(" EagleInstaller.exe # Normal operation");
|
||||
Console.WriteLine(" EagleInstaller.exe --login # Force new login");
|
||||
Console.WriteLine(" EagleInstaller.exe --clear-credentials # Clear saved login");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
service_url = https://eagle0.net
|
||||
username = client
|
||||
password = bWhsDKH-LPUJ4LwdgDJY
|
||||
manifest_name = eagle0WIN_manifest.txt
|
||||
remote_manifest_prefix = assets/unity3d/
|
||||
remote_asset_prefix = assets/unity3d/win/
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
# Eagle0 Installer GUI Update
|
||||
|
||||
The installer has been transformed from a console application to a modern Windows Forms GUI application that displays file download information in a scrollable window.
|
||||
|
||||
## Key Changes
|
||||
|
||||
### **New GUI Components**
|
||||
|
||||
**1. MainForm.cs**
|
||||
- **Dual-panel design**: Login view and Progress view
|
||||
- **Login Panel**: Username, password, server URL, remember credentials checkbox
|
||||
- **Progress Panel**: Status label + scrollable text box with file download details
|
||||
- **Terminal-style output**: Black background, green text, console font
|
||||
- **Timestamped messages**: Each log entry shows `[HH:mm:ss] message`
|
||||
|
||||
**2. Logger.cs**
|
||||
- **Unified logging system**: Outputs to both console AND GUI
|
||||
- **Thread-safe**: Uses Invoke for cross-thread GUI updates
|
||||
- **Multiple log levels**: WriteLine, UpdateStatus, WriteError, WriteWarning
|
||||
- **Auto-scrolling**: Progress text box automatically scrolls to show latest messages
|
||||
|
||||
### **User Experience Flow**
|
||||
|
||||
**1. Application Startup**
|
||||
- Windows Forms application opens with professional login dialog
|
||||
- Saved credentials auto-populated (if available)
|
||||
- "Remember credentials" checkbox for convenience
|
||||
|
||||
**2. Login Process**
|
||||
- User enters/confirms credentials
|
||||
- Click "Login" button to proceed
|
||||
- Form switches to progress view
|
||||
|
||||
**3. Progress Display**
|
||||
- **Status line**: Shows current operation (e.g., "Downloading files...")
|
||||
- **Scrollable log**: Shows detailed file-by-file progress
|
||||
- **Color-coded messages**:
|
||||
- ✓ Green checkmarks for successful downloads
|
||||
- ✗ Red X marks for errors
|
||||
- Blue status updates
|
||||
- **Change Credentials button**: Allows switching back to login
|
||||
|
||||
**4. File Download Information**
|
||||
Now appears in the GUI progress window:
|
||||
```
|
||||
[14:32:15] Downloading: game/eagle0.exe
|
||||
[14:32:16] ✓ Verified and saved: game/eagle0.exe
|
||||
[14:32:16] Downloading: game/assets/textures.bundle
|
||||
[14:32:18] ✓ Verified and saved: game/assets/textures.bundle
|
||||
[14:32:18] Up to date: game/data.unity3d
|
||||
[14:32:18] All downloads completed successfully.
|
||||
```
|
||||
|
||||
### **Technical Implementation**
|
||||
|
||||
**Unified Logging System:**
|
||||
```csharp
|
||||
// Both console and GUI output
|
||||
Logger.WriteLine("Downloading: game/eagle0.exe");
|
||||
Logger.UpdateStatus("Update completed successfully!");
|
||||
Logger.WriteError("Network connection failed");
|
||||
```
|
||||
|
||||
**Thread-Safe GUI Updates:**
|
||||
```csharp
|
||||
public void AppendProgress(string message) {
|
||||
if (progressTextBox.InvokeRequired) {
|
||||
progressTextBox.Invoke(new Action<string>(AppendProgress), message);
|
||||
return;
|
||||
}
|
||||
progressTextBox.AppendText($"[{DateTime.Now:HH:mm:ss}] {message}\r\n");
|
||||
progressTextBox.ScrollToCaret();
|
||||
}
|
||||
```
|
||||
|
||||
**Dual Output Support:**
|
||||
- **Console mode**: When run from command line, still shows console output
|
||||
- **GUI mode**: When double-clicked, shows Windows Forms interface
|
||||
- **Both simultaneously**: Console + GUI for debugging
|
||||
|
||||
### **Credential Management Integration**
|
||||
|
||||
All existing credential features work with the new GUI:
|
||||
- **Command line flags**: `--login`, `--clear-credentials`, `--help`
|
||||
- **Saved credentials**: Auto-populate form fields
|
||||
- **Authentication errors**: Show login form with error message
|
||||
- **Unity client sharing**: Credentials still saved for game client
|
||||
|
||||
### **File Structure Changes**
|
||||
|
||||
**New Files:**
|
||||
- `MainForm.cs` - Main Windows Forms interface
|
||||
- `Logger.cs` - Unified console/GUI logging system
|
||||
- `GUI_UPDATE.md` - This documentation
|
||||
|
||||
**Modified Files:**
|
||||
- `Program.cs` - GUI application lifecycle management
|
||||
- `EagleUpdater.cs` - All Console.WriteLine → Logger calls
|
||||
- `EagleInstaller.csproj` - Added Windows Forms support
|
||||
|
||||
### **Benefits**
|
||||
|
||||
1. **Professional Appearance**: Modern Windows Forms interface
|
||||
2. **Better User Experience**: Clear progress indication with scrollable log
|
||||
3. **File Download Visibility**: Users can see exactly which files are being processed
|
||||
4. **Error Transparency**: Color-coded success/failure indicators
|
||||
5. **Credential Management**: Integrated login with remember option
|
||||
6. **Backwards Compatibility**: Command line flags still work
|
||||
7. **Debug Friendly**: Both console and GUI output available
|
||||
|
||||
### **Example Output in GUI**
|
||||
|
||||
```
|
||||
STATUS: Fetching new manifest...
|
||||
[14:30:42] Checking for installer updates...
|
||||
[14:30:43] Installer is up to date
|
||||
[14:30:43] Update attempt 1 of 3...
|
||||
[14:30:43] Downloading: game/eagle0.exe
|
||||
[14:30:45] ✓ Verified and saved: game/eagle0.exe
|
||||
[14:30:45] Downloading: game/assets/music.bundle
|
||||
[14:30:47] ✓ Verified and saved: game/assets/music.bundle
|
||||
[14:30:47] Up to date: game/data.unity3d
|
||||
[14:30:47] Up to date: game/config.json
|
||||
[14:30:47] All downloads completed successfully.
|
||||
STATUS: Update completed successfully!
|
||||
[14:30:48] Launching Eagle0...
|
||||
```
|
||||
|
||||
The installer now provides a professional, user-friendly interface while maintaining all existing functionality including the two-stage installer updates and comprehensive credential management system.
|
||||
@@ -0,0 +1,6 @@
|
||||
# installer_version=1.1.0
|
||||
# installer_url=assets/installer/EagleInstaller.exe
|
||||
a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456 game/eagle0.exe
|
||||
b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef1234567 game/data.unity3d
|
||||
c3d4e5f6789012345678901234567890abcdef1234567890abcdef1234567890 game/assets/textures.bundle
|
||||
d4e5f6789012345678901234567890abcdef1234567890abcdef12345678901a game/assets/audio.bundle
|
||||
@@ -0,0 +1,15 @@
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
|
||||
|
||||
go_library(
|
||||
name = "installer_build_handler_lib",
|
||||
srcs = ["main.go"],
|
||||
importpath = "github.com/nolen777/eagle0/src/main/go/net/eagle0/build/installer_build_handler",
|
||||
visibility = ["//visibility:private"],
|
||||
deps = ["//src/main/go/net/eagle0/util/aws"],
|
||||
)
|
||||
|
||||
go_binary(
|
||||
name = "installer_build_handler",
|
||||
embed = [":installer_build_handler_lib"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
@@ -0,0 +1,103 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"github.com/nolen777/eagle0/src/main/go/net/eagle0/util/aws"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
var bucketName = "eagle0-windows"
|
||||
var installerKey = "installer/EagleInstaller.exe"
|
||||
var versionKey = "installer/version.txt"
|
||||
|
||||
func sha(filePath string) (string, error) {
|
||||
fileToCheck, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer fileToCheck.Close()
|
||||
|
||||
h := sha256.New()
|
||||
if _, err := io.Copy(h, fileToCheck); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%x", h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 2 {
|
||||
log.Fatal("Usage: installer_build_handler <path-to-EagleInstaller.exe>")
|
||||
}
|
||||
|
||||
installerPath := os.Args[1]
|
||||
|
||||
// Verify the file exists
|
||||
if _, err := os.Stat(installerPath); os.IsNotExist(err) {
|
||||
log.Fatalf("Installer file does not exist: %s", installerPath)
|
||||
}
|
||||
|
||||
// Calculate SHA256 of the installer
|
||||
installerSha, err := sha(installerPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to calculate SHA256: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("Uploading installer with SHA256: %s", installerSha)
|
||||
|
||||
// Create BucketBasics instance
|
||||
bucketBasics, err := aws.NewBucketBasics()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create AWS client: %v", err)
|
||||
}
|
||||
|
||||
// Upload installer to S3
|
||||
err = bucketBasics.UploadFile(bucketName, installerKey, installerPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to upload installer: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("Successfully uploaded installer to s3://%s/%s", bucketName, installerKey)
|
||||
|
||||
// Create a version/metadata file with the SHA256
|
||||
versionContent := fmt.Sprintf("sha256=%s\nversion=1.0.0\nurl=installer/EagleInstaller.exe", installerSha)
|
||||
|
||||
// Write version info to temp file
|
||||
tempVersionFile := filepath.Join(os.TempDir(), "installer_version.txt")
|
||||
err = os.WriteFile(tempVersionFile, []byte(versionContent), 0644)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create version file: %v", err)
|
||||
}
|
||||
|
||||
// Upload version file to S3
|
||||
err = bucketBasics.UploadFile(bucketName, versionKey, tempVersionFile)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to upload version file: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("Successfully uploaded version info to s3://%s/%s", bucketName, versionKey)
|
||||
|
||||
// Create installer manifest section
|
||||
installerManifest := fmt.Sprintf("installer_version=1.0.0\ninstaller_url=%s\ninstaller_sha256=%s", installerKey, installerSha)
|
||||
|
||||
// Write installer manifest to temp file for manifest manager
|
||||
tempManifestFile := filepath.Join(os.TempDir(), "installer_manifest.txt")
|
||||
err = os.WriteFile(tempManifestFile, []byte(installerManifest), 0644)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create installer manifest file: %v", err)
|
||||
}
|
||||
|
||||
log.Println("Updating unified manifest...")
|
||||
// Note: In the GitHub workflow, this will be called via bazel run
|
||||
// bazel run //src/main/go/net/eagle0/build/manifest_manager:manifest_manager -- installer tempManifestFile
|
||||
|
||||
// Clean up temp files
|
||||
os.Remove(tempVersionFile)
|
||||
os.Remove(tempManifestFile)
|
||||
|
||||
log.Println("Installer deployment completed successfully!")
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
|
||||
|
||||
go_library(
|
||||
name = "manifest_manager_lib",
|
||||
srcs = ["main.go"],
|
||||
importpath = "github.com/nolen777/eagle0/src/main/go/net/eagle0/build/manifest_manager",
|
||||
visibility = ["//visibility:private"],
|
||||
deps = ["//src/main/go/net/eagle0/util/aws"],
|
||||
)
|
||||
|
||||
go_binary(
|
||||
name = "manifest_manager",
|
||||
embed = [":manifest_manager_lib"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
@@ -0,0 +1,148 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/nolen777/eagle0/src/main/go/net/eagle0/util/aws"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const bucketName = "eagle0-windows"
|
||||
const manifestPath = "eagle0_manifest.txt"
|
||||
|
||||
type ManifestManager struct {
|
||||
bucketBasics aws.BucketBasics
|
||||
}
|
||||
|
||||
type ManifestSection struct {
|
||||
Name string // "unity3d" or "installer"
|
||||
Content string
|
||||
}
|
||||
|
||||
func NewManifestManager() (*ManifestManager, error) {
|
||||
bb, err := aws.NewBucketBasics()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ManifestManager{bucketBasics: bb}, nil
|
||||
}
|
||||
|
||||
func (mm *ManifestManager) parseManifest(content string) map[string]string {
|
||||
sections := make(map[string]string)
|
||||
lines := strings.Split(content, "\n")
|
||||
|
||||
var currentSection string
|
||||
var sectionLines []string
|
||||
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "# ") && strings.HasSuffix(line, "_section_start") {
|
||||
// Save previous section if exists
|
||||
if currentSection != "" {
|
||||
sections[currentSection] = strings.Join(sectionLines, "\n")
|
||||
}
|
||||
// Start new section
|
||||
currentSection = strings.TrimSuffix(strings.TrimPrefix(line, "# "), "_section_start")
|
||||
sectionLines = []string{}
|
||||
} else if strings.HasPrefix(line, "# ") && strings.HasSuffix(line, "_section_end") {
|
||||
// End current section
|
||||
if currentSection != "" {
|
||||
sections[currentSection] = strings.Join(sectionLines, "\n")
|
||||
currentSection = ""
|
||||
sectionLines = []string{}
|
||||
}
|
||||
} else if currentSection != "" {
|
||||
sectionLines = append(sectionLines, line)
|
||||
}
|
||||
}
|
||||
|
||||
return sections
|
||||
}
|
||||
|
||||
func (mm *ManifestManager) buildManifest(sections map[string]string) string {
|
||||
var manifest strings.Builder
|
||||
|
||||
manifest.WriteString(fmt.Sprintf("# Generated: %s\n", time.Now().Format(time.RFC3339)))
|
||||
manifest.WriteString("# Eagle0 Unified Manifest\n")
|
||||
|
||||
// Installer metadata as comments (for client compatibility)
|
||||
if installer, exists := sections["installer"]; exists {
|
||||
lines := strings.Split(strings.TrimSpace(installer), "\n")
|
||||
for _, line := range lines {
|
||||
if line != "" {
|
||||
manifest.WriteString(fmt.Sprintf("# %s\n", line))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
manifest.WriteString("\n")
|
||||
|
||||
// Unity3D section (file hashes)
|
||||
if unity, exists := sections["unity3d"]; exists {
|
||||
manifest.WriteString(unity)
|
||||
if !strings.HasSuffix(unity, "\n") {
|
||||
manifest.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
return manifest.String()
|
||||
}
|
||||
|
||||
func (mm *ManifestManager) UpdateSection(sectionName, sectionContent string) error {
|
||||
log.Printf("Updating manifest section: %s", sectionName)
|
||||
|
||||
// Fetch existing manifest
|
||||
existing, err := mm.bucketBasics.FetchString(bucketName, manifestPath)
|
||||
if err != nil {
|
||||
log.Printf("No existing manifest found, creating new one")
|
||||
existing = ""
|
||||
}
|
||||
|
||||
// Parse sections
|
||||
sections := mm.parseManifest(existing)
|
||||
|
||||
// Update the specified section
|
||||
sections[sectionName] = sectionContent
|
||||
|
||||
// Build new manifest
|
||||
newManifest := mm.buildManifest(sections)
|
||||
|
||||
// Upload updated manifest
|
||||
err = mm.bucketBasics.UploadBytes(bucketName, manifestPath, []byte(newManifest))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to upload manifest: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("Successfully updated manifest section: %s", sectionName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 3 {
|
||||
log.Fatal("Usage: manifest_manager <section_name> <section_content_file>")
|
||||
}
|
||||
|
||||
sectionName := os.Args[1]
|
||||
contentFile := os.Args[2]
|
||||
|
||||
// Read section content from file
|
||||
content, err := os.ReadFile(contentFile)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to read content file: %v", err)
|
||||
}
|
||||
|
||||
// Create manifest manager
|
||||
mm, err := NewManifestManager()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create manifest manager: %v", err)
|
||||
}
|
||||
|
||||
// Update the section
|
||||
err = mm.UpdateSection(sectionName, string(content))
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to update manifest: %v", err)
|
||||
}
|
||||
|
||||
log.Println("Manifest update completed successfully!")
|
||||
}
|
||||
@@ -3,15 +3,29 @@ package main
|
||||
import (
|
||||
"github.com/nolen777/eagle0/src/main/go/net/eagle0/util/aws"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var bucketName = "eagle0-windows"
|
||||
var destinationRoot = "unity3d/"
|
||||
|
||||
func getAsset(w http.ResponseWriter, r *http.Request) {
|
||||
// Get the file name from the URL
|
||||
fileName := r.URL.Path[len("/unity3d/"):]
|
||||
urlPath := r.URL.Path
|
||||
var bucketName string
|
||||
var destinationRoot string
|
||||
var fileName string
|
||||
|
||||
// Check if the URL path starts with "/unity3d/"
|
||||
if strings.HasPrefix(urlPath, "/unity3d/") {
|
||||
bucketName = "eagle0-windows"
|
||||
destinationRoot = "unity3d/"
|
||||
fileName = urlPath[len("/unity3d/"):]
|
||||
} else if strings.HasPrefix(urlPath, "/headshots/") {
|
||||
bucketName = "eagle0-headshots"
|
||||
destinationRoot = ""
|
||||
fileName = urlPath[len("/headshots/"):]
|
||||
} else {
|
||||
// If the URL path does not start with either, return an error
|
||||
http.Error(w, "Invalid URL path", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
// Create a presigned URL for the file
|
||||
url, err := aws.GetPresignedURL(bucketName, destinationRoot+fileName, 10*time.Minute)
|
||||
@@ -25,7 +39,7 @@ func getAsset(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("GET /unity3d/", getAsset)
|
||||
http.HandleFunc("GET /", getAsset)
|
||||
|
||||
err := http.ListenAndServe(":3333", nil)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
|
||||
|
||||
go_binary(
|
||||
name = "headshots",
|
||||
embed = [":headshots_lib"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "headshots_lib",
|
||||
srcs = ["main.go"],
|
||||
importpath = "github.com/nolen777/eagle0/src/main/go/net/eagle0/util/hero_generation/headshots",
|
||||
visibility = ["//visibility:private"],
|
||||
deps = ["//src/main/go/net/eagle0/util/tsv"],
|
||||
)
|
||||
@@ -0,0 +1,338 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/nolen777/eagle0/src/main/go/net/eagle0/util/tsv"
|
||||
)
|
||||
|
||||
// Hero represents a hero from the TSV data
|
||||
type Hero struct {
|
||||
Name string
|
||||
Strength int
|
||||
Agility int
|
||||
Constitution int
|
||||
Wisdom int
|
||||
Charisma int
|
||||
Sum int
|
||||
Vigor int
|
||||
Integrity int
|
||||
Ambition int
|
||||
Gregariousness int
|
||||
Bravery int
|
||||
Profession string
|
||||
GreatPerson bool
|
||||
ImagePath string
|
||||
Gender string
|
||||
Backstory string
|
||||
Personality string
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Check for command line arguments
|
||||
if len(os.Args) < 3 {
|
||||
fmt.Fprintf(os.Stderr, "Usage: %s <input-file> <output-file>\n", os.Args[0])
|
||||
fmt.Fprintf(os.Stderr, "Example: %s input.herodata output.herodata\n", os.Args[0])
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
inputPath := os.Args[1]
|
||||
outputPath := os.Args[2]
|
||||
fmt.Printf("Reading hero data from: %s\n", inputPath)
|
||||
fmt.Printf("Writing modified data to: %s\n", outputPath)
|
||||
|
||||
// Load the TSV data
|
||||
lines, err := tsv.LoadLines(inputPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Error loading TSV file: %v", err)
|
||||
}
|
||||
|
||||
if len(lines) == 0 {
|
||||
log.Fatal("No data found in file")
|
||||
}
|
||||
|
||||
fmt.Printf("Loaded %d lines of data\n", len(lines))
|
||||
|
||||
// Find the image_path field index in the header
|
||||
imagePathIndex := -1
|
||||
nameIndex := -1
|
||||
if len(lines) > 0 {
|
||||
for i, field := range lines[0] {
|
||||
if field == "image_path" {
|
||||
imagePathIndex = i
|
||||
}
|
||||
if field == "name" {
|
||||
nameIndex = i
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if imagePathIndex == -1 {
|
||||
log.Fatal("Could not find 'image_path' field in header")
|
||||
}
|
||||
if nameIndex == -1 {
|
||||
log.Fatal("Could not find 'name' field in header")
|
||||
}
|
||||
|
||||
fmt.Printf("Found image_path field at index %d\n", imagePathIndex)
|
||||
fmt.Printf("Found name field at index %d\n", nameIndex)
|
||||
|
||||
// Generate timestamp for this run
|
||||
timestamp := time.Now().Unix()
|
||||
|
||||
// Process lines and replace image paths
|
||||
outputLines := make([][]string, len(lines))
|
||||
// Copy header as-is
|
||||
outputLines[0] = lines[0]
|
||||
|
||||
for i, line := range lines[1:] { // Skip header
|
||||
lineNum := i + 1 // Adjust for header skip
|
||||
if len(line) <= imagePathIndex || len(line) <= nameIndex {
|
||||
log.Printf("Warning: line %d has insufficient fields (%d), copying as-is", lineNum+1, len(line))
|
||||
outputLines[lineNum] = line
|
||||
continue
|
||||
}
|
||||
|
||||
// Copy the line
|
||||
newLine := make([]string, len(line))
|
||||
copy(newLine, line)
|
||||
|
||||
// Replace the image path with generated placeholder
|
||||
heroName := line[nameIndex]
|
||||
newImagePath := generateImagePath(timestamp, lineNum, heroName)
|
||||
newLine[imagePathIndex] = newImagePath
|
||||
|
||||
outputLines[lineNum] = newLine
|
||||
}
|
||||
|
||||
// Write output file
|
||||
err = writeTSVFile(outputPath, outputLines)
|
||||
if err != nil {
|
||||
log.Fatalf("Error writing output file: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Successfully wrote %d lines to %s\n", len(outputLines), outputPath)
|
||||
}
|
||||
|
||||
func generateImagePath(timestamp int64, index int, heroName string) string {
|
||||
// Create FNV-1a 64-bit hash of the hero name
|
||||
h := fnv.New64a()
|
||||
h.Write([]byte(heroName))
|
||||
hash := h.Sum64()
|
||||
|
||||
// Convert to hex string
|
||||
hashHex := fmt.Sprintf("%016x", hash)
|
||||
|
||||
// Generate path: "generated/{TIMESTAMP}_{INDEX}_{HASH}.png"
|
||||
return fmt.Sprintf("generated/%d_%d_%s.png", timestamp, index, hashHex)
|
||||
}
|
||||
|
||||
func writeTSVFile(filename string, lines [][]string) error {
|
||||
file, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
for _, line := range lines {
|
||||
// Join fields with tabs and write line
|
||||
output := strings.Join(line, "\t") + "\n"
|
||||
_, err := file.WriteString(output)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FieldIndices holds the column indices for each field
|
||||
type FieldIndices struct {
|
||||
Name int
|
||||
Strength int
|
||||
Agility int
|
||||
Constitution int
|
||||
Wisdom int
|
||||
Charisma int
|
||||
Sum int
|
||||
Vigor int
|
||||
Integrity int
|
||||
Ambition int
|
||||
Gregariousness int
|
||||
Bravery int
|
||||
Profession int
|
||||
GreatPerson int
|
||||
ImagePath int
|
||||
Gender int
|
||||
Backstory int
|
||||
Personality int
|
||||
}
|
||||
|
||||
// findFieldIndices parses the header row to find the index of each field
|
||||
func findFieldIndices(header []string) (*FieldIndices, error) {
|
||||
indices := &FieldIndices{
|
||||
Name: -1,
|
||||
Strength: -1,
|
||||
Agility: -1,
|
||||
Constitution: -1,
|
||||
Wisdom: -1,
|
||||
Charisma: -1,
|
||||
Sum: -1,
|
||||
Vigor: -1,
|
||||
Integrity: -1,
|
||||
Ambition: -1,
|
||||
Gregariousness: -1,
|
||||
Bravery: -1,
|
||||
Profession: -1,
|
||||
GreatPerson: -1,
|
||||
ImagePath: -1,
|
||||
Gender: -1,
|
||||
Backstory: -1,
|
||||
Personality: -1,
|
||||
}
|
||||
|
||||
fieldMap := map[string]*int{
|
||||
"name": &indices.Name,
|
||||
"strength": &indices.Strength,
|
||||
"agility": &indices.Agility,
|
||||
"constitution": &indices.Constitution,
|
||||
"wisdom": &indices.Wisdom,
|
||||
"charisma": &indices.Charisma,
|
||||
"sum": &indices.Sum,
|
||||
"vigor": &indices.Vigor,
|
||||
"integrity": &indices.Integrity,
|
||||
"ambition": &indices.Ambition,
|
||||
"gregariousness": &indices.Gregariousness,
|
||||
"bravery": &indices.Bravery,
|
||||
"profession": &indices.Profession,
|
||||
"great person": &indices.GreatPerson,
|
||||
"image_path": &indices.ImagePath,
|
||||
"gender": &indices.Gender,
|
||||
"backstory": &indices.Backstory,
|
||||
"personality": &indices.Personality,
|
||||
}
|
||||
|
||||
for i, field := range header {
|
||||
if ptr, exists := fieldMap[strings.ToLower(field)]; exists {
|
||||
*ptr = i
|
||||
}
|
||||
}
|
||||
|
||||
// Check that all required fields were found
|
||||
requiredFields := []string{"name", "strength", "agility", "constitution", "wisdom", "charisma", "sum", "vigor", "integrity", "ambition", "gregariousness", "bravery", "profession", "great person", "image_path", "gender", "backstory", "personality"}
|
||||
for _, field := range requiredFields {
|
||||
if ptr, exists := fieldMap[field]; exists && *ptr == -1 {
|
||||
return nil, fmt.Errorf("required field '%s' not found in header", field)
|
||||
}
|
||||
}
|
||||
|
||||
return indices, nil
|
||||
}
|
||||
|
||||
func parseHeroLine(fields []string, indices *FieldIndices) (*Hero, error) {
|
||||
maxIndex := 0
|
||||
indexValues := []int{indices.Name, indices.Strength, indices.Agility, indices.Constitution, indices.Wisdom, indices.Charisma, indices.Sum, indices.Vigor, indices.Integrity, indices.Ambition, indices.Gregariousness, indices.Bravery, indices.Profession, indices.GreatPerson, indices.ImagePath, indices.Gender, indices.Backstory, indices.Personality}
|
||||
for _, idx := range indexValues {
|
||||
if idx > maxIndex {
|
||||
maxIndex = idx
|
||||
}
|
||||
}
|
||||
|
||||
if len(fields) <= maxIndex {
|
||||
return nil, fmt.Errorf("insufficient fields: expected at least %d, got %d", maxIndex+1, len(fields))
|
||||
}
|
||||
|
||||
hero := &Hero{
|
||||
Name: fields[indices.Name],
|
||||
Profession: fields[indices.Profession],
|
||||
ImagePath: fields[indices.ImagePath],
|
||||
Gender: fields[indices.Gender],
|
||||
Backstory: fields[indices.Backstory],
|
||||
Personality: fields[indices.Personality],
|
||||
}
|
||||
|
||||
// Parse numeric fields
|
||||
var err error
|
||||
|
||||
if hero.Strength, err = strconv.Atoi(fields[indices.Strength]); err != nil {
|
||||
return nil, fmt.Errorf("invalid strength: %v", err)
|
||||
}
|
||||
|
||||
if hero.Agility, err = strconv.Atoi(fields[indices.Agility]); err != nil {
|
||||
return nil, fmt.Errorf("invalid agility: %v", err)
|
||||
}
|
||||
|
||||
if hero.Constitution, err = strconv.Atoi(fields[indices.Constitution]); err != nil {
|
||||
return nil, fmt.Errorf("invalid constitution: %v", err)
|
||||
}
|
||||
|
||||
if hero.Wisdom, err = strconv.Atoi(fields[indices.Wisdom]); err != nil {
|
||||
return nil, fmt.Errorf("invalid wisdom: %v", err)
|
||||
}
|
||||
|
||||
if hero.Charisma, err = strconv.Atoi(fields[indices.Charisma]); err != nil {
|
||||
return nil, fmt.Errorf("invalid charisma: %v", err)
|
||||
}
|
||||
|
||||
if hero.Sum, err = strconv.Atoi(fields[indices.Sum]); err != nil {
|
||||
return nil, fmt.Errorf("invalid sum: %v", err)
|
||||
}
|
||||
|
||||
if hero.Vigor, err = strconv.Atoi(fields[indices.Vigor]); err != nil {
|
||||
return nil, fmt.Errorf("invalid vigor: %v", err)
|
||||
}
|
||||
|
||||
if hero.Integrity, err = strconv.Atoi(fields[indices.Integrity]); err != nil {
|
||||
return nil, fmt.Errorf("invalid integrity: %v", err)
|
||||
}
|
||||
|
||||
if hero.Ambition, err = strconv.Atoi(fields[indices.Ambition]); err != nil {
|
||||
return nil, fmt.Errorf("invalid ambition: %v", err)
|
||||
}
|
||||
|
||||
if hero.Gregariousness, err = strconv.Atoi(fields[indices.Gregariousness]); err != nil {
|
||||
return nil, fmt.Errorf("invalid gregariousness: %v", err)
|
||||
}
|
||||
|
||||
if hero.Bravery, err = strconv.Atoi(fields[indices.Bravery]); err != nil {
|
||||
return nil, fmt.Errorf("invalid bravery: %v", err)
|
||||
}
|
||||
|
||||
// Parse boolean field
|
||||
switch strings.ToLower(fields[indices.GreatPerson]) {
|
||||
case "true":
|
||||
hero.GreatPerson = true
|
||||
case "false":
|
||||
hero.GreatPerson = false
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid great person value: %s", fields[indices.GreatPerson])
|
||||
}
|
||||
|
||||
return hero, nil
|
||||
}
|
||||
|
||||
func printHero(lineNum int, hero *Hero) {
|
||||
fmt.Printf("Hero #%d:\n", lineNum)
|
||||
fmt.Printf(" Name: %s\n", hero.Name)
|
||||
fmt.Printf(" Gender: %s\n", hero.Gender)
|
||||
fmt.Printf(" Profession: %s\n", hero.Profession)
|
||||
fmt.Printf(" Great Person: %t\n", hero.GreatPerson)
|
||||
fmt.Printf(" Image Path: %s\n", hero.ImagePath)
|
||||
fmt.Printf(" Stats:\n")
|
||||
fmt.Printf(" Strength: %d, Agility: %d, Constitution: %d\n", hero.Strength, hero.Agility, hero.Constitution)
|
||||
fmt.Printf(" Wisdom: %d, Charisma: %d, Sum: %d\n", hero.Wisdom, hero.Charisma, hero.Sum)
|
||||
fmt.Printf(" Vigor: %d, Integrity: %d, Ambition: %d\n", hero.Vigor, hero.Integrity, hero.Ambition)
|
||||
fmt.Printf(" Gregariousness: %d, Bravery: %d\n", hero.Gregariousness, hero.Bravery)
|
||||
fmt.Printf(" Personality: %s\n", hero.Personality)
|
||||
fmt.Printf(" Backstory: %.100s", hero.Backstory)
|
||||
if len(hero.Backstory) > 100 {
|
||||
fmt.Printf("...")
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
|
||||
|
||||
go_library(
|
||||
name = "heroformatter_lib",
|
||||
srcs = ["main.go"],
|
||||
importpath = "github.com/nolen777/eagle0/src/main/go/net/eagle0/util/hero_generation/heroformatter",
|
||||
visibility = ["//visibility:private"],
|
||||
deps = ["//src/main/go/net/eagle0/util/tsv"],
|
||||
)
|
||||
|
||||
go_binary(
|
||||
name = "heroformatter",
|
||||
embed = [":heroformatter_lib"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user