mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-29 09:35:42 +00:00
Compare commits
32
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a56567449a | ||
|
|
e105461692 | ||
|
|
2e03352dea | ||
|
|
8c19a93f3c | ||
|
|
9b2bce6537 | ||
|
|
ee4914dcc8 | ||
|
|
1e019c533a | ||
|
|
3d092e580f | ||
|
|
85e530c5a4 | ||
|
|
486960a6aa | ||
|
|
1cb2dd7b6a | ||
|
|
5483c732cc | ||
|
|
6402a8c283 | ||
|
|
eb762e1bae | ||
|
|
be31464e99 | ||
|
|
e6f9d4e4ac | ||
|
|
4d3b2ddb36 | ||
|
|
6edb4de0dc | ||
|
|
72a0f84105 | ||
|
|
f73798ae6e | ||
|
|
19691682e0 | ||
|
|
0e51bece68 | ||
|
|
a89f740b3b | ||
|
|
7959da0a5a | ||
|
|
bc119b2aab | ||
|
|
25c7788254 | ||
|
|
1246f8bcf6 | ||
|
|
f3e44fb9cf | ||
|
|
50aa61b77c | ||
|
|
facfcf9ac9 | ||
|
|
4c21368f96 | ||
|
|
7eccd69a01 |
+23
-17
@@ -201,30 +201,31 @@ The following actions still have proto usage, blocked by utility dependencies:
|
||||
|
||||
| Action | Proto Usage | Blocker |
|
||||
|--------|-------------|---------|
|
||||
| `EndBattleAftermathPhaseAction` | 1 `toProto` call | `ProvinceViewFilter` needs Scala types |
|
||||
| `EndBattleAftermathPhaseAction` | **Unblocked** | `ProvinceViewFilter` Scala overload added (PR #4752) |
|
||||
| `NewRoundAction` | 1 `fromProto` call | `ChronicleEventGenerator` returns proto |
|
||||
| `EndHandleRiotsPhaseAction` | 1 `toProto` call | `CommandChoiceHelpers` takes proto GameState |
|
||||
| `PerformVassalCommandsPhaseAction` | 1 `toProto` call | `CommandChoiceHelpers` takes proto GameState |
|
||||
| `PerformVassalDefenseDecisionsAction` | 1 `toProto` call | `CommandChoiceHelpers` takes proto GameState |
|
||||
| `EndVassalCommandsPhaseAction` | 1 `toProto` call | `CommandChoiceHelpers` takes proto GameState |
|
||||
| `PerformReconResolutionAction` | Uses `ProvinceViewFilter` | `ProvinceViewFilter` needs Scala types |
|
||||
| `PerformReconResolutionAction` | **Unblocked** | `ProvinceViewFilter` Scala overload added (PR #4752) |
|
||||
| `ResolveBattleAction` | Heavy proto usage | Large refactor needed |
|
||||
|
||||
### Estimated Effort (Remaining)
|
||||
|
||||
| Component | Lines | Complexity |
|
||||
|-----------|-------|------------|
|
||||
| `ProvinceViewFilter` to Scala | ~150 | Medium |
|
||||
| `ProvinceViewFilter` faction-filtered views | ~100 | Medium (PR #4752 completed server-side) |
|
||||
| `CommandChoiceHelpers` to Scala | ~2000 | High |
|
||||
| `ChronicleEventGenerator` to Scala | ~400 | Medium |
|
||||
| History API updates | ~100 | Low |
|
||||
| **Total Remaining** | **~2650** | |
|
||||
| **Total Remaining** | **~2600** | |
|
||||
|
||||
### Validation
|
||||
- [x] `ActionResultApplier` created and tested
|
||||
- [x] `RandomStateSequencer` threads Scala GameState throughout
|
||||
- [x] `RoundPhaseAdvancer` uses T-types internally
|
||||
- [ ] `ProvinceViewFilter` uses Scala types
|
||||
- [x] `ProvinceViewFilter` has Scala overload for server-side use (PR #4752)
|
||||
- [ ] `ProvinceViewFilter` faction-filtered views use Scala types
|
||||
- [ ] `CommandChoiceHelpers` uses Scala types
|
||||
- [ ] History APIs vend Scala types
|
||||
- [ ] No `ActionResultProtoConverter.toProto()` calls except at persistence/gRPC boundaries
|
||||
@@ -248,22 +249,27 @@ Remove remaining direct proto imports from utility classes.
|
||||
| `BattalionTypeLoader.scala` | Keep proto for file loading, convert immediately after |
|
||||
| `BeastUtils.scala` | **Complete** - now uses Scala `BeastInfo` only |
|
||||
|
||||
### View Filters (Blocking Full Deproto)
|
||||
### View Filters (Partially Complete)
|
||||
|
||||
The `ProvinceViewFilter` utility currently works entirely with proto types, blocking full deproto of actions that generate province views:
|
||||
The view filter utilities now have Scala overloads for server-side use:
|
||||
|
||||
| File | Issue | Needed |
|
||||
|------|-------|--------|
|
||||
| `ProvinceViewFilter.scala` | Takes proto `Province`/`GameState`, returns proto `ProvinceView` | Scala `ProvinceViewT` model |
|
||||
| `GameStateViewFilter.scala` | Uses proto types throughout | Depends on `ProvinceViewT` |
|
||||
| `GameStateViewDiffer.scala` | Works with view protos | Depends on `ProvinceViewT` |
|
||||
| File | Status | Notes |
|
||||
|------|--------|-------|
|
||||
| `ProvinceViewFilter.scala` | **Partial** | `filteredProvinceView(ProvinceT, ScalaGameState)` added (PR #4752) |
|
||||
| `ArmyFilter.scala` | **Partial** | `filterArmy(ScalaArmy, Map[BattalionId, BattalionT], Option[FactionId])` added |
|
||||
| `BattalionViewFilter.scala` | **Complete** | Uses Scala `BattalionT` throughout |
|
||||
| `GameStateViewFilter.scala` | Pending | Uses proto types throughout |
|
||||
| `GameStateViewDiffer.scala` | Pending | Works with view protos |
|
||||
|
||||
**Blocked Actions**:
|
||||
- `EndBattleAftermathPhaseAction` - uses `ProvinceViewFilter` for `revelationChange`, requires lazy proto conversion
|
||||
- `PerformReconResolutionAction` - uses `ProvinceViewFilter` for reconned provinces
|
||||
- `GameStateFactionExtensions` - uses `ProvinceViewFilter` for `updatedReconnedProvinces`
|
||||
**Unblocked Actions** (PR #4752):
|
||||
- `EndBattleAftermathPhaseAction` - can now use `filteredProvinceView(province, scalaGameState)`
|
||||
- `PerformReconResolutionAction` - can now use Scala overload
|
||||
- `GameStateFactionExtensions` - can now use `updatedReconnedProvinces` with Scala types
|
||||
|
||||
**Solution**: Create Scala `ProvinceViewT` (and possibly `ProvinceViewC`) that mirrors the proto `ProvinceView`. Then create a protoless `ProvinceViewFilter` that operates on Scala types. The proto version can delegate to the Scala version + convert, or we maintain both during transition.
|
||||
**Remaining Work**:
|
||||
- Faction-filtered `filteredProvinceView(Province, GameState, FactionId)` still uses proto types
|
||||
- `withdrawnFromProvinceView` still uses proto types
|
||||
- These are needed for client-facing views with visibility restrictions
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -65,11 +65,6 @@ private:
|
||||
const GameStateW& guessedState,
|
||||
const CommandListSPtr& realAvailableCommands) const -> CommandChoiceResults;
|
||||
|
||||
[[nodiscard]] auto ChooseCommandIndex(
|
||||
const GameSettingsSPtr& settings,
|
||||
const net::eagle0::shardok::api::GameStateView& gsv,
|
||||
const CommandListSPtr& realAvailableCommands) const -> CommandChoiceResults;
|
||||
|
||||
public:
|
||||
explicit ShardokAIClient(
|
||||
PlayerId playerId,
|
||||
@@ -86,6 +81,12 @@ public:
|
||||
[[nodiscard]] auto ChooseCommandIndex(const ShardokEngine& engine) const
|
||||
-> CommandChoiceResults;
|
||||
|
||||
// Overload that works on copies of state - allows caller to release lock during AI thinking
|
||||
[[nodiscard]] auto ChooseCommandIndex(
|
||||
const GameSettingsSPtr& settings,
|
||||
const net::eagle0::shardok::api::GameStateView& gsv,
|
||||
const CommandListSPtr& realAvailableCommands) const -> CommandChoiceResults;
|
||||
|
||||
// MCTS configuration methods (only relevant when using MCTS algorithm)
|
||||
[[nodiscard]] auto GetMCTSConfig() const -> const mcts::MCTSConfig& { return mctsConfig; }
|
||||
void SetMCTSConfig(const mcts::MCTSConfig& config) { mctsConfig = config; }
|
||||
|
||||
@@ -24,9 +24,6 @@ using std::scoped_lock;
|
||||
using std::string;
|
||||
using std::unique_lock;
|
||||
using std::weak_ptr;
|
||||
using std::chrono::duration;
|
||||
|
||||
static constexpr duration kWaitForUpdatesDuration = std::chrono::milliseconds(5000);
|
||||
|
||||
using net::eagle0::shardok::common::GameStatus;
|
||||
|
||||
@@ -111,17 +108,67 @@ void ShardokGameController::DoAIThread() {
|
||||
if (aiClients.empty()) { printf("No AI players, exiting AI thread.\n"); }
|
||||
|
||||
while (aiThreadKeepGoing) {
|
||||
while (incomingRegistrations > 0) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
// Phase 1: Gather data for AI decision (brief lock)
|
||||
shared_ptr<ShardokAIClient> aiClient;
|
||||
PlayerId playerId;
|
||||
GameSettingsSPtr settings;
|
||||
net::eagle0::shardok::api::GameStateView gsv;
|
||||
CommandListSPtr availableCommands;
|
||||
size_t expectedHistoryCount;
|
||||
|
||||
{
|
||||
unique_lock lk(masterLock);
|
||||
|
||||
if (engine->GameIsOver()) {
|
||||
aiThreadKeepGoing = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
playerId = engine->GetCurrentPlayerId();
|
||||
aiClient = LockedAIClientForPid(playerId);
|
||||
|
||||
if (!aiClient) {
|
||||
// Not an AI player's turn - wait for signal
|
||||
aiCondition.wait(lk);
|
||||
aiThreadKeepGoing = aiThreadKeepGoing && !engine->GameIsOver();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get copies of everything the AI needs
|
||||
settings = engine->GetGameSettings();
|
||||
gsv = engine->GetGameStateView(playerId);
|
||||
availableCommands = engine->GetAvailableCommandsForAIPlayer(playerId);
|
||||
expectedHistoryCount = engine->GetUnfilteredHistoryCount();
|
||||
}
|
||||
// Lock released - polls can now get through
|
||||
|
||||
if (availableCommands->empty()) {
|
||||
printf("no commands for player %d\n", playerId);
|
||||
continue;
|
||||
}
|
||||
|
||||
unique_lock lk(masterLock);
|
||||
if (LockedCheckOneAICommand()) {
|
||||
// Phase 2: AI thinks (NO LOCK - this is the slow part)
|
||||
const auto results = aiClient->ChooseCommandIndex(settings, gsv, availableCommands);
|
||||
|
||||
// Phase 3: Post the command (brief lock)
|
||||
{
|
||||
unique_lock lk(masterLock);
|
||||
|
||||
// Verify state hasn't changed while we were thinking
|
||||
if (engine->GetUnfilteredHistoryCount() != expectedHistoryCount) {
|
||||
// State changed (e.g., human posted command) - re-evaluate
|
||||
printf("AI: State changed while thinking, re-evaluating\n");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (engine->GameIsOver()) {
|
||||
aiThreadKeepGoing = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
engine->PostCommand(playerId, results.chosenIndex);
|
||||
LockedNotifyClients();
|
||||
aiThreadKeepGoing = aiThreadKeepGoing && !engine->GameIsOver();
|
||||
} else {
|
||||
aiCondition.wait(lk);
|
||||
aiThreadKeepGoing = aiThreadKeepGoing && !engine->GameIsOver();
|
||||
aiThreadKeepGoing = !engine->GameIsOver();
|
||||
}
|
||||
}
|
||||
printf("Exiting AI thread.\n");
|
||||
@@ -136,24 +183,6 @@ ShardokGameController::~ShardokGameController() {
|
||||
aiThread.join();
|
||||
}
|
||||
|
||||
auto ShardokGameController::LockedCheckOneAICommand() -> bool {
|
||||
if (engine->GameIsOver()) {
|
||||
printf("Game is over!\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
const PlayerId currentPid = engine->GetCurrentPlayerId();
|
||||
if (const shared_ptr<ShardokAIClient> currentPlayerClient = LockedAIClientForPid(currentPid)) {
|
||||
const int index = currentPlayerClient->ChooseCommandIndex(*engine).chosenIndex;
|
||||
|
||||
engine->PostCommand(currentPid, index);
|
||||
LockedNotifyClients();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void CheckFactionId(
|
||||
const unique_ptr<ShardokEngine> &engine,
|
||||
const PlayerId shardokPlayerId,
|
||||
@@ -229,6 +258,7 @@ void ShardokGameController::PostPlacementCommands(
|
||||
}
|
||||
|
||||
auto ShardokGameController::GetCurrentGameStateBytes() -> byte_vector {
|
||||
scoped_lock<mutex> guard(masterLock);
|
||||
return engine->GetCurrentGameStateBytes();
|
||||
}
|
||||
|
||||
@@ -245,15 +275,9 @@ auto ShardokGameController::GetUpdates(const int64_t startingActionId) -> AllUpd
|
||||
awrs = engine->GetGameHistory(startingActionId);
|
||||
incomingRegistrations--;
|
||||
|
||||
// If the current player is an AI, wait for some results to post. Otherwise go ahead and
|
||||
// return, we might be telling the caller about available commands.
|
||||
if (awrs.empty() && !engine->GameIsOver() &&
|
||||
engine->GetPlayerInfos()[engine->GetCurrentPlayerId()].is_ai()) {
|
||||
updateCondition.wait_for(guard, kWaitForUpdatesDuration);
|
||||
incomingRegistrations++;
|
||||
awrs = engine->GetGameHistory(startingActionId);
|
||||
incomingRegistrations--;
|
||||
}
|
||||
// Note: Previously this had a 5-second wait for AI players to support long-polling.
|
||||
// With streaming (WaitForUpdatesAndPush), the caller already waits for updates,
|
||||
// so this wait is no longer needed. GetGameStatus is deprecated in favor of streaming.
|
||||
|
||||
updates.mainResults.reserve(awrs.size());
|
||||
std::ranges::transform(
|
||||
@@ -292,6 +316,9 @@ auto ShardokGameController::GetUpdates(const int64_t startingActionId) -> AllUpd
|
||||
-1,
|
||||
engine->FilterNewResults(-1, startingActionId),
|
||||
nullptr);
|
||||
|
||||
auto gameStateBytes = engine->GetCurrentGameStateBytes();
|
||||
updates.currentGameState.swap(gameStateBytes);
|
||||
}
|
||||
|
||||
return updates;
|
||||
@@ -321,4 +348,74 @@ auto ShardokGameController::ResolvedPlayerInfos()
|
||||
return engine->GetPlayerInfos();
|
||||
}
|
||||
|
||||
void ShardokGameController::RegisterSubscriber(std::shared_ptr<StreamSubscriber> subscriber) {
|
||||
scoped_lock<mutex> guard(subscriberLock);
|
||||
subscribers.push_back(subscriber);
|
||||
}
|
||||
|
||||
void ShardokGameController::UnregisterSubscriber(const StreamSubscriber *subscriber) {
|
||||
scoped_lock<mutex> guard(subscriberLock);
|
||||
subscribers.erase(
|
||||
std::remove_if(
|
||||
subscribers.begin(),
|
||||
subscribers.end(),
|
||||
[subscriber](const std::weak_ptr<StreamSubscriber> &weakSub) {
|
||||
auto sub = weakSub.lock();
|
||||
return !sub || sub.get() == subscriber;
|
||||
}),
|
||||
subscribers.end());
|
||||
}
|
||||
|
||||
auto ShardokGameController::WaitForUpdatesAndPush(
|
||||
std::shared_ptr<StreamSubscriber> subscriber,
|
||||
int64_t startingActionId) -> bool {
|
||||
int64_t lastPushedActionId = startingActionId;
|
||||
|
||||
while (subscriber->IsActive()) {
|
||||
bool gameOver = false;
|
||||
GameOverInfo gameOverInfo{};
|
||||
|
||||
{
|
||||
unique_lock<mutex> guard(masterLock);
|
||||
|
||||
// Wait for updates or game over
|
||||
updateCondition.wait(guard, [this, lastPushedActionId] {
|
||||
return engine->GetUnfilteredHistoryCount() >
|
||||
static_cast<size_t>(lastPushedActionId) ||
|
||||
engine->GameIsOver();
|
||||
});
|
||||
|
||||
if (!subscriber->IsActive()) { return false; }
|
||||
|
||||
gameOver = engine->GameIsOver();
|
||||
|
||||
if (gameOver) {
|
||||
gameOverInfo.gameStatus = fb::ToProto(engine->GetGameStatus());
|
||||
gameOverInfo.playerInfos = engine->GetPlayerInfos();
|
||||
gameOverInfo.endGameUnits = engine->EndGameUnits();
|
||||
}
|
||||
}
|
||||
// Lock released - GetUpdates will acquire its own lock
|
||||
|
||||
if (gameOver) {
|
||||
subscriber->OnGameOver(gameOverInfo);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get updates outside the lock (GetUpdates acquires masterLock internally)
|
||||
AllUpdates updates = GetUpdates(lastPushedActionId);
|
||||
lastPushedActionId = updates.newUnfilteredCount;
|
||||
|
||||
if (!updates.mainResults.empty()) {
|
||||
subscriber->OnUpdate(
|
||||
updates.mainResults,
|
||||
updates.filteredResults,
|
||||
updates.newUnfilteredCount,
|
||||
updates.currentGameState);
|
||||
}
|
||||
}
|
||||
|
||||
return false; // Subscriber disconnected
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#define ShardokGameController_hpp
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
@@ -29,6 +30,50 @@ using std::shared_ptr;
|
||||
using std::unique_ptr;
|
||||
using std::weak_ptr;
|
||||
|
||||
// Forward declaration
|
||||
class ShardokGameController;
|
||||
|
||||
/// Info about a game that has ended, for notifying subscribers
|
||||
struct GameOverInfo {
|
||||
vector<net::eagle0::shardok::common::PlayerInfo> playerInfos;
|
||||
vector<net::eagle0::shardok::storage::ResolvedUnit> endGameUnits;
|
||||
net::eagle0::shardok::common::GameStatus gameStatus;
|
||||
};
|
||||
|
||||
/// Updates for a single player (includes faction ID for client routing)
|
||||
struct OnePlayerUpdates {
|
||||
int32_t eagleFactionId;
|
||||
vector<ActionResultView> resultViews;
|
||||
shared_ptr<AvailableCommands> availableCommands;
|
||||
|
||||
OnePlayerUpdates(
|
||||
const int32_t fid,
|
||||
const vector<ActionResultView>& arvs,
|
||||
const shared_ptr<AvailableCommands>& acs)
|
||||
: eagleFactionId(fid),
|
||||
resultViews(arvs),
|
||||
availableCommands(acs) {}
|
||||
};
|
||||
|
||||
/// Interface for subscribers that receive streaming updates from a game
|
||||
class StreamSubscriber {
|
||||
public:
|
||||
virtual ~StreamSubscriber() = default;
|
||||
|
||||
/// Called when new game updates are available
|
||||
virtual void OnUpdate(
|
||||
const vector<ActionResult>& mainResults,
|
||||
const vector<OnePlayerUpdates>& filteredResults,
|
||||
int32_t newUnfilteredCount,
|
||||
const byte_vector& currentGameState) = 0;
|
||||
|
||||
/// Called when the game ends
|
||||
virtual void OnGameOver(const GameOverInfo& info) = 0;
|
||||
|
||||
/// Returns true if this subscriber is still active and should receive updates
|
||||
[[nodiscard]] virtual auto IsActive() const -> bool = 0;
|
||||
};
|
||||
|
||||
class ShardokGameController {
|
||||
private:
|
||||
// This lock should be held any time we call into engine or modify clients.
|
||||
@@ -39,10 +84,17 @@ private:
|
||||
// Fires whenever there is a new game state update.
|
||||
mutable std::condition_variable updateCondition{};
|
||||
|
||||
// Stream subscribers - protected by separate lock to avoid deadlock with masterLock
|
||||
mutable std::mutex subscriberLock{};
|
||||
std::vector<std::weak_ptr<StreamSubscriber>> subscribers{};
|
||||
|
||||
string serializedRequest;
|
||||
|
||||
unique_ptr<ShardokEngine> engine;
|
||||
|
||||
// Cached immutable data - safe to access without lock since it never changes after construction
|
||||
const GameId cachedGameId;
|
||||
|
||||
std::atomic_int incomingRegistrations = 0;
|
||||
|
||||
const string mapName;
|
||||
@@ -60,11 +112,9 @@ private:
|
||||
|
||||
void LockedNotifyClients() const;
|
||||
|
||||
auto LockedCheckOneAICommand() -> bool;
|
||||
|
||||
auto LockedAIClientForPid(PlayerId pid) const -> shared_ptr<ShardokAIClient>;
|
||||
|
||||
static vector<shared_ptr<ShardokAIClient>> MakeAIClients(const unique_ptr<ShardokEngine> &e);
|
||||
static vector<shared_ptr<ShardokAIClient>> MakeAIClients(const unique_ptr<ShardokEngine>& e);
|
||||
|
||||
void DoAIThread();
|
||||
|
||||
@@ -75,6 +125,7 @@ public:
|
||||
string serializedRequest = "")
|
||||
: serializedRequest(std::move(serializedRequest)),
|
||||
engine(std::move(e)),
|
||||
cachedGameId(engine->GetGameId()),
|
||||
mapName(std::move(mapName)),
|
||||
logFilePath(MakeLogFilePath()),
|
||||
aiClients(MakeAIClients(engine)),
|
||||
@@ -98,26 +149,13 @@ public:
|
||||
PlayerId shardokPlayerId,
|
||||
int eagleFactionId,
|
||||
int64_t token,
|
||||
const vector<UnitPlacementInfo> &infos);
|
||||
|
||||
struct OnePlayerUpdates {
|
||||
int32_t eagleFactionId;
|
||||
vector<ActionResultView> resultViews;
|
||||
shared_ptr<AvailableCommands> availableCommands;
|
||||
|
||||
OnePlayerUpdates(
|
||||
const int32_t fid,
|
||||
const vector<ActionResultView> &arvs,
|
||||
const shared_ptr<AvailableCommands> &acs)
|
||||
: eagleFactionId(fid),
|
||||
resultViews(arvs),
|
||||
availableCommands(acs) {}
|
||||
};
|
||||
const vector<UnitPlacementInfo>& infos);
|
||||
|
||||
struct AllUpdates {
|
||||
vector<ActionResult> mainResults;
|
||||
vector<OnePlayerUpdates> filteredResults;
|
||||
int32_t newUnfilteredCount;
|
||||
byte_vector currentGameState;
|
||||
};
|
||||
auto GetUpdates(int64_t startingActionId) -> AllUpdates;
|
||||
auto GetCurrentGameStateBytes() -> byte_vector;
|
||||
@@ -127,13 +165,23 @@ public:
|
||||
auto ResolvedPlayerInfos() -> vector<net::eagle0::shardok::common::PlayerInfo>;
|
||||
auto EndGameUnits() -> vector<net::eagle0::shardok::storage::ResolvedUnit>;
|
||||
|
||||
[[nodiscard]] auto GetGameId() const -> GameId { return engine->GetGameId(); }
|
||||
|
||||
[[nodiscard]] auto GetHexMap() const -> const HexMap * {
|
||||
return engine->GetCurrentGameState()->hex_map();
|
||||
}
|
||||
[[nodiscard]] auto GetGameId() const -> GameId { return cachedGameId; }
|
||||
|
||||
[[nodiscard]] auto GetLogFilePath() const -> string { return logFilePath; }
|
||||
|
||||
/// Register a subscriber to receive streaming updates for this game.
|
||||
/// The subscriber will receive updates until it becomes inactive or is unregistered.
|
||||
void RegisterSubscriber(std::shared_ptr<StreamSubscriber> subscriber);
|
||||
|
||||
/// Unregister a subscriber. Safe to call even if the subscriber was never registered.
|
||||
void UnregisterSubscriber(const StreamSubscriber* subscriber);
|
||||
|
||||
/// Wait for game updates, pushing them to the given subscriber.
|
||||
/// Blocks until the game ends or the subscriber becomes inactive.
|
||||
/// Returns true if the game ended normally, false if subscriber disconnected.
|
||||
auto WaitForUpdatesAndPush(
|
||||
std::shared_ptr<StreamSubscriber> subscriber,
|
||||
int64_t startingActionId) -> bool;
|
||||
};
|
||||
} // namespace shardok
|
||||
|
||||
|
||||
@@ -529,6 +529,125 @@ auto FromInternalStatus(
|
||||
throw ShardokInternalErrorException("Bad unit status on resolved unit");
|
||||
}
|
||||
|
||||
auto EagleInterfaceImpl::SubscribeToGame(
|
||||
ServerContext *context,
|
||||
const GameSubscriptionRequest *request,
|
||||
grpc::ServerWriter<GameStatusResponse> *writer) -> Status {
|
||||
shared_ptr<ShardokGameController> controller;
|
||||
try {
|
||||
controller = ControllerForGame(request->game_id(), request->game_setup_info());
|
||||
} catch (NewGameException &e) { return e.GetStatus(); }
|
||||
|
||||
if (!controller) { return Status(StatusCode::NOT_FOUND, "Game not found"); }
|
||||
|
||||
// Send initial state
|
||||
GameStatusResponse initialResponse;
|
||||
PopulateGameStatusResponse(
|
||||
controller,
|
||||
request->game_setup_info().known_result_count(),
|
||||
&initialResponse);
|
||||
if (!writer->Write(initialResponse)) {
|
||||
return Status::OK; // Client disconnected
|
||||
}
|
||||
|
||||
// If game was already over, we're done
|
||||
if (initialResponse.has_game_over_response()) { return Status::OK; }
|
||||
|
||||
// Create a subscriber that writes to the gRPC stream
|
||||
class GrpcStreamSubscriber : public StreamSubscriber {
|
||||
private:
|
||||
grpc::ServerWriter<GameStatusResponse> *writer_;
|
||||
ServerContext *context_;
|
||||
std::atomic<bool> active_{true};
|
||||
std::string gameId_;
|
||||
|
||||
public:
|
||||
GrpcStreamSubscriber(
|
||||
grpc::ServerWriter<GameStatusResponse> *w,
|
||||
ServerContext *ctx,
|
||||
std::string gameId)
|
||||
: writer_(w),
|
||||
context_(ctx),
|
||||
gameId_(std::move(gameId)) {}
|
||||
|
||||
void OnUpdate(
|
||||
const vector<ActionResult> &mainResults,
|
||||
const vector<OnePlayerUpdates> &filteredResults,
|
||||
int32_t newUnfilteredCount,
|
||||
const byte_vector ¤tGameState) override {
|
||||
if (!active_) return;
|
||||
|
||||
GameStatusResponse response;
|
||||
response.set_game_id(gameId_);
|
||||
|
||||
response.mutable_game_update_response()->mutable_update_responses()->Add(
|
||||
begin(mainResults),
|
||||
end(mainResults));
|
||||
|
||||
// Add filtered results for each player with their faction IDs
|
||||
for (const auto &playerUpdate : filteredResults) {
|
||||
auto *filtered =
|
||||
response.mutable_game_update_response()->add_filtered_update_responses();
|
||||
filtered->set_eagle_faction_id(playerUpdate.eagleFactionId);
|
||||
filtered->mutable_action_result_views()->Add(
|
||||
begin(playerUpdate.resultViews),
|
||||
end(playerUpdate.resultViews));
|
||||
if (playerUpdate.availableCommands) {
|
||||
*filtered->mutable_available_commands() = *playerUpdate.availableCommands;
|
||||
}
|
||||
}
|
||||
|
||||
response.mutable_game_update_response()->set_total_action_result_count(
|
||||
newUnfilteredCount);
|
||||
*response.mutable_game_update_response()->mutable_current_game_state() =
|
||||
std::string(currentGameState.begin(), currentGameState.end());
|
||||
|
||||
if (!writer_->Write(response)) { active_ = false; }
|
||||
}
|
||||
|
||||
void OnGameOver(const GameOverInfo &info) override {
|
||||
if (!active_) return;
|
||||
|
||||
GameStatusResponse response;
|
||||
response.set_game_id(gameId_);
|
||||
|
||||
PopulateGameOverResponse(
|
||||
gameId_,
|
||||
info.gameStatus,
|
||||
info.playerInfos,
|
||||
info.endGameUnits,
|
||||
response.mutable_game_over_response());
|
||||
|
||||
writer_->Write(response);
|
||||
active_ = false;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto IsActive() const -> bool override {
|
||||
return active_ && !context_->IsCancelled();
|
||||
}
|
||||
};
|
||||
|
||||
auto subscriber =
|
||||
std::make_shared<GrpcStreamSubscriber>(writer, context, controller->GetGameId());
|
||||
|
||||
controller->RegisterSubscriber(subscriber);
|
||||
|
||||
// Wait for updates and push them until game ends or subscriber disconnects
|
||||
bool gameEnded = controller->WaitForUpdatesAndPush(
|
||||
subscriber,
|
||||
request->game_setup_info().known_result_count());
|
||||
|
||||
controller->UnregisterSubscriber(subscriber.get());
|
||||
|
||||
if (gameEnded) {
|
||||
printf("SubscribeToGame: Game ended normally\n");
|
||||
} else {
|
||||
printf("SubscribeToGame: Subscriber disconnected\n");
|
||||
}
|
||||
|
||||
return Status::OK;
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
#ifndef NDEBUG
|
||||
|
||||
@@ -33,6 +33,7 @@ using grpc::Status;
|
||||
using net::eagle0::common::GameSetupInfo;
|
||||
using net::eagle0::common::GameStatusRequest;
|
||||
using net::eagle0::common::GameStatusResponse;
|
||||
using net::eagle0::common::GameSubscriptionRequest;
|
||||
using net::eagle0::common::HexMapNamesRequest;
|
||||
using net::eagle0::common::HexMapNamesResponse;
|
||||
using net::eagle0::common::HexMapRequest;
|
||||
@@ -78,6 +79,11 @@ public:
|
||||
ServerContext* context,
|
||||
const HexMapNamesRequest* request,
|
||||
HexMapNamesResponse* response) -> Status override;
|
||||
|
||||
auto SubscribeToGame(
|
||||
ServerContext* context,
|
||||
const GameSubscriptionRequest* request,
|
||||
grpc::ServerWriter<GameStatusResponse>* writer) -> Status override;
|
||||
};
|
||||
} // namespace shardok
|
||||
|
||||
|
||||
+14
-1
@@ -120,6 +120,19 @@ namespace eagle {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Force the circuit breaker to allow an immediate reconnect attempt.
|
||||
/// Resets the state to HalfOpen so the next connection will be a test.
|
||||
/// </summary>
|
||||
public void ForceReconnect() {
|
||||
lock (this) {
|
||||
if (_state == State.Open) {
|
||||
_state = State.HalfOpen;
|
||||
_logger.LogLine("[CIRCUIT] OPEN → HALF_OPEN (forced by user)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get human-readable status for UI display.
|
||||
/// </summary>
|
||||
@@ -138,7 +151,7 @@ namespace eagle {
|
||||
var timeUntilTest = OpenTimeoutSeconds -
|
||||
(DateTime.UtcNow - _openedAt.Value).TotalSeconds;
|
||||
if (timeUntilTest > 0) {
|
||||
return $"Server unavailable. Retrying in {(int)timeUntilTest}s";
|
||||
return $"Server unavailable. Retrying in {Math.Min(10, (int)timeUntilTest)}s";
|
||||
}
|
||||
}
|
||||
return "Server unavailable. Testing...";
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using Net.Eagle0.Eagle.Api;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace eagle {
|
||||
/// <summary>
|
||||
@@ -26,6 +27,9 @@ namespace eagle {
|
||||
private PersistentClientConnection _connection;
|
||||
private IGameStateProvider _gameStateProvider;
|
||||
|
||||
[Tooltip("Optional button to force immediate reconnection when server is down")]
|
||||
public Button retryButton;
|
||||
|
||||
// Update interval in seconds
|
||||
private const float UpdateInterval = 0.5f;
|
||||
private float _timeSinceLastUpdate = 0f;
|
||||
@@ -39,9 +43,10 @@ namespace eagle {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the PersistentClientConnection - this assumes it's accessible
|
||||
// In production, this would need proper dependency injection
|
||||
// For now, the connection will be set externally or found via another method
|
||||
if (retryButton != null) {
|
||||
retryButton.onClick.AddListener(OnRetryClicked);
|
||||
retryButton.gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -73,11 +78,12 @@ namespace eagle {
|
||||
// Check circuit breaker state first - it takes precedence
|
||||
var circuitState = _connection.CircuitBreaker.CurrentState;
|
||||
if (circuitState == ConnectionCircuitBreaker.State.Open) {
|
||||
SetRetryButtonVisible(true);
|
||||
var nextTest = _connection.CircuitBreaker.NextTestAttempt;
|
||||
if (nextTest.HasValue) {
|
||||
var timeUntilTest = nextTest.Value - DateTime.UtcNow;
|
||||
if (timeUntilTest.TotalSeconds > 0) {
|
||||
int seconds = (int)Math.Ceiling(timeUntilTest.TotalSeconds);
|
||||
int seconds = Math.Min(10, (int)Math.Ceiling(timeUntilTest.TotalSeconds));
|
||||
_textComponent.text =
|
||||
$"<color=red>●</color> Server down. Retry in {seconds}s";
|
||||
return;
|
||||
@@ -86,6 +92,7 @@ namespace eagle {
|
||||
_textComponent.text = "<color=red>●</color> Server unavailable";
|
||||
return;
|
||||
} else if (circuitState == ConnectionCircuitBreaker.State.HalfOpen) {
|
||||
SetRetryButtonVisible(false);
|
||||
_textComponent.text = "<color=yellow>●</color> Testing connection...";
|
||||
return;
|
||||
}
|
||||
@@ -94,6 +101,11 @@ namespace eagle {
|
||||
var state = _connection.CurrentState;
|
||||
var nextAttempt = _connection.NextReconnectAttempt;
|
||||
|
||||
// Show retry button if we're counting down to a reconnect attempt
|
||||
bool isCountingDown = state == ConnectionState.Reconnecting && nextAttempt.HasValue &&
|
||||
(nextAttempt.Value - DateTime.UtcNow).TotalSeconds > 0;
|
||||
SetRetryButtonVisible(isCountingDown);
|
||||
|
||||
string statusText = state switch {
|
||||
ConnectionState.Connected => GetConnectedStatusText(),
|
||||
ConnectionState.Connecting => "<color=yellow>●</color> Connecting...",
|
||||
@@ -142,8 +154,16 @@ namespace eagle {
|
||||
return "<color=yellow>●</color> Reconnecting...";
|
||||
}
|
||||
|
||||
int secondsRemaining = (int)Math.Ceiling(timeUntilRetry.TotalSeconds);
|
||||
int secondsRemaining = Math.Min(10, (int)Math.Ceiling(timeUntilRetry.TotalSeconds));
|
||||
return $"<color=orange>●</color> Retry in {secondsRemaining}s";
|
||||
}
|
||||
|
||||
private void SetRetryButtonVisible(bool visible) {
|
||||
if (retryButton != null) { retryButton.gameObject.SetActive(visible); }
|
||||
}
|
||||
|
||||
private void OnRetryClicked() {
|
||||
if (_connection != null) { _connection.ForceReconnect(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,12 +229,11 @@ namespace eagle {
|
||||
|
||||
void OnApplicationPause(bool pause) {
|
||||
if (ModelUpdater == null) { return; }
|
||||
if (pause) {
|
||||
ModelUpdater.StopListeningForUpdates();
|
||||
} else {
|
||||
// Fire-and-forget - subscription is awaited internally and failures are logged
|
||||
_ = ModelUpdater.StartListeningForUpdates();
|
||||
}
|
||||
// Don't unsubscribe on pause - this caused a race condition where the subscriber
|
||||
// could be lost if pause happened before the async StartListeningForUpdates completed.
|
||||
// With MainQueue rate-limiting, keeping the subscription during pause is safe.
|
||||
// Reconnects will continue to work, and updates will queue up and be processed on
|
||||
// resume.
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
@@ -43,7 +43,7 @@ namespace eagle {
|
||||
TokenId? CommandToken { get; }
|
||||
TokenId LastPostedToken { get; }
|
||||
|
||||
Dictionary<String, ShardokGameModel> ShardokGameModels { get; }
|
||||
ConcurrentDictionary<String, ShardokGameModel> ShardokGameModels { get; }
|
||||
|
||||
private bool ShardokGameModelIsRunning(ShardokGameModel sgm) =>
|
||||
sgm.GameStatus.State == GameStatus.Types.State.GameRunning
|
||||
@@ -105,10 +105,9 @@ namespace eagle {
|
||||
_currentModel.ShardokGameModels
|
||||
.Select(sgm => {
|
||||
var needsResync = _shardokNeedsResync.GetValueOrDefault(sgm.Key, false);
|
||||
// Use thread-safe count from gRPC thread updates, fall back to model
|
||||
var count = _shardokResultCounts.GetValueOrDefault(
|
||||
sgm.Key,
|
||||
sgm.Value.History.Count());
|
||||
// Use thread-safe count from gRPC thread updates. Fall back to 0 if
|
||||
// not yet tracked (avoids accessing non-thread-safe History.Count).
|
||||
var count = _shardokResultCounts.GetValueOrDefault(sgm.Key, 0);
|
||||
return new IClientConnectionSubscriber.ShardokViewStatus {
|
||||
shardokGameId = sgm.Key,
|
||||
filteredResultCount = needsResync ? 0 : count,
|
||||
@@ -199,7 +198,8 @@ namespace eagle {
|
||||
return new List<AvailableCommand>();
|
||||
}
|
||||
|
||||
public Dictionary<String, ShardokGameModel> ShardokGameModels { get; set; }
|
||||
// Thread-safe: accessed from heartbeat timer thread via ShardokViewStatuses
|
||||
public ConcurrentDictionary<String, ShardokGameModel> ShardokGameModels { get; set; }
|
||||
|
||||
public FactionView MaybeDestroyedFaction(FactionId factionId) {
|
||||
if (ActiveFactions.TryGetValue(factionId, out var factionView)) {
|
||||
@@ -241,7 +241,8 @@ namespace eagle {
|
||||
new Dictionary<ProvinceId, OneProvinceAvailableCommands>();
|
||||
|
||||
_currentModel.GsView = new GameStateView();
|
||||
_currentModel.ShardokGameModels = new Dictionary<ShardokGameId, ShardokGameModel>();
|
||||
_currentModel.ShardokGameModels =
|
||||
new ConcurrentDictionary<ShardokGameId, ShardokGameModel>();
|
||||
|
||||
_currentModel.BattalionTypes = new Dictionary<BattalionTypeId, BattalionType>();
|
||||
|
||||
@@ -312,8 +313,10 @@ namespace eagle {
|
||||
ServerStatus = updateItem.ActionResultResponse.ServerGameStatus;
|
||||
}
|
||||
|
||||
_lastUnfilteredResultCount =
|
||||
updateItem.ActionResultResponse.UnfilteredResultCountAfter;
|
||||
// Note: _lastUnfilteredResultCount is updated on the gRPC thread in
|
||||
// UpdateResultCounts() before enqueueing. We don't update it here to avoid
|
||||
// race conditions where a backlogged MainQueue update overwrites a newer count.
|
||||
|
||||
if (updateItem.ActionResultResponse.ActionResultViews.Any() ||
|
||||
updateItem.ActionResultResponse.AvailableCommands == null ||
|
||||
updateItem.ActionResultResponse.AvailableCommands.Token !=
|
||||
@@ -339,7 +342,9 @@ namespace eagle {
|
||||
// Battle may have ended before we could create the model - remove
|
||||
// any stale reference and skip this update
|
||||
if (shardokGameModel == null) {
|
||||
_currentModel.ShardokGameModels.Remove(oneResponse.ShardokGameId);
|
||||
_currentModel.ShardokGameModels.TryRemove(
|
||||
oneResponse.ShardokGameId,
|
||||
out _);
|
||||
_shardokResultCounts.TryRemove(oneResponse.ShardokGameId, out _);
|
||||
continue;
|
||||
}
|
||||
@@ -368,7 +373,9 @@ namespace eagle {
|
||||
shardokGameModel;
|
||||
} else {
|
||||
// Game ended - remove from active models so UI knows battle is over
|
||||
_currentModel.ShardokGameModels.Remove(oneResponse.ShardokGameId);
|
||||
_currentModel.ShardokGameModels.TryRemove(
|
||||
oneResponse.ShardokGameId,
|
||||
out _);
|
||||
_shardokResultCounts.TryRemove(oneResponse.ShardokGameId, out _);
|
||||
}
|
||||
}
|
||||
@@ -478,6 +485,31 @@ namespace eagle {
|
||||
_currentModel.BattalionTypes =
|
||||
startingState.BattalionTypes.ToDictionary(bt => bt.TypeId, bt => bt);
|
||||
_currentModel.ChronicleEntries = startingState.ChronicleEntries.ToList();
|
||||
|
||||
// For any outstanding battles not in ShardokGameModels, create models and mark for
|
||||
// resync. This ensures fresh clients get Shardok state for ongoing battles.
|
||||
bool needsResubscribe = false;
|
||||
foreach (var battle in _currentModel.ShardokBattles) {
|
||||
if (!_currentModel.ShardokGameModels.ContainsKey(battle.ShardokGameId)) {
|
||||
var model = MakeGameModel(battle.ShardokGameId);
|
||||
if (model != null) {
|
||||
_currentModel.ShardokGameModels[battle.ShardokGameId] = model;
|
||||
MarkShardokForResync(battle.ShardokGameId);
|
||||
_connectionLogger.LogLine(
|
||||
$"[STATE_RESYNC] Created ShardokGameModel for outstanding battle {battle.ShardokGameId}");
|
||||
needsResubscribe = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we created new ShardokGameModels, re-subscribe to request their full state.
|
||||
// The original subscribe didn't include these battles since we didn't know about them
|
||||
// yet.
|
||||
if (needsResubscribe) {
|
||||
_connectionLogger.LogLine(
|
||||
"[STATE_RESYNC] Re-subscribing to request full Shardok state for new battles");
|
||||
_ = StartListeningForUpdates();
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleUpdates(List<ActionResultView> results) {
|
||||
@@ -780,7 +812,7 @@ namespace eagle {
|
||||
// the Eagle update removing the battle arrives before the Shardok Victory update.
|
||||
if (_currentModel.ShardokGameModels.TryGetValue(rb, out var sgm)) {
|
||||
sgm.MarkBattleEnded("Battle has ended.");
|
||||
_currentModel.ShardokGameModels.Remove(rb);
|
||||
_currentModel.ShardokGameModels.TryRemove(rb, out _);
|
||||
}
|
||||
_shardokResultCounts.TryRemove(rb, out _);
|
||||
|
||||
|
||||
+3
@@ -132,7 +132,10 @@ namespace eagle {
|
||||
|
||||
public void ProvinceHovered(ProvinceId? pid) {
|
||||
// Highlight moving armies table
|
||||
// Check row count to avoid index out of range if data changed after table was built
|
||||
var rowCount = movingArmiesTable.RowCount;
|
||||
MovingArmies.Each((army, i) => {
|
||||
if (i >= rowCount) return;
|
||||
var row = movingArmiesTable.ComponentAt<MovingArmyTableRow>(i);
|
||||
if (army.DestinationProvinceId == pid || army.OriginProvinceId == pid) {
|
||||
row.ShadeOn();
|
||||
|
||||
+35
-4
@@ -1,8 +1,12 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Net.Eagle0.Eagle.Common;
|
||||
using Net.Eagle0.Eagle.Views;
|
||||
|
||||
namespace eagle.Notifications.ARNNotifications {
|
||||
using ProvinceId = System.Int32;
|
||||
using HeroId = System.Int32;
|
||||
|
||||
public static class ProfessionGainedDetailsNotificationGenerator {
|
||||
public static readonly ARNNotificationGenerator Generator = GenerateNotifications;
|
||||
|
||||
@@ -44,19 +48,46 @@ namespace eagle.Notifications.ARNNotifications {
|
||||
: "Profession Gained";
|
||||
}
|
||||
|
||||
private static ProvinceId? FindProvinceForHero(HeroId heroId, IGameModel model) {
|
||||
foreach (var province in model.Provinces.Values) {
|
||||
if (province.FullInfo?.RulingFactionHeroIds.Contains(heroId) == true) {
|
||||
return province.Id;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static IEnumerable<Notification> GenerateNotifications(
|
||||
Net.Eagle0.Eagle.Common.Notification notification,
|
||||
IGameModel currentModel) {
|
||||
var details = notification.Details.ProfessionGainedDetails;
|
||||
var hero = currentModel.Heroes[details.HeroId];
|
||||
var factionName = currentModel.FactionName(details.FactionId);
|
||||
var professionName = GetProfessionName(details.NewProfession);
|
||||
var article = GetArticle(professionName);
|
||||
|
||||
var affectedProvinces = currentModel.ProvincesForFaction(details.FactionId);
|
||||
string textTemplate;
|
||||
List<ProvinceId> affectedProvinces;
|
||||
|
||||
string textTemplate =
|
||||
$"{{Hero}} of {factionName} became {article} {professionName}.\n\n";
|
||||
if (details.FactionId == currentModel.PlayerId) {
|
||||
// Player's own hero - vary text based on faction leader status
|
||||
string heroDescription;
|
||||
if (hero.IsFactionLeader) {
|
||||
heroDescription =
|
||||
$"Your sworn {DisplayNames.SiblingDescription(hero.PronounGender)}";
|
||||
} else {
|
||||
heroDescription = "Your vassal";
|
||||
}
|
||||
textTemplate = $"{heroDescription} {{Hero}} became {article} {professionName}.\n\n";
|
||||
var heroProvince = FindProvinceForHero(details.HeroId, currentModel);
|
||||
affectedProvinces = heroProvince.HasValue
|
||||
? new List<ProvinceId> { heroProvince.Value }
|
||||
: currentModel.ProvincesForFaction(details.FactionId);
|
||||
} else {
|
||||
// Another faction's hero
|
||||
var factionName = currentModel.FactionName(details.FactionId);
|
||||
textTemplate = $"{{Hero}} of {factionName} became {article} {professionName}.\n\n";
|
||||
affectedProvinces = currentModel.ProvincesForFaction(details.FactionId);
|
||||
}
|
||||
|
||||
var heroPlaceholders = new Dictionary<string, (string nameTextId, string fallback)> {
|
||||
{ "Hero", (hero.NameTextId, "A hero") }
|
||||
|
||||
+7
-1
@@ -31,7 +31,13 @@ namespace eagle.Notifications.ARNNotifications {
|
||||
};
|
||||
|
||||
if (playerId.HasValue && playerId.Value == paidToFactionId) {
|
||||
// no notification
|
||||
yield return DynamicTextNotification.StreamingDynamicNotification(
|
||||
title: "Ransom Accepted",
|
||||
textTemplate: $"We have accepted the ransom from {currentModel.FactionName(paidByFactionId)} 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 == paidByFactionId) {
|
||||
yield return DynamicTextNotification.StreamingDynamicNotification(
|
||||
title: "Ransom Accepted",
|
||||
|
||||
+13
@@ -9,6 +9,7 @@ namespace eagle.Notifications {
|
||||
private List<GeneratedTextListener> textListeners = new();
|
||||
private string textTemplate;
|
||||
private Dictionary<string, string> placeholderValues = new();
|
||||
private Dictionary<string, string> fallbackValues = new();
|
||||
|
||||
public DynamicTextNotification(
|
||||
string title,
|
||||
@@ -72,6 +73,9 @@ namespace eagle.Notifications {
|
||||
string nameTextId = kvp.Value.nameTextId;
|
||||
string fallback = kvp.Value.fallback;
|
||||
|
||||
// Store fallback so UpdateText can use it for missing placeholders
|
||||
fallbackValues[placeholder] = fallback;
|
||||
|
||||
if (!string.IsNullOrEmpty(nameTextId)) {
|
||||
var listener = new GeneratedTextListener(
|
||||
nameTextId,
|
||||
@@ -95,6 +99,15 @@ namespace eagle.Notifications {
|
||||
|
||||
private void UpdateText() {
|
||||
string result = textTemplate;
|
||||
|
||||
// First apply fallbacks for any placeholder without a loaded value
|
||||
foreach (var kvp in fallbackValues) {
|
||||
if (!placeholderValues.ContainsKey(kvp.Key)) {
|
||||
result = result.Replace($"{{{kvp.Key}}}", kvp.Value);
|
||||
}
|
||||
}
|
||||
|
||||
// Then apply actual loaded values
|
||||
foreach (var kvp in placeholderValues) {
|
||||
result = result.Replace($"{{{kvp.Key}}}", kvp.Value);
|
||||
}
|
||||
|
||||
+64
-35
@@ -69,6 +69,17 @@ namespace eagle {
|
||||
private ConnectionCircuitBreaker _circuitBreaker = new ConnectionCircuitBreaker();
|
||||
public ConnectionCircuitBreaker CircuitBreaker => _circuitBreaker;
|
||||
|
||||
/// <summary>
|
||||
/// Force an immediate reconnection attempt, bypassing the circuit breaker timeout.
|
||||
/// </summary>
|
||||
public void ForceReconnect() {
|
||||
_circuitBreaker.ForceReconnect();
|
||||
_retryTimer?.Dispose();
|
||||
NextReconnectAttempt = null;
|
||||
LogConnectionEvent("force_reconnect", "User requested immediate reconnect");
|
||||
Task.Run(() => Connect());
|
||||
}
|
||||
|
||||
private DateTime? GetDeadlineFromNow() {
|
||||
return DateTime.UtcNow.AddSeconds(TimeoutSeconds);
|
||||
}
|
||||
@@ -1060,51 +1071,69 @@ namespace eagle {
|
||||
}
|
||||
}
|
||||
|
||||
// Grace period after connect before sync mismatch triggers reconnect.
|
||||
// Allows time to receive and process historical results after fresh subscribe.
|
||||
private const double SyncMismatchGracePeriodSeconds = 60.0;
|
||||
|
||||
private void HandleHeartbeatResponse(HeartbeatResponse response) {
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"[HEARTBEAT] Got response, server_timestamp={response.ServerTimestamp}");
|
||||
|
||||
// Check for sync mismatches reported by server
|
||||
if (response.GameSyncResults.Count > 0) {
|
||||
foreach (var syncResult in response.GameSyncResults) {
|
||||
if (!syncResult.EagleInSync) {
|
||||
bool hasMismatch = false;
|
||||
foreach (var syncResult in response.GameSyncResults) {
|
||||
if (!syncResult.EagleInSync) {
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"[SYNC_MISMATCH] Game {syncResult.GameId}: Eagle out of sync, " +
|
||||
$"server has {syncResult.ServerUnfilteredResultCount} results");
|
||||
LogConnectionEvent(
|
||||
"sync_mismatch_eagle",
|
||||
$"game={syncResult.GameId}, server_count={syncResult.ServerUnfilteredResultCount}");
|
||||
hasMismatch = true;
|
||||
}
|
||||
|
||||
foreach (var shardokResult in syncResult.ShardokSyncResults) {
|
||||
if (!shardokResult.InSync) {
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"[SYNC_MISMATCH] Game {syncResult.GameId}: Eagle out of sync, " +
|
||||
$"server has {syncResult.ServerUnfilteredResultCount} results");
|
||||
$"[SYNC_MISMATCH] Game {syncResult.GameId}, Shardok {shardokResult.ShardokGameId}: " +
|
||||
$"out of sync, server has {shardokResult.ServerFilteredResultCount} results");
|
||||
LogConnectionEvent(
|
||||
"sync_mismatch_eagle",
|
||||
$"game={syncResult.GameId}, server_count={syncResult.ServerUnfilteredResultCount}");
|
||||
}
|
||||
|
||||
foreach (var shardokResult in syncResult.ShardokSyncResults) {
|
||||
if (!shardokResult.InSync) {
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"[SYNC_MISMATCH] Game {syncResult.GameId}, Shardok {shardokResult.ShardokGameId}: " +
|
||||
$"out of sync, server has {shardokResult.ServerFilteredResultCount} results");
|
||||
LogConnectionEvent(
|
||||
"sync_mismatch_shardok",
|
||||
$"game={syncResult.GameId}, shardok={shardokResult.ShardokGameId}, " +
|
||||
$"server_count={shardokResult.ServerFilteredResultCount}");
|
||||
}
|
||||
"sync_mismatch_shardok",
|
||||
$"game={syncResult.GameId}, shardok={shardokResult.ShardokGameId}, " +
|
||||
$"server_count={shardokResult.ServerFilteredResultCount}");
|
||||
hasMismatch = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger resync by reconnecting - this will re-subscribe with current counts
|
||||
// and the server will send missing updates
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
"[SYNC_MISMATCH] Detected sync mismatch, triggering reconnect to resync");
|
||||
LogConnectionEvent("sync_mismatch_reconnect", "Triggering reconnect to resync");
|
||||
|
||||
// Schedule reconnect to resync
|
||||
lock (this) {
|
||||
_streamingCall?.Dispose();
|
||||
_streamingCall = null;
|
||||
_threadCancellationTokenSource?.Cancel();
|
||||
}
|
||||
MarkAllShardokGamesForResync();
|
||||
CancelAllPendingSubscriptionAcks();
|
||||
ScheduleReconnect("SyncMismatch");
|
||||
}
|
||||
|
||||
if (!hasMismatch) return;
|
||||
|
||||
// Grace period after connect - allow time to receive historical results
|
||||
if (_lastSuccessfulConnect.HasValue) {
|
||||
var timeSinceConnect =
|
||||
(DateTime.UtcNow - _lastSuccessfulConnect.Value).TotalSeconds;
|
||||
if (timeSinceConnect < SyncMismatchGracePeriodSeconds) {
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"[SYNC_MISMATCH] Ignoring mismatch during grace period ({timeSinceConnect:F1}s < {SyncMismatchGracePeriodSeconds}s)");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger resync by reconnecting - this will re-subscribe with current counts
|
||||
// and the server will send missing updates
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
"[SYNC_MISMATCH] Detected sync mismatch, triggering reconnect to resync");
|
||||
LogConnectionEvent("sync_mismatch_reconnect", "Triggering reconnect to resync");
|
||||
|
||||
// Schedule reconnect to resync
|
||||
lock (this) {
|
||||
_streamingCall?.Dispose();
|
||||
_streamingCall = null;
|
||||
_threadCancellationTokenSource?.Cancel();
|
||||
}
|
||||
MarkAllShardokGamesForResync();
|
||||
CancelAllPendingSubscriptionAcks();
|
||||
ScheduleReconnect("SyncMismatch");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32467,7 +32467,7 @@ GameObject:
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 2147483647
|
||||
m_IsActive: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &300036923
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -33904,6 +33904,7 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: 727a520b2deac4a28ae9c6a00e54c2e7, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::eagle.ConnectionStatusUI
|
||||
retryButton: {fileID: 0}
|
||||
--- !u!114 &320759869
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -50851,6 +50852,148 @@ CanvasRenderer:
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 464655241}
|
||||
m_CullTransparentMesh: 0
|
||||
--- !u!1 &465641331
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 465641332}
|
||||
- component: {fileID: 465641336}
|
||||
- component: {fileID: 465641335}
|
||||
- component: {fileID: 465641334}
|
||||
- component: {fileID: 465641333}
|
||||
m_Layer: 0
|
||||
m_Name: Button
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 0
|
||||
--- !u!224 &465641332
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 465641331}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 1405807865}
|
||||
m_Father: {fileID: 1012849519}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 210.00002, y: -27.5}
|
||||
m_SizeDelta: {x: 80, y: 25}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &465641333
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 465641331}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
|
||||
m_IgnoreLayout: 0
|
||||
m_MinWidth: 80
|
||||
m_MinHeight: 25
|
||||
m_PreferredWidth: 80
|
||||
m_PreferredHeight: -1
|
||||
m_FlexibleWidth: -1
|
||||
m_FlexibleHeight: -1
|
||||
m_LayoutPriority: 1
|
||||
--- !u!114 &465641334
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 465641331}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Button
|
||||
m_Navigation:
|
||||
m_Mode: 3
|
||||
m_WrapAround: 0
|
||||
m_SelectOnUp: {fileID: 0}
|
||||
m_SelectOnDown: {fileID: 0}
|
||||
m_SelectOnLeft: {fileID: 0}
|
||||
m_SelectOnRight: {fileID: 0}
|
||||
m_Transition: 1
|
||||
m_Colors:
|
||||
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
|
||||
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
|
||||
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
|
||||
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
|
||||
m_ColorMultiplier: 1
|
||||
m_FadeDuration: 0.1
|
||||
m_SpriteState:
|
||||
m_HighlightedSprite: {fileID: 0}
|
||||
m_PressedSprite: {fileID: 0}
|
||||
m_SelectedSprite: {fileID: 0}
|
||||
m_DisabledSprite: {fileID: 0}
|
||||
m_AnimationTriggers:
|
||||
m_NormalTrigger: Normal
|
||||
m_HighlightedTrigger: Highlighted
|
||||
m_PressedTrigger: Pressed
|
||||
m_SelectedTrigger: Selected
|
||||
m_DisabledTrigger: Disabled
|
||||
m_Interactable: 1
|
||||
m_TargetGraphic: {fileID: 465641335}
|
||||
m_OnClick:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
--- !u!114 &465641335
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 465641331}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0}
|
||||
m_Type: 1
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
m_FillMethod: 4
|
||||
m_FillAmount: 1
|
||||
m_FillClockwise: 1
|
||||
m_FillOrigin: 0
|
||||
m_UseSpriteMesh: 0
|
||||
m_PixelsPerUnitMultiplier: 1
|
||||
--- !u!222 &465641336
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 465641331}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!1 &466028234
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -56271,9 +56414,9 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_IgnoreLayout: 0
|
||||
m_MinWidth: 176
|
||||
m_MinWidth: 250
|
||||
m_MinHeight: -1
|
||||
m_PreferredWidth: 176
|
||||
m_PreferredWidth: 250
|
||||
m_PreferredHeight: -1
|
||||
m_FlexibleWidth: -1
|
||||
m_FlexibleHeight: 1
|
||||
@@ -121559,6 +121702,7 @@ RectTransform:
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 1453223014}
|
||||
- {fileID: 465641332}
|
||||
m_Father: {fileID: 512260365}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
@@ -170015,6 +170159,144 @@ CanvasRenderer:
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1405279533}
|
||||
m_CullTransparentMesh: 0
|
||||
--- !u!1 &1405807864
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 1405807865}
|
||||
- component: {fileID: 1405807867}
|
||||
- component: {fileID: 1405807866}
|
||||
m_Layer: 0
|
||||
m_Name: Text (TMP)
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &1405807865
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1405807864}
|
||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 465641332}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1405807866
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1405807864}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.TextMeshPro::TMPro.TextMeshProUGUI
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_text: Reconnect
|
||||
m_isRightToLeft: 0
|
||||
m_fontAsset: {fileID: 11400000, guid: ec2736df0379a45bfa7349b652fd07d6, type: 2}
|
||||
m_sharedMaterial: {fileID: 1026247389042360723, guid: ec2736df0379a45bfa7349b652fd07d6,
|
||||
type: 2}
|
||||
m_fontSharedMaterials: []
|
||||
m_fontMaterial: {fileID: 0}
|
||||
m_fontMaterials: []
|
||||
m_fontColor32:
|
||||
serializedVersion: 2
|
||||
rgba: 4281479730
|
||||
m_fontColor: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
|
||||
m_enableVertexGradient: 0
|
||||
m_colorMode: 3
|
||||
m_fontColorGradient:
|
||||
topLeft: {r: 1, g: 1, b: 1, a: 1}
|
||||
topRight: {r: 1, g: 1, b: 1, a: 1}
|
||||
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
|
||||
bottomRight: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_fontColorGradientPreset: {fileID: 0}
|
||||
m_spriteAsset: {fileID: 0}
|
||||
m_tintAllSprites: 0
|
||||
m_StyleSheet: {fileID: 0}
|
||||
m_TextStyleHashCode: -1183493901
|
||||
m_overrideHtmlColors: 0
|
||||
m_faceColor:
|
||||
serializedVersion: 2
|
||||
rgba: 4294967295
|
||||
m_fontSize: 12
|
||||
m_fontSizeBase: 12
|
||||
m_fontWeight: 400
|
||||
m_enableAutoSizing: 0
|
||||
m_fontSizeMin: 18
|
||||
m_fontSizeMax: 72
|
||||
m_fontStyle: 0
|
||||
m_HorizontalAlignment: 2
|
||||
m_VerticalAlignment: 512
|
||||
m_textAlignment: 65535
|
||||
m_characterSpacing: 0
|
||||
m_characterHorizontalScale: 1
|
||||
m_wordSpacing: 0
|
||||
m_lineSpacing: 0
|
||||
m_lineSpacingMax: 0
|
||||
m_paragraphSpacing: 0
|
||||
m_charWidthMaxAdj: 0
|
||||
m_TextWrappingMode: 1
|
||||
m_wordWrappingRatios: 0.4
|
||||
m_overflowMode: 0
|
||||
m_linkedTextComponent: {fileID: 0}
|
||||
parentLinkedComponent: {fileID: 0}
|
||||
m_enableKerning: 0
|
||||
m_ActiveFontFeatures: 6e72656b
|
||||
m_enableExtraPadding: 0
|
||||
checkPaddingRequired: 0
|
||||
m_isRichText: 1
|
||||
m_EmojiFallbackSupport: 1
|
||||
m_parseCtrlCharacters: 1
|
||||
m_isOrthographic: 1
|
||||
m_isCullingEnabled: 0
|
||||
m_horizontalMapping: 0
|
||||
m_verticalMapping: 0
|
||||
m_uvLineOffset: 0
|
||||
m_geometrySortingOrder: 0
|
||||
m_IsTextObjectScaleStatic: 0
|
||||
m_VertexBufferAutoSizeReduction: 0
|
||||
m_useMaxVisibleDescender: 1
|
||||
m_pageToDisplay: 1
|
||||
m_margin: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_isUsingLegacyAnimationComponent: 0
|
||||
m_isVolumetricText: 0
|
||||
m_hasFontAssetChanged: 0
|
||||
m_baseMaterial: {fileID: 0}
|
||||
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
|
||||
--- !u!222 &1405807867
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1405807864}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!1 &1406178529
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -174532,7 +174814,7 @@ MonoBehaviour:
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_text: 3440
|
||||
m_text: Server down. Reconnecting in 10s
|
||||
m_isRightToLeft: 0
|
||||
m_fontAsset: {fileID: 11400000, guid: ec2736df0379a45bfa7349b652fd07d6, type: 2}
|
||||
m_sharedMaterial: {fileID: 1026247389042360723, guid: ec2736df0379a45bfa7349b652fd07d6,
|
||||
@@ -174560,8 +174842,8 @@ MonoBehaviour:
|
||||
m_faceColor:
|
||||
serializedVersion: 2
|
||||
rgba: 4294967295
|
||||
m_fontSize: 18
|
||||
m_fontSizeBase: 18
|
||||
m_fontSize: 12
|
||||
m_fontSizeBase: 12
|
||||
m_fontWeight: 400
|
||||
m_enableAutoSizing: 0
|
||||
m_fontSizeMin: 18
|
||||
@@ -174645,6 +174927,7 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: 727a520b2deac4a28ae9c6a00e54c2e7, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::eagle.ConnectionStatusUI
|
||||
retryButton: {fileID: 465641334}
|
||||
--- !u!1 &1453601446
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -190489,7 +190772,7 @@ GameObject:
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 2147483647
|
||||
m_IsActive: 1
|
||||
m_IsActive: 0
|
||||
--- !u!224 &1566150938
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
|
||||
+94
-94
@@ -385,98 +385,99 @@ namespace Shardok {
|
||||
Model.MyUncommittedUnits.Where(uv => uv.Location.Row == -1).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the ShardokGameModel is updated. This is invoked from UpdateAction,
|
||||
/// which is called from ShardokGameModel.HandleUpdates, which runs on MainQueue.
|
||||
/// No need to re-enqueue - we're already on the main thread.
|
||||
/// </summary>
|
||||
void ModelUpdated() {
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
if (Model == null) { return; }
|
||||
SetHeroLabels();
|
||||
SetModifiers();
|
||||
UpdateReserves();
|
||||
if (Model == null) { return; }
|
||||
SetHeroLabels();
|
||||
SetModifiers();
|
||||
UpdateReserves();
|
||||
|
||||
HandleEnemyStartingPositionOverlays();
|
||||
HandleEnemyStartingPositionOverlays();
|
||||
|
||||
endTurnButton.interactable = false;
|
||||
endTurnButton.interactable = false;
|
||||
|
||||
if (Model.GameStatus != null &&
|
||||
(Model.GameStatus.State == GameStatus.Types.State.Victory)) {
|
||||
turnStatusLabel.text = "Game Over!";
|
||||
if (Model.GameStatus != null &&
|
||||
(Model.GameStatus.State == GameStatus.Types.State.Victory)) {
|
||||
turnStatusLabel.text = "Game Over!";
|
||||
|
||||
gameOverText.text = Model.GameStatus.Description;
|
||||
gameOverCanvas.gameObject.SetActive(true);
|
||||
gameOverText.text = Model.GameStatus.Description;
|
||||
gameOverCanvas.gameObject.SetActive(true);
|
||||
|
||||
endTurnButton.GetComponentInChildren<TMP_Text>().text = "Back to Eagle";
|
||||
endTurnButton.interactable = true;
|
||||
} else if (Model.GameStatus != null && Model.MyTurn) {
|
||||
gameOverCanvas.gameObject.SetActive(false);
|
||||
turnStatusLabel.text = "Your Turn";
|
||||
endTurnButton.GetComponentInChildren<TMP_Text>().text = "Back to Eagle";
|
||||
endTurnButton.interactable = true;
|
||||
} else if (Model.GameStatus != null && Model.MyTurn) {
|
||||
gameOverCanvas.gameObject.SetActive(false);
|
||||
turnStatusLabel.text = "Your Turn";
|
||||
|
||||
if (Model.InSetUp) {
|
||||
endTurnButton.GetComponentInChildren<TMP_Text>().text = "Commit";
|
||||
if (Model.InSetUp) {
|
||||
endTurnButton.GetComponentInChildren<TMP_Text>().text = "Commit";
|
||||
|
||||
var unplacedUnitsWithLocations =
|
||||
Model.MyUncommittedUnits
|
||||
.Where(u => u.Location.Row >= 0 && u.Location.Column >= 0)
|
||||
.ToList();
|
||||
var unplacedUnitsWithLocations =
|
||||
Model.MyUncommittedUnits
|
||||
.Where(u => u.Location.Row >= 0 && u.Location.Column >= 0)
|
||||
.ToList();
|
||||
|
||||
if (!Model.MyTurn) {
|
||||
endTurnButton.interactable = false;
|
||||
} else if (unplacedUnitsWithLocations.Count() < 1) {
|
||||
endTurnButton.GetComponentInChildren<TMP_Text>().text =
|
||||
"No units placed";
|
||||
endTurnButton.interactable = false;
|
||||
} else if (unplacedUnitsWithLocations.Count() > 10) {
|
||||
endTurnButton.GetComponentInChildren<TMP_Text>().text =
|
||||
"Too many units";
|
||||
endTurnButton.interactable = false;
|
||||
} else {
|
||||
endTurnButton.interactable = true;
|
||||
}
|
||||
|
||||
SetDisplayedCommandGroup(0);
|
||||
if (!Model.MyTurn) {
|
||||
endTurnButton.interactable = false;
|
||||
} else if (unplacedUnitsWithLocations.Count() < 1) {
|
||||
endTurnButton.GetComponentInChildren<TMP_Text>().text = "No units placed";
|
||||
endTurnButton.interactable = false;
|
||||
} else if (unplacedUnitsWithLocations.Count() > 10) {
|
||||
endTurnButton.GetComponentInChildren<TMP_Text>().text = "Too many units";
|
||||
endTurnButton.interactable = false;
|
||||
} else {
|
||||
endTurnButton.GetComponentInChildren<TMP_Text>().text = "End Turn";
|
||||
}
|
||||
|
||||
if (Model.HasAvailableCommandWhere(
|
||||
command => commandTypeUIManager.CommandGroupForType(command.Type) ==
|
||||
CommandTypeUIManager.EndTurnCommandGroup)) {
|
||||
endTurnButton.interactable = true;
|
||||
}
|
||||
|
||||
SelectAppropriateDefaultCommand();
|
||||
SetDisplayedCommandGroup(0);
|
||||
} else {
|
||||
turnStatusLabel.text = $"{Model.CurrentPlayerName}'s Turn";
|
||||
endTurnButton.GetComponentInChildren<TMP_Text>().text = "End Turn";
|
||||
}
|
||||
|
||||
locationNameText.text = Model.LocationName;
|
||||
|
||||
if (Model.History.Count > 0) {
|
||||
string monthString = new DateTime(777, Model.Month, 1)
|
||||
.ToString("MMMM", CultureInfo.InvariantCulture);
|
||||
roundInfoText.text = $"{monthString} {Model.CurrentRound}";
|
||||
Weather weather = Model.Weather;
|
||||
if (weather != null) {
|
||||
roundInfoText.text += ", " + ProtoExtensions.WeatherToString(weather);
|
||||
}
|
||||
if (Model.HasAvailableCommandWhere(
|
||||
command => commandTypeUIManager.CommandGroupForType(command.Type) ==
|
||||
CommandTypeUIManager.EndTurnCommandGroup)) {
|
||||
endTurnButton.interactable = true;
|
||||
}
|
||||
|
||||
if (Model.History.Count == 0) {
|
||||
turnHistoryButtonText.text = NoHistoryText;
|
||||
} else if (Model.History.Count > _lastRetrievedHistoryCount) {
|
||||
for (int i = _lastRetrievedHistoryCount; i < Model.History.Count; i++) {
|
||||
var historyEntry = Model.History[i];
|
||||
turnHistoryPanel.AddLine(GetActionResultDescription(historyEntry));
|
||||
SelectAppropriateDefaultCommand();
|
||||
} else {
|
||||
turnStatusLabel.text = $"{Model.CurrentPlayerName}'s Turn";
|
||||
}
|
||||
|
||||
ActionType type = historyEntry.Type;
|
||||
var thisSound = soundManager.SoundForType(type);
|
||||
if (thisSound != null) { audioClipSource.PlayOneShot(thisSound, 1.0f); }
|
||||
}
|
||||
_lastRetrievedHistoryCount = Model.History.Count;
|
||||
turnHistoryButtonText.text = GetActionResultDescription(Model.History.Last());
|
||||
SetModifiers();
|
||||
locationNameText.text = Model.LocationName;
|
||||
|
||||
if (Model.History.Count > 0) {
|
||||
string monthString = new DateTime(777, Model.Month, 1)
|
||||
.ToString("MMMM", CultureInfo.InvariantCulture);
|
||||
roundInfoText.text = $"{monthString} {Model.CurrentRound}";
|
||||
Weather weather = Model.Weather;
|
||||
if (weather != null) {
|
||||
roundInfoText.text += ", " + ProtoExtensions.WeatherToString(weather);
|
||||
}
|
||||
}
|
||||
|
||||
SetupArmiesTable();
|
||||
});
|
||||
if (Model.History.Count == 0) {
|
||||
turnHistoryButtonText.text = NoHistoryText;
|
||||
} else if (Model.History.Count > _lastRetrievedHistoryCount) {
|
||||
for (int i = _lastRetrievedHistoryCount; i < Model.History.Count; i++) {
|
||||
var historyEntry = Model.History[i];
|
||||
turnHistoryPanel.AddLine(GetActionResultDescription(historyEntry));
|
||||
|
||||
ActionType type = historyEntry.Type;
|
||||
var thisSound = soundManager.SoundForType(type);
|
||||
if (thisSound != null) { audioClipSource.PlayOneShot(thisSound, 1.0f); }
|
||||
}
|
||||
_lastRetrievedHistoryCount = Model.History.Count;
|
||||
turnHistoryButtonText.text = GetActionResultDescription(Model.History.Last());
|
||||
SetModifiers();
|
||||
}
|
||||
|
||||
SetupArmiesTable();
|
||||
}
|
||||
|
||||
private void SetupArmiesTable() {
|
||||
@@ -707,34 +708,33 @@ namespace Shardok {
|
||||
}
|
||||
|
||||
void SetModifiers() {
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
hexGrid.ClearCellModifierImages();
|
||||
// Called from ModelUpdated which runs on MainQueue - no need to re-enqueue
|
||||
hexGrid.ClearCellModifierImages();
|
||||
|
||||
for (byte row = 0; row < Model.Map.RowCount; row++) {
|
||||
for (byte column = 0; column < Model.Map.ColumnCount; column++) {
|
||||
Coords coords = new Coords();
|
||||
coords.Row = row;
|
||||
coords.Column = column;
|
||||
for (byte row = 0; row < Model.Map.RowCount; row++) {
|
||||
for (byte column = 0; column < Model.Map.ColumnCount; column++) {
|
||||
Coords coords = new Coords();
|
||||
coords.Row = row;
|
||||
coords.Column = column;
|
||||
|
||||
var terrain = Model.Map.TerrainAt(coords);
|
||||
int cellIndex = MapCoordsToGridIndex(coords);
|
||||
int numberForCell = _randomNumberForCellIndex[cellIndex];
|
||||
hexGrid.SetCellTerrainImage(
|
||||
cellIndex,
|
||||
_imageForTerrainTracker
|
||||
.GetImageForTerrain(terrain, numberForCell, Model.Month));
|
||||
if (terrain.Modifier?.Fire != null) {
|
||||
hexGrid.SetCellModifierEffect(cellIndex, fireEffectPrefab);
|
||||
} else {
|
||||
hexGrid.SetCellModifierEffect(cellIndex, null);
|
||||
}
|
||||
var terrain = Model.Map.TerrainAt(coords);
|
||||
int cellIndex = MapCoordsToGridIndex(coords);
|
||||
int numberForCell = _randomNumberForCellIndex[cellIndex];
|
||||
hexGrid.SetCellTerrainImage(
|
||||
cellIndex,
|
||||
_imageForTerrainTracker
|
||||
.GetImageForTerrain(terrain, numberForCell, Model.Month));
|
||||
if (terrain.Modifier?.Fire != null) {
|
||||
hexGrid.SetCellModifierEffect(cellIndex, fireEffectPrefab);
|
||||
} else {
|
||||
hexGrid.SetCellModifierEffect(cellIndex, null);
|
||||
}
|
||||
|
||||
if (terrain.Modifier?.Bridge != null) {
|
||||
hexGrid.SetCellModifierImage(cellIndex, bridgeImage);
|
||||
}
|
||||
if (terrain.Modifier?.Bridge != null) {
|
||||
hexGrid.SetCellModifierImage(cellIndex, bridgeImage);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void HandleButton() {
|
||||
|
||||
@@ -82,7 +82,7 @@ public class ShardokGameModel {
|
||||
public int Month { get; private set; }
|
||||
private List<PlayerTotals> PlayerTotals { get; set; }
|
||||
|
||||
public List<CommandDescriptor> AvailableCommands { get; private set; }
|
||||
public List<CommandDescriptor> AvailableCommands { get; private set; } = new();
|
||||
public Action UpdateAction { get; set; }
|
||||
public List<PlayerWithHostility> players;
|
||||
public String GetPlayerName(PlayerId playerId) {
|
||||
@@ -217,6 +217,8 @@ public class ShardokGameModel {
|
||||
? newCommands.CurrentCommand.ToList()
|
||||
: newCommands.PreviewCommand.ToList();
|
||||
}
|
||||
// Trigger UI refresh after commands are updated so unit action indicators reflect new state
|
||||
UpdateAction?.Invoke();
|
||||
}
|
||||
|
||||
public bool HasTargetedCommand(Coords start, Coords target, List<CommandType> possibleTypes) {
|
||||
|
||||
+8
-3
@@ -138,9 +138,9 @@ namespace common {
|
||||
// Use TMP's calculated preferred height - this is the actual text size
|
||||
contentHeight = tmpText.preferredHeight;
|
||||
|
||||
// Ensure Content RectTransform is tall enough to contain the text
|
||||
// This is necessary for ScrollRect to scroll properly
|
||||
if (scrollRect.content.rect.height < contentHeight) {
|
||||
// Sync Content RectTransform height with actual text size.
|
||||
// Must both grow AND shrink to prevent blank space when scrolling to bottom.
|
||||
if (Mathf.Abs(scrollRect.content.rect.height - contentHeight) > 1f) {
|
||||
scrollRect.content.SetSizeWithCurrentAnchors(
|
||||
RectTransform.Axis.Vertical,
|
||||
contentHeight);
|
||||
@@ -203,6 +203,11 @@ namespace common {
|
||||
return;
|
||||
}
|
||||
|
||||
// Hold Shift to pause scrolling (lets user read at their own pace)
|
||||
bool isPausedByUser =
|
||||
Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift);
|
||||
if (isPausedByUser) return;
|
||||
|
||||
// Handle pause at bottom
|
||||
if (_isPausedAtBottom) {
|
||||
_pauseTimer += Time.deltaTime;
|
||||
|
||||
+33
-8
@@ -54,18 +54,43 @@ namespace common.GUIUtils {
|
||||
Type = type,
|
||||
Time = DateTime.UtcNow
|
||||
});
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
errorTextField.text = AllMessageText;
|
||||
panel.gameObject.SetActive(true);
|
||||
});
|
||||
|
||||
// Try to display immediately if possible, otherwise queue for later
|
||||
try {
|
||||
if (MainQueue.Q != null) {
|
||||
MainQueue.Q.Enqueue(ShowErrorPanel);
|
||||
} else {
|
||||
_pendingShow = true;
|
||||
}
|
||||
} catch {
|
||||
// MainQueue not ready yet - will show in Update
|
||||
_pendingShow = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool _pendingShow = false;
|
||||
|
||||
private void ShowErrorPanel() {
|
||||
if (errorTextField != null && panel != null) {
|
||||
errorTextField.text = AllMessageText;
|
||||
panel.gameObject.SetActive(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Register for log messages as early as possible
|
||||
void Awake() { Application.logMessageReceivedThreaded += HandleLog; }
|
||||
|
||||
void Update() {
|
||||
// Handle errors that occurred before MainQueue was ready
|
||||
if (_pendingShow) {
|
||||
_pendingShow = false;
|
||||
ShowErrorPanel();
|
||||
}
|
||||
}
|
||||
|
||||
// Use this for initialization
|
||||
void Start() {
|
||||
panel.gameObject.SetActive(false);
|
||||
Application.logMessageReceivedThreaded += HandleLog;
|
||||
}
|
||||
void Start() { panel.gameObject.SetActive(false); }
|
||||
|
||||
public void DismissClicked() {
|
||||
lock (_messages) {
|
||||
|
||||
@@ -21,14 +21,24 @@ option java_outer_classname = "EagleInterface";
|
||||
option objc_class_prefix = "E0G";
|
||||
|
||||
service ShardokInternalInterface {
|
||||
// Server-side streaming for game updates - replaces polling via GetGameStatus
|
||||
rpc SubscribeToGame(GameSubscriptionRequest) returns (stream GameStatusResponse) {}
|
||||
|
||||
rpc PostCommand(PostCommandRequest) returns (GameStatusResponse) {}
|
||||
rpc PostPlacementCommands(PlacementCommandsRequest) returns (GameStatusResponse) {}
|
||||
|
||||
// Deprecated: Use SubscribeToGame for streaming updates instead
|
||||
rpc GetGameStatus(GameStatusRequest) returns (GameStatusResponse) {}
|
||||
|
||||
rpc GetHexMap(HexMapRequest) returns (HexMapResponse) {}
|
||||
rpc GetHexMapNames(HexMapNamesRequest) returns (HexMapNamesResponse) {}
|
||||
}
|
||||
|
||||
message GameSubscriptionRequest {
|
||||
string game_id = 1;
|
||||
GameSetupInfo game_setup_info = 2;
|
||||
}
|
||||
|
||||
message PostCommandRequest {
|
||||
string game_id = 1;
|
||||
int32 player_id = 2;
|
||||
|
||||
@@ -114,6 +114,7 @@ class ActionResultApplierImpl(validator: Option[ScalaValidator]) extends ActionR
|
||||
// Apply all remaining entity changes using extension methods
|
||||
val finalState = stateAfterChangedBattalions
|
||||
.applyProvinceActed(result.provinceIdActed)
|
||||
.applyLastCommand(result.provinceId, result.lastCommandTypeForActingProvince)
|
||||
.applyNewBattalions(result.newBattalions, result.provinceId)
|
||||
.applyDestroyedBattalionIds(result.destroyedBattalionIds)
|
||||
.applyNewHeroes(result.newHeroes)
|
||||
|
||||
@@ -54,6 +54,7 @@ scala_library(
|
||||
deps = [
|
||||
":province_update_helpers",
|
||||
":province_update_helpers2",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library:eagle_internal_exception",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:eagle_require",
|
||||
@@ -154,6 +155,7 @@ scala_library(
|
||||
":game_state_hero_extensions",
|
||||
":game_state_misc_extensions",
|
||||
":game_state_province_extensions",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_faction_trait",
|
||||
@@ -169,6 +171,7 @@ scala_library(
|
||||
":game_state_hero_extensions",
|
||||
":game_state_misc_extensions",
|
||||
":game_state_province_extensions",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:extra_xp_for_stat_bump_over100",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:xp_for_stat_bump",
|
||||
|
||||
+18
@@ -1,6 +1,7 @@
|
||||
package net.eagle0.eagle.library.actions.applier
|
||||
|
||||
import net.eagle0.eagle.*
|
||||
import net.eagle0.eagle.api.selected_command.SelectedCommand
|
||||
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
|
||||
import net.eagle0.eagle.library.EagleInternalException
|
||||
import net.eagle0.eagle.model.action_result.changed_province.concrete.ChangedProvinceC
|
||||
@@ -75,5 +76,22 @@ object GameStateProvinceExtensions {
|
||||
}
|
||||
.getOrElse(gameState)
|
||||
}
|
||||
|
||||
def applyLastCommand(
|
||||
provinceId: Option[ProvinceId],
|
||||
lastCommand: Option[SelectedCommand]
|
||||
): GameState =
|
||||
provinceId
|
||||
.filter(_ => lastCommand.exists(!_.isEmpty))
|
||||
.map { pid =>
|
||||
val province = gameState.provinces(pid) match {
|
||||
case p: ProvinceC => p
|
||||
case p => throw new EagleInternalException(s"Unknown ProvinceT type: ${p.getClass}")
|
||||
}
|
||||
gameState.copy(
|
||||
provinces = gameState.provinces.updated(pid, province.copy(lastCommand = lastCommand))
|
||||
)
|
||||
}
|
||||
.getOrElse(gameState)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,8 +209,10 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types:end_aftermath_phase_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:battle_revelation_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:notification_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/date",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/llm_request",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/view/province:province_view",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:battle_revelation",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/faction:faction_relationship",
|
||||
@@ -1288,7 +1290,7 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types:end_recon_resolution_phase_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types:recon_succeeded_result_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/view/province:province_view",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province:incoming_end_turn_action",
|
||||
|
||||
+20
-17
@@ -22,7 +22,9 @@ import net.eagle0.eagle.model.action_result.{ActionResultT, NotificationDetails,
|
||||
import net.eagle0.eagle.model.action_result.changed_province.concrete.ChangedProvinceC
|
||||
import net.eagle0.eagle.model.action_result.concrete.{ActionResultC, ChangedFactionC, ChangedHeroC, NotificationC}
|
||||
import net.eagle0.eagle.model.action_result.types.{CapturedHeroResolvedResultType, EndAftermathPhaseResultType}
|
||||
import net.eagle0.eagle.model.proto_converters.date.DateConverter
|
||||
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
|
||||
import net.eagle0.eagle.model.proto_converters.view.province.ProvinceViewConverter
|
||||
import net.eagle0.eagle.model.state.{BattleRevelation, RoundPhase}
|
||||
import net.eagle0.eagle.model.state.faction.FactionRelationship
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
@@ -350,12 +352,12 @@ case class EndBattleAftermathPhaseAction(
|
||||
actionResultApplier: ActionResultApplier
|
||||
) extends ProtolessRandomSequentialResultsAction {
|
||||
|
||||
// Revelation changes still need proto for ProvinceViewFilter
|
||||
// Withdrew case still needs proto for withdrawnFromProvinceView (no Scala overload yet)
|
||||
private def revelationChange(
|
||||
battleRevelation: BattleRevelation,
|
||||
gs: GameState
|
||||
): RevelationChange = {
|
||||
// Lazy conversion to proto for ProvinceViewFilter calls
|
||||
// Lazy conversion to proto only for withdrawnFromProvinceView
|
||||
lazy val gsProto = GameStateConverter.toProto(gs)
|
||||
lazy val provinceProto = gsProto.provinces(battleRevelation.provinceId)
|
||||
|
||||
@@ -367,27 +369,28 @@ case class EndBattleAftermathPhaseAction(
|
||||
ChangedFactionC(
|
||||
factionId = battleRevelation.revealedToFactionId,
|
||||
updatedReconnedProvinces = Vector(
|
||||
ProvinceViewFilter
|
||||
.withdrawnFromProvinceView(
|
||||
province = provinceProto,
|
||||
gs = gsProto,
|
||||
factionId = battleRevelation.revealedToFactionId
|
||||
)
|
||||
.withAsOf(gsProto.currentDate.get)
|
||||
ProvinceViewConverter.toProto(
|
||||
ProvinceViewFilter
|
||||
.withdrawnFromProvinceView(
|
||||
province = provinceProto,
|
||||
gs = gsProto,
|
||||
factionId = battleRevelation.revealedToFactionId
|
||||
)
|
||||
.copy(asOf = gsProto.currentDate.map(d => DateConverter.fromProto(Some(d))))
|
||||
)
|
||||
)
|
||||
)
|
||||
case DidBattle =>
|
||||
ChangedFactionC(
|
||||
factionId = battleRevelation.revealedToFactionId,
|
||||
updatedReconnedProvinces = Vector(
|
||||
ProvinceViewFilter
|
||||
.filteredProvinceView(
|
||||
provinceProto,
|
||||
gsProto
|
||||
// FIXME: setting factionId to None right now to grab the full info. This
|
||||
// probably isn't exactly what we want.
|
||||
)
|
||||
.withAsOf(gsProto.currentDate.get)
|
||||
ProvinceViewConverter.toProto(
|
||||
ProvinceViewFilter
|
||||
.filteredProvinceView(
|
||||
gs.provinces(battleRevelation.provinceId),
|
||||
gs
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
},
|
||||
|
||||
+8
-13
@@ -1,7 +1,6 @@
|
||||
package net.eagle0.eagle.library.actions.impl.action
|
||||
|
||||
import net.eagle0.common.{FunctionalRandom, RandomState}
|
||||
import net.eagle0.eagle.internal.game_state.GameState as GameStateProto
|
||||
import net.eagle0.eagle.library.actions.applier.ActionResultApplier
|
||||
import net.eagle0.eagle.library.actions.impl.common.ProtolessRandomSequentialResultsAction
|
||||
import net.eagle0.eagle.library.actions.random_state_sequencer.RandomStateSequencer
|
||||
@@ -11,7 +10,7 @@ import net.eagle0.eagle.model.action_result.changed_province.concrete.ChangedPro
|
||||
import net.eagle0.eagle.model.action_result.concrete.{ActionResultC, ChangedFactionC, ClientTextVisibilityExtensionC}
|
||||
import net.eagle0.eagle.model.action_result.types.{EndReconResolutionPhaseResultType, ReconSucceededResultType}
|
||||
import net.eagle0.eagle.model.action_result.ActionResultT
|
||||
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
|
||||
import net.eagle0.eagle.model.proto_converters.view.province.ProvinceViewConverter
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.province.{IncomingEndTurnAction, IncomingRecon}
|
||||
import net.eagle0.eagle.model.state.RoundPhase
|
||||
@@ -22,9 +21,6 @@ case class PerformReconResolutionAction(
|
||||
actionResultApplier: ActionResultApplier
|
||||
) extends ProtolessRandomSequentialResultsAction {
|
||||
|
||||
// Proto GameState still needed for ProvinceViewFilter.filteredProvinceView
|
||||
private lazy val startingGameStateProto: GameStateProto = GameStateConverter.toProto(gameState)
|
||||
|
||||
override protected def randomResults(
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Vector[ActionResultT]] =
|
||||
@@ -94,14 +90,13 @@ case class PerformReconResolutionAction(
|
||||
ChangedFactionC(
|
||||
factionId = fromFactionId,
|
||||
updatedReconnedProvinces = Vector(
|
||||
ProvinceViewFilter
|
||||
.filteredProvinceView(
|
||||
startingGameStateProto.provinces(provinceId),
|
||||
startingGameStateProto
|
||||
// FIXME: setting factionId to None right now to grab the full info. This
|
||||
// probably isn't exactly what we want.
|
||||
)
|
||||
.withAsOf(startingGameStateProto.currentDate.get)
|
||||
ProvinceViewConverter.toProto(
|
||||
ProvinceViewFilter
|
||||
.filteredProvinceView(
|
||||
gameState.provinces(provinceId),
|
||||
gameState
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
+2
-2
@@ -119,8 +119,8 @@ object ResolveRansomOfferCommand {
|
||||
case Accepted =>
|
||||
NotificationDetails.RansomPaid(
|
||||
ransomedHeroId = ransomOffer.prisonerToBeRansomed.prisonerHeroId,
|
||||
ransomPaidByFactionId = actingFactionId,
|
||||
ransomPaidToFactionId = ransomOffer.originatingFactionId
|
||||
ransomPaidByFactionId = ransomOffer.originatingFactionId,
|
||||
ransomPaidToFactionId = actingFactionId
|
||||
)
|
||||
|
||||
case Rejected =>
|
||||
|
||||
@@ -386,8 +386,12 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/util:__pkg__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/stat_with_condition",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/views:stat_with_condition_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/stat_with_condition",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package net.eagle0.eagle.library.util
|
||||
|
||||
import net.eagle0.eagle.model.view.stat_with_condition.{Condition, StatWithCondition as StatWithConditionT}
|
||||
import net.eagle0.eagle.views.stat_with_condition.StatWithCondition
|
||||
import net.eagle0.eagle.views.stat_with_condition.StatWithCondition.Condition.{HIGH, LOW, MEDIUM}
|
||||
|
||||
@@ -11,6 +12,13 @@ object StatWithConditionUtils {
|
||||
minHigh = 55
|
||||
)
|
||||
|
||||
def supportScala(support: Double): StatWithConditionT =
|
||||
scWithRangesScala(
|
||||
support.floor.toInt,
|
||||
minMedium = 40,
|
||||
minHigh = 55
|
||||
)
|
||||
|
||||
def scWithRanges(
|
||||
stat: Int,
|
||||
minMedium: Int,
|
||||
@@ -23,4 +31,17 @@ object StatWithConditionUtils {
|
||||
else if stat >= minMedium then MEDIUM
|
||||
else LOW
|
||||
)
|
||||
|
||||
def scWithRangesScala(
|
||||
stat: Int,
|
||||
minMedium: Int,
|
||||
minHigh: Int
|
||||
): StatWithConditionT =
|
||||
StatWithConditionT(
|
||||
stat = stat,
|
||||
condition =
|
||||
if stat >= minHigh then Condition.High
|
||||
else if stat >= minMedium then Condition.Medium
|
||||
else Condition.Low
|
||||
)
|
||||
}
|
||||
|
||||
@@ -56,6 +56,13 @@ object FactionUtils {
|
||||
): Boolean =
|
||||
hasTruce(fid1, fid2, factions) || hasAlliance(fid1, fid2, factions)
|
||||
|
||||
def factionsAreHostile(
|
||||
fid1: FactionId,
|
||||
fid2: FactionId,
|
||||
factions: Vector[FactionT]
|
||||
): Boolean =
|
||||
fid1 != fid2 && !hasTruceOrAlliance(fid1, fid2, factions)
|
||||
|
||||
private def incomingHids(province: ProvinceT): Vector[HeroId] =
|
||||
province.incomingArmies
|
||||
.map(_.army)
|
||||
|
||||
@@ -42,11 +42,13 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:gold_increase_per_economy",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:min_support_for_taxes",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:per_development_resource_limit",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:battalion_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province/concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:army",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:beast_info",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/faction",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero:gender",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
|
||||
@@ -9,11 +9,13 @@ import net.eagle0.eagle.library.settings.{
|
||||
MinSupportForTaxes,
|
||||
PerDevelopmentResourceLimit
|
||||
}
|
||||
import net.eagle0.eagle.library.util.BattalionUtils
|
||||
import net.eagle0.eagle.library.EagleInternalException
|
||||
import net.eagle0.eagle.model.action_result.changed_province.concrete.ChangedProvinceC
|
||||
import net.eagle0.eagle.model.action_result.changed_province.ChangedProvinceT
|
||||
import net.eagle0.eagle.model.state.{BattalionTypeId, BeastInfo, MovingArmy}
|
||||
import net.eagle0.eagle.model.state.faction.FactionT
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.hero.HeroT
|
||||
import net.eagle0.eagle.model.state.province.{
|
||||
BeastsEvent,
|
||||
@@ -74,6 +76,15 @@ object ProvinceUtils {
|
||||
) * FoodIncreasePerAgriculture.doubleValue).toInt
|
||||
)
|
||||
|
||||
def monthlyFoodConsumption(
|
||||
province: ProvinceT,
|
||||
gs: GameState
|
||||
): Int =
|
||||
BattalionUtils.monthlyConsumedFood(
|
||||
province.battalionIds.map(gs.battalions),
|
||||
gs.battalionTypes
|
||||
)
|
||||
|
||||
def provinceLeader(
|
||||
startingProvince: ProvinceT,
|
||||
rulingFactionOpt: Option[FactionT],
|
||||
|
||||
@@ -1,14 +1,34 @@
|
||||
package net.eagle0.eagle.library.util.view_filters
|
||||
|
||||
import net.eagle0.eagle.{BattalionId, FactionId}
|
||||
import net.eagle0.eagle.internal.army.Army
|
||||
import net.eagle0.eagle.internal.battalion.Battalion
|
||||
import net.eagle0.eagle.internal.army.Army as ProtoArmy
|
||||
import net.eagle0.eagle.internal.battalion.Battalion as ProtoBattalion
|
||||
import net.eagle0.eagle.model.proto_converters.CombatUnitConverter
|
||||
import net.eagle0.eagle.model.state.battalion.BattalionT
|
||||
import net.eagle0.eagle.model.state.Army as ScalaArmy
|
||||
import net.eagle0.eagle.model.view.army.ArmyView as ArmyViewT
|
||||
import net.eagle0.eagle.views.army_view.ArmyView
|
||||
|
||||
object ArmyFilter {
|
||||
// Pure Scala version
|
||||
def filterArmy(
|
||||
army: Army,
|
||||
battalions: Map[BattalionId, Battalion],
|
||||
army: ScalaArmy,
|
||||
battalions: Map[BattalionId, BattalionT],
|
||||
factionId: Option[FactionId]
|
||||
): ArmyViewT =
|
||||
ArmyViewT(
|
||||
factionId = army.factionId,
|
||||
units =
|
||||
if factionId.forall(_ == army.factionId) then army.units
|
||||
else Vector.empty,
|
||||
unitCount = army.units.length,
|
||||
troopCount = army.units.flatMap(_.battalionId).map(bid => battalions(bid)).map(_.size).sum
|
||||
)
|
||||
|
||||
// Proto version (legacy)
|
||||
def filterArmy(
|
||||
army: ProtoArmy,
|
||||
battalions: Map[BattalionId, ProtoBattalion],
|
||||
factionId: Option[FactionId]
|
||||
): ArmyView =
|
||||
ArmyView(
|
||||
@@ -19,4 +39,18 @@ object ArmyFilter {
|
||||
unitCount = army.units.length,
|
||||
troopCount = army.units.flatMap(_.battalionId).map(battalions).map(_.size).sum
|
||||
)
|
||||
|
||||
def filterArmyScala(
|
||||
army: ProtoArmy,
|
||||
battalions: Map[BattalionId, ProtoBattalion],
|
||||
factionId: Option[FactionId]
|
||||
): ArmyViewT =
|
||||
ArmyViewT(
|
||||
factionId = army.factionId,
|
||||
units =
|
||||
if factionId.forall(_ == army.factionId) then army.units.map(CombatUnitConverter.fromProto).toVector
|
||||
else Vector.empty,
|
||||
unitCount = army.units.length,
|
||||
troopCount = army.units.flatMap(_.battalionId).map(battalions).map(_.size).sum
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,11 +8,18 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/library:__subpackages__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/army:army_view",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:army_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:battalion_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/views:army_view_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:combat_unit_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:army",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/army:army_view",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -116,6 +123,7 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/view_filters:faction_view_filter",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/view_filters:hero_view_filter",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/view_filters:province_view_filter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/view/province:province_view",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -146,10 +154,15 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/library:__subpackages__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/battalion",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:battalion_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/views:battalion_view_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:battalion_type_id_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/battalion",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -161,20 +174,40 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/library:__subpackages__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/province:full_province_info",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/province:province_view",
|
||||
],
|
||||
deps = [
|
||||
":army_filter",
|
||||
":battalion_view_filter",
|
||||
":legacy_battalion_view_filter",
|
||||
":visibility",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:beast_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:date_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:incoming_army_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:province_event_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:stat_with_condition_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils:legacy_faction_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/hero:hero_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/hero:legacy_hero_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/province",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/province:legacy_province_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:unaffiliated_hero_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/date",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/hero:profession",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/province:order_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/view/province:full_province_info",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/view/province:province_view",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/incoming_army:incoming_army_view",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/province:full_province_info",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/province:province_view",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/province:unaffiliated_hero_basics",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package net.eagle0.eagle.library.util.view_filters
|
||||
|
||||
import net.eagle0.eagle.internal.game_state.GameState
|
||||
import net.eagle0.eagle.model.proto_converters.view.province.ProvinceViewConverter
|
||||
import net.eagle0.eagle.views.faction_view.FactionView
|
||||
import net.eagle0.eagle.views.game_state_view.GameStateView
|
||||
import net.eagle0.eagle.FactionId
|
||||
@@ -16,9 +17,11 @@ object GameStateViewFilter {
|
||||
currentDate = gs.currentDate,
|
||||
provinces = gs.provinces.map {
|
||||
case (pid, prov) =>
|
||||
pid -> factionId
|
||||
.map(fid => ProvinceViewFilter.filteredProvinceView(prov, gs, fid))
|
||||
.getOrElse(ProvinceViewFilter.filteredProvinceView(prov, gs))
|
||||
pid -> ProvinceViewConverter.toProto(
|
||||
factionId
|
||||
.map(fid => ProvinceViewFilter.filteredProvinceView(prov, gs, fid))
|
||||
.getOrElse(ProvinceViewFilter.filteredProvinceView(prov, gs))
|
||||
)
|
||||
},
|
||||
heroes = (gs.heroes ++ gs.killedHeroes).map {
|
||||
case (hid, hero) =>
|
||||
|
||||
+22
@@ -1,6 +1,8 @@
|
||||
package net.eagle0.eagle.library.util.view_filters
|
||||
|
||||
import net.eagle0.eagle.internal.battalion.Battalion
|
||||
import net.eagle0.eagle.model.proto_converters.BattalionTypeIdConverter
|
||||
import net.eagle0.eagle.model.view.battalion.BattalionView as BattalionViewT
|
||||
import net.eagle0.eagle.views.battalion_view.BattalionView
|
||||
|
||||
object LegacyBattalionViewFilter {
|
||||
@@ -14,6 +16,16 @@ object LegacyBattalionViewFilter {
|
||||
name = batt.name
|
||||
)
|
||||
|
||||
def filteredBattalionViewBelongingToPlayerScala(batt: Battalion): BattalionViewT =
|
||||
BattalionViewT(
|
||||
id = batt.id,
|
||||
typeId = BattalionTypeIdConverter.fromProto(batt.`type`),
|
||||
size = batt.size,
|
||||
training = Some(batt.training),
|
||||
armament = Some(batt.armament),
|
||||
name = batt.name
|
||||
)
|
||||
|
||||
def limitedBattalionView(batt: Battalion): BattalionView =
|
||||
BattalionView(
|
||||
id = batt.id,
|
||||
@@ -23,4 +35,14 @@ object LegacyBattalionViewFilter {
|
||||
armament = None,
|
||||
name = batt.name
|
||||
)
|
||||
|
||||
def limitedBattalionViewScala(batt: Battalion): BattalionViewT =
|
||||
BattalionViewT(
|
||||
id = batt.id,
|
||||
typeId = BattalionTypeIdConverter.fromProto(batt.`type`),
|
||||
size = batt.size,
|
||||
training = None,
|
||||
armament = None,
|
||||
name = batt.name
|
||||
)
|
||||
}
|
||||
|
||||
+104
-33
@@ -8,14 +8,77 @@ import net.eagle0.eagle.internal.province.Province
|
||||
import net.eagle0.eagle.internal.unaffiliated_hero.UnaffiliatedHero
|
||||
import net.eagle0.eagle.library.util.{IncomingArmyUtils, ProvinceEventUtils}
|
||||
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
|
||||
import net.eagle0.eagle.library.util.hero.HeroUtils
|
||||
import net.eagle0.eagle.library.util.hero.LegacyHeroUtils
|
||||
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
|
||||
import net.eagle0.eagle.library.util.StatWithConditionUtils.supportSc
|
||||
import net.eagle0.eagle.views.incoming_army_view.IncomingArmyView
|
||||
import net.eagle0.eagle.views.province_view.{FullProvinceInfo, ProvinceView, UnaffiliatedHeroBasics}
|
||||
import net.eagle0.eagle.library.util.province.ProvinceUtils
|
||||
import net.eagle0.eagle.library.util.StatWithConditionUtils.supportScala
|
||||
import net.eagle0.eagle.model.proto_converters.date.DateConverter
|
||||
import net.eagle0.eagle.model.proto_converters.hero.ProfessionConverter
|
||||
import net.eagle0.eagle.model.proto_converters.province.ProvinceOrderTypeConverter
|
||||
import net.eagle0.eagle.model.proto_converters.view.province.{FullProvinceInfoConverter, ProvinceViewConverter}
|
||||
import net.eagle0.eagle.model.proto_converters.UnaffiliatedHeroConverter
|
||||
import net.eagle0.eagle.model.state.game_state.GameState as ScalaGameState
|
||||
import net.eagle0.eagle.model.state.province.ProvinceT
|
||||
import net.eagle0.eagle.model.state.unaffiliated_hero.RecruitmentInfo
|
||||
import net.eagle0.eagle.model.view.incoming_army.{IncomingArmyView, UnitDetails}
|
||||
import net.eagle0.eagle.model.view.province.{FullProvinceInfo, ProvinceView, UnaffiliatedHeroBasics}
|
||||
import net.eagle0.eagle.FactionId
|
||||
|
||||
object ProvinceViewFilter {
|
||||
// =============== Scala GameState overloads ===============
|
||||
|
||||
/** Filtered view for server-side use (no faction visibility restrictions) */
|
||||
def filteredProvinceView(
|
||||
province: ProvinceT,
|
||||
gs: ScalaGameState
|
||||
): ProvinceView = {
|
||||
val factions = gs.factions.values.toVector
|
||||
val factionLeaderIds = factions.flatMap(_.leaderIds)
|
||||
|
||||
val info: FullProvinceInfo = FullProvinceInfo(
|
||||
rulingHeroId = province.rulingHeroId,
|
||||
rulingFactionHeroIds = province.rulingFactionHeroIds
|
||||
.map(hid => gs.heroes(hid))
|
||||
.sortWith(HeroUtils.sortOrderer(factionLeaderIds))
|
||||
.map(_.id),
|
||||
battalions = province.battalionIds
|
||||
.map(bid => gs.battalions(bid))
|
||||
.map(BattalionViewFilter.filteredBattalionViewBelongingToPlayer)
|
||||
.sortBy(_.typeId.value),
|
||||
defendingArmy = province.defendingArmy
|
||||
.map(army => ArmyFilter.filterArmy(army, gs.battalions, None)),
|
||||
economy = province.economy.toFloat,
|
||||
agriculture = province.agriculture.toFloat,
|
||||
infrastructure = province.infrastructure.toFloat,
|
||||
economyDevastation = province.economyDevastation.toFloat,
|
||||
agricultureDevastation = province.agricultureDevastation.toFloat,
|
||||
infrastructureDevastation = province.infrastructureDevastation.toFloat,
|
||||
gold = province.gold,
|
||||
food = province.food,
|
||||
priceIndex = province.priceIndex,
|
||||
goldCap = ProvinceUtils.goldCap(province),
|
||||
foodCap = ProvinceUtils.foodCap(province),
|
||||
support = Some(supportScala(province.support)),
|
||||
foodConsumption = ProvinceUtils.monthlyFoodConsumption(province, gs),
|
||||
provinceOrders = ProvinceOrderTypeConverter.toProto(province.provinceOrders),
|
||||
hexMapName = province.hexMapName,
|
||||
castleCount = province.castleCount,
|
||||
heroCap = province.heroCap,
|
||||
unaffiliatedHeroes = Vector.empty,
|
||||
rulerIsTraveling = province.rulerIsTraveling
|
||||
)
|
||||
|
||||
ProvinceView(
|
||||
id = province.id,
|
||||
name = province.name,
|
||||
rulingFactionId = province.rulingFactionId,
|
||||
fullInfo = Some(info),
|
||||
knownEvents = Vector.empty,
|
||||
asOf = gs.currentDate
|
||||
)
|
||||
}
|
||||
|
||||
private val universallyVisibleCheckers: List[ProvinceEvent => Boolean] = List(
|
||||
ProvinceEventUtils.isBlizzardEvent,
|
||||
ProvinceEventUtils.isFestivalEvent,
|
||||
@@ -36,21 +99,19 @@ object ProvinceViewFilter {
|
||||
name = province.name,
|
||||
rulingFactionId = province.rulingFactionId,
|
||||
fullInfo = Some(fullProvinceInfo(province, gs, None)),
|
||||
knownEvents = province.activeEvents,
|
||||
asOf = Some(gs.currentDate.get)
|
||||
knownEvents = province.activeEvents.toVector,
|
||||
asOf = Some(DateConverter.fromProto(gs.currentDate))
|
||||
)
|
||||
|
||||
private def deadHeroesFilteredOut(
|
||||
fullProvinceInfo: FullProvinceInfo,
|
||||
gameState: GameState
|
||||
): FullProvinceInfo =
|
||||
fullProvinceInfo.update(
|
||||
_.rulingFactionHeroIds.modify(
|
||||
_.filterNot(hid => gameState.killedHeroes.contains(hid))
|
||||
),
|
||||
_.unaffiliatedHeroes.modify(
|
||||
_.filterNot(uh => gameState.killedHeroes.contains(uh.heroId))
|
||||
)
|
||||
fullProvinceInfo.copy(
|
||||
rulingFactionHeroIds =
|
||||
fullProvinceInfo.rulingFactionHeroIds.filterNot(hid => gameState.killedHeroes.contains(hid)),
|
||||
unaffiliatedHeroes =
|
||||
fullProvinceInfo.unaffiliatedHeroes.filterNot(uh => gameState.killedHeroes.contains(uh.heroId))
|
||||
)
|
||||
|
||||
def filteredProvinceView(
|
||||
@@ -65,11 +126,13 @@ object ProvinceViewFilter {
|
||||
) ++ gs.factions.get(factionId)
|
||||
|
||||
// Take the most recent reconned view from this faction or any allies
|
||||
// (stored as proto, convert to Scala)
|
||||
val reconnedView =
|
||||
relevantFactions
|
||||
.flatMap(_.reconnedProvinces)
|
||||
.filter(_.id == province.id)
|
||||
.maxByOption(_.getAsOf)
|
||||
.map(ProvinceViewConverter.fromProto)
|
||||
|
||||
val showAll = province.rulingFactionId.exists { rfid =>
|
||||
Visibility.hasFullVisibility(factionId, rfid, gs)
|
||||
@@ -103,7 +166,7 @@ object ProvinceViewFilter {
|
||||
food = shipment.supplies.map(_.food).getOrElse(0),
|
||||
gold = shipment.supplies.map(_.gold).getOrElse(0)
|
||||
)
|
||||
}
|
||||
}.toVector
|
||||
else Vector()
|
||||
|
||||
val fullInfo = Option
|
||||
@@ -117,9 +180,9 @@ object ProvinceViewFilter {
|
||||
name = province.name,
|
||||
rulingFactionId = province.rulingFactionId,
|
||||
fullInfo = fullInfo,
|
||||
knownEvents = knownEvents,
|
||||
knownEvents = knownEvents.toVector,
|
||||
asOf =
|
||||
if showAll then Some(gs.currentDate.get)
|
||||
if showAll then Some(DateConverter.fromProto(gs.currentDate))
|
||||
else reconnedView.flatMap(_.asOf),
|
||||
knownIncomingArmies = incomingAttackers ++ incomingSupplies
|
||||
)
|
||||
@@ -131,10 +194,11 @@ object ProvinceViewFilter {
|
||||
gs: GameState,
|
||||
factionId: FactionId
|
||||
): ProvinceView = {
|
||||
// Stored as proto, convert to Scala
|
||||
val reconnedView = for {
|
||||
faction <- gs.factions.get(factionId)
|
||||
pv <- faction.reconnedProvinces.find(_.id == province.id)
|
||||
} yield pv
|
||||
} yield ProvinceViewConverter.fromProto(pv)
|
||||
|
||||
val knownEvents = province.activeEvents.filter { pe =>
|
||||
if reconnedView.exists(_.knownEvents.contains(pe)) then true
|
||||
@@ -148,10 +212,13 @@ object ProvinceViewFilter {
|
||||
val fullInfo = reconnedView
|
||||
.flatMap(_.fullInfo)
|
||||
.getOrElse(fullProvinceInfo(province, gs, Some(factionId)))
|
||||
.update(
|
||||
_.battalions := province.battalionIds.map(gs.battalions).map {
|
||||
LegacyBattalionViewFilter.limitedBattalionView
|
||||
}
|
||||
.copy(
|
||||
battalions = province.battalionIds
|
||||
.map(gs.battalions)
|
||||
.map {
|
||||
LegacyBattalionViewFilter.limitedBattalionViewScala
|
||||
}
|
||||
.toVector
|
||||
)
|
||||
|
||||
ProvinceView(
|
||||
@@ -159,7 +226,7 @@ object ProvinceViewFilter {
|
||||
name = province.name,
|
||||
rulingFactionId = province.rulingFactionId,
|
||||
fullInfo = Some(fullInfo),
|
||||
knownEvents = knownEvents,
|
||||
knownEvents = knownEvents.toVector,
|
||||
asOf = reconnedView.flatMap(_.asOf),
|
||||
knownIncomingArmies = incomingAttackers ++ incomingSupplies
|
||||
)
|
||||
@@ -172,13 +239,15 @@ object ProvinceViewFilter {
|
||||
): FullProvinceInfo = FullProvinceInfo(
|
||||
rulingHeroId = province.rulingHeroId,
|
||||
rulingFactionHeroIds = province.rulingFactionHeroIds
|
||||
.sortWith(LegacyHeroUtils.sortOrderer(gs)),
|
||||
.sortWith(LegacyHeroUtils.sortOrderer(gs))
|
||||
.toVector,
|
||||
battalions = province.battalionIds
|
||||
.map(gs.battalions)
|
||||
.map(LegacyBattalionViewFilter.filteredBattalionViewBelongingToPlayer)
|
||||
.sortBy(_.`type`.index),
|
||||
.map(LegacyBattalionViewFilter.filteredBattalionViewBelongingToPlayerScala)
|
||||
.sortBy(_.typeId.value)
|
||||
.toVector,
|
||||
defendingArmy = province.defendingArmy
|
||||
.map(ArmyFilter.filterArmy(_, gs.battalions, factionId)),
|
||||
.map(ArmyFilter.filterArmyScala(_, gs.battalions, factionId)),
|
||||
economy = province.economy.toFloat,
|
||||
agriculture = province.agriculture.toFloat,
|
||||
infrastructure = province.infrastructure.toFloat,
|
||||
@@ -190,13 +259,13 @@ object ProvinceViewFilter {
|
||||
priceIndex = province.priceIndex,
|
||||
goldCap = LegacyProvinceUtils.goldCap(province),
|
||||
foodCap = LegacyProvinceUtils.foodCap(province),
|
||||
support = Some(supportSc(province.support)),
|
||||
support = Some(supportScala(province.support)),
|
||||
foodConsumption = LegacyProvinceUtils.monthlyFoodConsumption(province.id, gs),
|
||||
provinceOrders = province.provinceOrders,
|
||||
hexMapName = province.hexMapName,
|
||||
castleCount = province.castleCount,
|
||||
heroCap = province.heroCap,
|
||||
unaffiliatedHeroes = province.unaffiliatedHeroes.map(unaffiliatedHeroInfo(_, gs)),
|
||||
unaffiliatedHeroes = province.unaffiliatedHeroes.map(unaffiliatedHeroInfo(_, gs)).toVector,
|
||||
rulerIsTraveling = province.rulerIsTraveling
|
||||
)
|
||||
|
||||
@@ -224,6 +293,7 @@ object ProvinceViewFilter {
|
||||
)
|
||||
)
|
||||
.map(army => incomingArmyInfo(ma = army, gs = gs, showDetails = false))
|
||||
.toVector
|
||||
|
||||
private def incomingArmyInfo(
|
||||
ma: MovingArmy,
|
||||
@@ -246,14 +316,14 @@ object ProvinceViewFilter {
|
||||
unitDetails =
|
||||
if showDetails then
|
||||
ma.getArmy.units.map { cu =>
|
||||
IncomingArmyView.UnitDetails(
|
||||
UnitDetails(
|
||||
heroId = cu.heroId,
|
||||
battalion = cu.battalionId.map(gs.battalions).map { b =>
|
||||
LegacyBattalionViewFilter
|
||||
.filteredBattalionViewBelongingToPlayer(b)
|
||||
.filteredBattalionViewBelongingToPlayerScala(b)
|
||||
}
|
||||
)
|
||||
}
|
||||
}.toVector
|
||||
else Vector()
|
||||
)
|
||||
|
||||
@@ -265,10 +335,11 @@ object ProvinceViewFilter {
|
||||
UnaffiliatedHeroBasics(
|
||||
heroId = hero.id,
|
||||
nameTextId = hero.nameTextId,
|
||||
profession = hero.profession,
|
||||
status = uh.`type`,
|
||||
profession = ProfessionConverter.fromProto(hero.profession),
|
||||
status = UnaffiliatedHeroConverter.unaffiliatedHeroTypeFromProto(uh.`type`),
|
||||
isFactionLeader = LegacyFactionUtils.isFactionLeader(hero.id, gs),
|
||||
recruitmentInfo = uh.recruitmentInfo
|
||||
recruitmentInfo =
|
||||
uh.recruitmentInfo.fold(RecruitmentInfo.Unknown)(UnaffiliatedHeroConverter.recruitmentInfoFromProto)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package net.eagle0.eagle.model.action_result
|
||||
|
||||
import net.eagle0.eagle.{BattalionId, FactionId, HeroId, ProvinceId, RoundId}
|
||||
import net.eagle0.eagle.api.selected_command.SelectedCommand
|
||||
import net.eagle0.eagle.model.action_result.changed_province.ChangedProvinceT
|
||||
import net.eagle0.eagle.model.action_result.generated_text_request.GeneratedTextRequestT
|
||||
import net.eagle0.eagle.model.action_result.types.base.ActionResultType
|
||||
@@ -27,7 +28,7 @@ trait ActionResultT {
|
||||
def newRoundId: Option[RoundId]
|
||||
def newDate: Option[Date]
|
||||
|
||||
// def lastCommandTypeForActingProvince: Option[CommandType]
|
||||
def lastCommandTypeForActingProvince: Option[SelectedCommand]
|
||||
|
||||
def newBattle: Option[ShardokBattle]
|
||||
// def resolvedBattle: Option[String]
|
||||
@@ -73,7 +74,7 @@ trait ActionResultT {
|
||||
newRoundPhase: Option[RoundPhase] = newRoundPhase,
|
||||
newRoundId: Option[RoundId] = newRoundId,
|
||||
newDate: Option[Date] = newDate,
|
||||
// lastCommandTypeForActingProvince: Option[CommandType] = None,
|
||||
lastCommandTypeForActingProvince: Option[SelectedCommand] = lastCommandTypeForActingProvince,
|
||||
newBattle: Option[ShardokBattle] = newBattle,
|
||||
// resolvedBattle: Option[String] = None,
|
||||
// resolvedImminentBattle: Option[Int] = None,
|
||||
|
||||
@@ -51,6 +51,7 @@ scala_library(
|
||||
":changed_hero_trait",
|
||||
":client_text_visibility_extension_trait",
|
||||
":notification_trait",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package net.eagle0.eagle.model.action_result.concrete
|
||||
|
||||
import net.eagle0.eagle.{BattalionId, FactionId, HeroId, ProvinceId, RoundId}
|
||||
import net.eagle0.eagle.api.selected_command.SelectedCommand
|
||||
import net.eagle0.eagle.model.action_result.{
|
||||
ActionResultT,
|
||||
ChangedBattalionT,
|
||||
@@ -34,6 +35,7 @@ case class ActionResultC(
|
||||
newRoundPhase: Option[RoundPhase] = None,
|
||||
newRoundId: Option[RoundId] = None,
|
||||
newDate: Option[Date] = None,
|
||||
lastCommandTypeForActingProvince: Option[SelectedCommand] = None,
|
||||
newBattle: Option[ShardokBattle] = None,
|
||||
newChronicleEntry: Option[ChronicleEntry] = None,
|
||||
changedBattalions: Vector[ChangedBattalionT] = Vector.empty,
|
||||
@@ -68,6 +70,7 @@ case class ActionResultC(
|
||||
newRoundPhase: Option[RoundPhase] = newRoundPhase,
|
||||
newRoundId: Option[RoundId] = newRoundId,
|
||||
newDate: Option[Date] = newDate,
|
||||
lastCommandTypeForActingProvince: Option[SelectedCommand] = lastCommandTypeForActingProvince,
|
||||
newBattle: Option[ShardokBattle] = newBattle,
|
||||
newChronicleEntry: Option[ChronicleEntry] = newChronicleEntry,
|
||||
changedBattalions: Vector[ChangedBattalionT] = changedBattalions,
|
||||
@@ -99,6 +102,7 @@ case class ActionResultC(
|
||||
newRoundPhase = newRoundPhase,
|
||||
newRoundId = newRoundId,
|
||||
newDate = newDate,
|
||||
lastCommandTypeForActingProvince = lastCommandTypeForActingProvince,
|
||||
newBattle = newBattle,
|
||||
newChronicleEntry = newChronicleEntry,
|
||||
changedBattalions = changedBattalions,
|
||||
|
||||
@@ -16,6 +16,7 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types/base:action_result_type",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
|
||||
|
||||
+3
@@ -1,6 +1,7 @@
|
||||
package net.eagle0.eagle.model.proto_converters
|
||||
|
||||
import net.eagle0.eagle.{BattalionId, FactionId, HeroId, ProvinceId, RoundId}
|
||||
import net.eagle0.eagle.api.selected_command.SelectedCommand
|
||||
import net.eagle0.eagle.common.action_result_type.ActionResultType as ActionResultTypeProto
|
||||
import net.eagle0.eagle.common.round_phase.NewRoundPhase as NewRoundPhaseProto
|
||||
import net.eagle0.eagle.internal.action_result.ActionResult as ActionResultProto
|
||||
@@ -53,6 +54,7 @@ object ActionResultProtoConverter {
|
||||
newRoundPhase: Option[RoundPhase],
|
||||
newRoundId: Option[RoundId],
|
||||
newDate: Option[Date],
|
||||
lastCommandTypeForActingProvince: Option[SelectedCommand],
|
||||
newBattle: Option[ShardokBattle],
|
||||
newChronicleEntry: Option[ChronicleEntry],
|
||||
changedBattalions: Vector[ChangedBattalionT],
|
||||
@@ -86,6 +88,7 @@ object ActionResultProtoConverter {
|
||||
newRoundPhase = newRoundPhase.map(rp => NewRoundPhaseProto(RoundPhaseConverter.toProto(rp))),
|
||||
newRoundId = newRoundId,
|
||||
newDate = newDate.map(DateConverter.toProto),
|
||||
lastCommandTypeForActingProvince = lastCommandTypeForActingProvince.getOrElse(SelectedCommand.Empty),
|
||||
newBattle = newBattle.map(ShardokBattleConverter.toProto),
|
||||
newChronicleEntry = newChronicleEntry.map(ChronicleEntryConverter.toProto),
|
||||
changedBattalions = changedBattalions.map {
|
||||
|
||||
@@ -24,6 +24,7 @@ scala_library(
|
||||
":changed_province_converter",
|
||||
":notification_converter",
|
||||
":round_phase_converter",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:action_result_type_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
@@ -110,6 +111,9 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/view/battalion:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type_id",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:battalion_type_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
@@ -257,10 +261,14 @@ scala_library(
|
||||
srcs = ["CombatUnitConverter.scala"],
|
||||
visibility = [
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/view_filters:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/view:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/actions/impl/command:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:combat_unit",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:combat_unit_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
@@ -426,11 +434,15 @@ scala_library(
|
||||
visibility = [
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/view_filters:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/province:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/view:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/unaffiliated_hero",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:unaffiliated_hero_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
|
||||
@@ -31,6 +31,11 @@ object UnaffiliatedHeroConverter {
|
||||
UNAFFILIATED_HERO_RETURNING_PRISONER -> UnaffiliatedHeroType.ReturningPrisoner
|
||||
)
|
||||
|
||||
def unaffiliatedHeroTypeFromProto(
|
||||
protoType: UnaffiliatedHeroTypeProto
|
||||
): UnaffiliatedHeroType =
|
||||
unaffiliatedHeroTypeMap.getOrElse(protoType, UnaffiliatedHeroType.Unknown)
|
||||
|
||||
def recruitmentInfoFromProto(
|
||||
recruitmentInfoProto: RecruitmentInfoProto
|
||||
): RecruitmentInfo = recruitmentInfoProto.status match {
|
||||
|
||||
@@ -7,6 +7,7 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/quest_fulfillment:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/view_filters:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/model:__subpackages__",
|
||||
"//src/main/scala/net/eagle0/eagle/service:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/service/new_game_creation:__pkg__",
|
||||
|
||||
@@ -85,6 +85,7 @@ scala_library(
|
||||
visibility = [
|
||||
"//src/main/scala/net/eagle0/eagle/library:__subpackages__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/view_filters:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:__subpackages__",
|
||||
"//src/main/scala/net/eagle0/eagle/service:__pkg__",
|
||||
|
||||
@@ -45,6 +45,7 @@ scala_library(
|
||||
srcs = ["ProvinceEventConverter.scala"],
|
||||
visibility = [
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/view_filters:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
|
||||
],
|
||||
@@ -65,6 +66,7 @@ scala_library(
|
||||
visibility = [
|
||||
"//src/main/scala/net/eagle0/eagle/library:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/view_filters:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
|
||||
+3
-11
@@ -100,7 +100,7 @@ object ProvinceConverter {
|
||||
hexMapName: String,
|
||||
castleCount: Int,
|
||||
heroCap: Int,
|
||||
lastCommand: Any
|
||||
lastCommand: Option[SelectedCommand]
|
||||
) =>
|
||||
ProvinceProto(
|
||||
id = id,
|
||||
@@ -131,15 +131,7 @@ object ProvinceConverter {
|
||||
activeEvents = activeEvents.map(ProvinceEventConverter.toProto),
|
||||
lastBeastsDate = lastBeastsDate.map(DateConverter.toProto),
|
||||
lastRiotDate = lastRiotDate.map(DateConverter.toProto),
|
||||
lastCommand = lastCommand match {
|
||||
case Some(sc: SelectedCommand) =>
|
||||
sc
|
||||
case Some(x) =>
|
||||
throw new ProtoConversionException(
|
||||
s"Unknown lastCommand type: ${x.getClass}"
|
||||
)
|
||||
case None => SelectedCommand.Empty
|
||||
},
|
||||
lastCommand = lastCommand.getOrElse(SelectedCommand.Empty),
|
||||
incomingArmies = incomingArmies.map(ArmyConverter.toProto),
|
||||
hostileArmies = hostileArmies.map(ArmyConverter.toProto),
|
||||
defendingArmy = defendingArmy.map(ArmyConverter.toProto),
|
||||
@@ -228,7 +220,7 @@ object ProvinceConverter {
|
||||
activeEvents = activeEvents.map(ProvinceEventConverter.fromProto).toVector,
|
||||
lastBeastsDate = lastBeastsDate.map(date => DateConverter.fromProto(Some(date))),
|
||||
lastRiotDate = lastRiotDate.map(date => DateConverter.fromProto(Some(date))),
|
||||
lastCommand = Some(lastCommand),
|
||||
lastCommand = Option.when(!lastCommand.isEmpty)(lastCommand),
|
||||
incomingArmies = incomingArmies.map(ArmyConverter.fromProto).toVector,
|
||||
hostileArmies = hostileArmies.map(ArmyConverter.fromProto).toVector,
|
||||
defendingArmy = defendingArmy.map(ArmyConverter.fromProto),
|
||||
|
||||
+2
-2
@@ -12,7 +12,7 @@ import net.eagle0.eagle.common.unaffiliated_hero_type.UnaffiliatedHeroType.{
|
||||
import net.eagle0.eagle.common.unaffiliated_hero_type.UnaffiliatedHeroType as UnaffiliatedHeroTypeProto
|
||||
import net.eagle0.eagle.model.proto_converters.hero.ProfessionConverter
|
||||
import net.eagle0.eagle.model.proto_converters.UnaffiliatedHeroConverter
|
||||
import net.eagle0.eagle.model.state.unaffiliated_hero.UnaffiliatedHeroType
|
||||
import net.eagle0.eagle.model.state.unaffiliated_hero.{RecruitmentInfo, UnaffiliatedHeroType}
|
||||
import net.eagle0.eagle.model.view.province.UnaffiliatedHeroBasics
|
||||
import net.eagle0.eagle.views.province_view.UnaffiliatedHeroBasics as UnaffiliatedHeroBasicsProto
|
||||
|
||||
@@ -50,6 +50,6 @@ object UnaffiliatedHeroBasicsConverter {
|
||||
isFactionLeader = proto.isFactionLeader,
|
||||
recruitmentInfo = proto.recruitmentInfo
|
||||
.map(ri => UnaffiliatedHeroConverter.recruitmentInfoFromProto(ri))
|
||||
.getOrElse(throw new IllegalArgumentException("RecruitmentInfo is required"))
|
||||
.getOrElse(RecruitmentInfo.Unknown)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/province:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/validations:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/view_filters:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:__subpackages__",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/shardok_battle:__pkg__",
|
||||
@@ -41,6 +42,7 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/battalion:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/library:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/util:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/model/proto_converters/view:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
],
|
||||
@@ -121,6 +123,7 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/model/view:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/library:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/util:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/model/proto_converters/view:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
|
||||
@@ -84,6 +84,7 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/util/hero_generation:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/library:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/actions:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/model/proto_converters/view:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/service/new_game_creation:__pkg__",
|
||||
],
|
||||
deps = [],
|
||||
|
||||
@@ -63,6 +63,7 @@ scala_library(
|
||||
":event",
|
||||
":incoming_end_turn_action",
|
||||
":orders",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:army",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type_id",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package net.eagle0.eagle.model.state.province
|
||||
|
||||
import net.eagle0.eagle.{BattalionId, FactionId, HeroId, ProvinceId}
|
||||
import net.eagle0.eagle.api.selected_command.SelectedCommand
|
||||
import net.eagle0.eagle.model.state.{
|
||||
Army,
|
||||
BattalionTypeId,
|
||||
@@ -86,6 +87,8 @@ trait ProvinceT {
|
||||
def castleCount: Int
|
||||
def heroCap: Int
|
||||
|
||||
def lastCommand: Option[SelectedCommand]
|
||||
|
||||
def updateWith(
|
||||
name: String = name,
|
||||
neighbors: Vector[Neighbor] = neighbors,
|
||||
@@ -122,7 +125,8 @@ trait ProvinceT {
|
||||
battleRevelations: Vector[BattleRevelation] = battleRevelations,
|
||||
hexMapName: String = hexMapName,
|
||||
castleCount: Int = castleCount,
|
||||
heroCap: Int = heroCap
|
||||
heroCap: Int = heroCap,
|
||||
lastCommand: Option[SelectedCommand] = lastCommand
|
||||
): ProvinceT
|
||||
|
||||
def withRulingFactionId(rulingFactionId: FactionId): ProvinceT =
|
||||
|
||||
@@ -18,6 +18,7 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province:orders",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:army",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:battle_revelation",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package net.eagle0.eagle.model.state.province.concrete
|
||||
|
||||
import net.eagle0.eagle.{BattalionId, FactionId, HeroId, ProvinceId}
|
||||
import net.eagle0.eagle.api.selected_command.SelectedCommand
|
||||
import net.eagle0.eagle.model.state.{
|
||||
Army,
|
||||
BattalionTypeId,
|
||||
@@ -61,10 +62,7 @@ case class ProvinceC(
|
||||
hexMapName: String = "",
|
||||
castleCount: Int = 0,
|
||||
heroCap: Int = 0,
|
||||
// This is super gross, but I don't want to import this proto here when only one use case needs it,
|
||||
// so let's just match for it when we need it until I can replace the command proto with something
|
||||
// more lightweight.
|
||||
lastCommand: Option[Any] = None
|
||||
lastCommand: Option[SelectedCommand] = None
|
||||
) extends ProvinceT {
|
||||
override def updateWith(
|
||||
name: String = name,
|
||||
@@ -102,7 +100,8 @@ case class ProvinceC(
|
||||
battleRevelations: Vector[BattleRevelation] = battleRevelations,
|
||||
hexMapName: String = hexMapName,
|
||||
castleCount: Int = castleCount,
|
||||
heroCap: Int = heroCap
|
||||
heroCap: Int = heroCap,
|
||||
lastCommand: Option[SelectedCommand] = lastCommand
|
||||
): ProvinceT =
|
||||
ProvinceC(
|
||||
id = this.id,
|
||||
|
||||
@@ -8,6 +8,7 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/actions:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/util:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/model/proto_converters/view:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
|
||||
@@ -10,6 +10,11 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/view:__subpackages__",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero:__pkg__",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/util/view_filters:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/model/proto_converters/view:__subpackages__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type_id",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
|
||||
@@ -15,7 +15,7 @@ case class IncomingArmyView(
|
||||
originProvinceId: ProvinceId,
|
||||
destinationProvinceId: ProvinceId,
|
||||
arrivalRoundId: RoundId,
|
||||
food: Int,
|
||||
gold: Int,
|
||||
food: Int = 0,
|
||||
gold: Int = 0,
|
||||
unitDetails: Vector[UnitDetails] = Vector.empty
|
||||
)
|
||||
|
||||
@@ -4,6 +4,10 @@ scala_library(
|
||||
name = "unaffiliated_hero_basics",
|
||||
srcs = ["UnaffiliatedHeroBasics.scala"],
|
||||
visibility = ["//visibility:public"],
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero:profession",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/unaffiliated_hero",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero:profession",
|
||||
|
||||
@@ -378,8 +378,14 @@ class GamesManager(
|
||||
shardokGameId: ShardokGameId
|
||||
): Boolean =
|
||||
this.synchronized {
|
||||
gameControllerInfos(eagleGameId).controller.engine.history
|
||||
.hasShardokPlayerAvailableCommands(shardokGameId)
|
||||
val controller = gameControllerInfos(eagleGameId).controller
|
||||
val humanFactionIds = controller.humanClients.map(_.factionId)
|
||||
val history = controller.engine.history
|
||||
humanFactionIds.exists { fid =>
|
||||
history
|
||||
.shardokPlayerAvailableCommands(shardokGameId, fid)
|
||||
.exists(_.currentCommand.nonEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
def gamesFor(name: String): Vector[GamePlayerInfo] = this.synchronized {
|
||||
|
||||
+117
-86
@@ -4,6 +4,7 @@ import scala.collection.mutable
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
import scala.util.{Failure, Success}
|
||||
|
||||
import io.grpc.stub.StreamObserver
|
||||
import net.eagle0.common.common_unit.*
|
||||
import net.eagle0.common.game_setup_info.{GameSetupInfo, NewGameRequest}
|
||||
import net.eagle0.common.player_info.{AlliedPlayer, PlayerSetupInfo}
|
||||
@@ -48,13 +49,113 @@ class ShardokInterfaceGrpcClient(
|
||||
resolveNewGameRequest(battle, newGameRequest)
|
||||
}
|
||||
|
||||
private def waitingForHumanPlayer(battle: ShardokBattle): Boolean =
|
||||
resolutionReceiver.hasHumanPlayerCommands(
|
||||
eagleGameId = battle.eagleGameId,
|
||||
shardokGameId = battle.shardokGameId
|
||||
private def playerInfo(
|
||||
shardokPlayer: ShardokPlayer,
|
||||
eagleGameState: GameState,
|
||||
playerToUserMap: Map[FactionId, String]
|
||||
): PlayerSetupInfo =
|
||||
PlayerSetupInfo(
|
||||
`type` = if playerToUserMap.contains(shardokPlayer.eagleFid) then HUMAN else AI,
|
||||
eagleFactionId = shardokPlayer.eagleFid,
|
||||
userName = playerToUserMap.getOrElse(shardokPlayer.eagleFid, ""),
|
||||
units = toCommon(
|
||||
shardokPlayer.getArmyGroup.armies.toVector,
|
||||
eagleGameState
|
||||
),
|
||||
defender = shardokPlayer.isDefender,
|
||||
victoryConditions = shardokPlayer.victoryConditions,
|
||||
food = shardokPlayer.food,
|
||||
allies = eagleGameState
|
||||
.factions(shardokPlayer.eagleFid)
|
||||
.factionRelationships
|
||||
.filter(_.relationshipLevel != HOSTILE)
|
||||
.map(_.targetFactionId)
|
||||
.map(fid => AlliedPlayer(eagleFactionId = fid))
|
||||
)
|
||||
|
||||
private def handleBattleResponse(update: GameStatusResponse): Unit = {
|
||||
/**
|
||||
* Subscribes to game updates using server-side streaming.
|
||||
*
|
||||
* This replaces the previous polling approach (gameStatusRunner) with a push-based model. The server pushes updates
|
||||
* as they occur, reducing latency and eliminating polling overhead.
|
||||
*/
|
||||
private def subscribeToGame(
|
||||
eagleGameId: GameId,
|
||||
shardokGameId: ShardokGameId,
|
||||
newGameRequest: Option[NewGameRequest]
|
||||
): Unit = {
|
||||
val request = GameSubscriptionRequest(
|
||||
gameId = shardokGameId,
|
||||
gameSetupInfo = Some(
|
||||
GameSetupInfo(
|
||||
knownResultCount = resolutionReceiver.knownResultCount(
|
||||
eagleGameId = eagleGameId,
|
||||
shardokGameId = shardokGameId
|
||||
),
|
||||
currentGameState = resolutionReceiver.currentGameState(
|
||||
eagleGameId = eagleGameId,
|
||||
shardokGameId = shardokGameId
|
||||
),
|
||||
newGameRequest = newGameRequest
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val responseObserver = new StreamObserver[GameStatusResponse] {
|
||||
override def onNext(response: GameStatusResponse): Unit =
|
||||
try handleStreamingResponse(response, eagleGameId, shardokGameId)
|
||||
catch {
|
||||
case e: Exception =>
|
||||
println(s"Exception processing stream response for $shardokGameId: $e")
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
override def onError(t: Throwable): Unit = {
|
||||
println(s"Stream error for $shardokGameId: $t")
|
||||
// Reconnect after a delay if the battle is still pending
|
||||
scheduleReconnect(eagleGameId, shardokGameId)
|
||||
}
|
||||
|
||||
override def onCompleted(): Unit =
|
||||
println(s"Stream completed for $shardokGameId")
|
||||
}
|
||||
|
||||
grpcClient.subscribeToGame(request, responseObserver)
|
||||
}
|
||||
|
||||
/** Schedules a reconnection attempt after stream disconnection with exponential backoff. */
|
||||
private def scheduleReconnect(
|
||||
eagleGameId: GameId,
|
||||
shardokGameId: ShardokGameId,
|
||||
delayMs: Long = 1000
|
||||
): Unit =
|
||||
Future {
|
||||
Thread.sleep(delayMs)
|
||||
pendingBattles.synchronized {
|
||||
if pendingBattles.contains(shardokGameId) then
|
||||
println(s"Reconnecting stream for $shardokGameId (delay was ${delayMs}ms)")
|
||||
try subscribeToGame(eagleGameId, shardokGameId, None)
|
||||
catch {
|
||||
case e: Exception =>
|
||||
println(s"Reconnect failed for $shardokGameId: $e")
|
||||
// Exponential backoff with max 30 seconds
|
||||
val nextDelay = Math.min(delayMs * 2, 30000)
|
||||
scheduleReconnect(eagleGameId, shardokGameId, nextDelay)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles responses from the streaming subscription.
|
||||
*
|
||||
* Unlike handleBattleResponse, this does not trigger further polling since updates are pushed automatically via the
|
||||
* stream.
|
||||
*/
|
||||
private def handleStreamingResponse(
|
||||
update: GameStatusResponse,
|
||||
eagleGameId: GameId,
|
||||
shardokGameId: ShardokGameId
|
||||
): Unit = {
|
||||
val maybeBattle = pendingBattles.synchronized {
|
||||
pendingBattles.get(update.gameId)
|
||||
}
|
||||
@@ -102,85 +203,15 @@ class ShardokInterfaceGrpcClient(
|
||||
currentGameStateBytes = gameUpdateResponse.currentGameState.toByteArray
|
||||
)
|
||||
)
|
||||
// Only do the gameStatusRunner if there are no available commands. If there are available commands, just wait
|
||||
// for the next client post.
|
||||
if !waitingForHumanPlayer(battle) then
|
||||
gameStatusRunner(
|
||||
eagleGameId = battle.eagleGameId,
|
||||
shardokGameId = battle.shardokGameId,
|
||||
newGameRequest = None
|
||||
)
|
||||
// Note: No polling needed - updates are pushed via stream
|
||||
case Specific.Empty =>
|
||||
// FIXME: maybe a delay
|
||||
if !waitingForHumanPlayer(battle) then
|
||||
gameStatusRunner(
|
||||
eagleGameId = battle.eagleGameId,
|
||||
shardokGameId = battle.shardokGameId,
|
||||
newGameRequest = None
|
||||
)
|
||||
// Empty response - nothing to process
|
||||
}
|
||||
case None =>
|
||||
println(s"No pending battle found for ${update.gameId}")
|
||||
}
|
||||
}
|
||||
|
||||
private def playerInfo(
|
||||
shardokPlayer: ShardokPlayer,
|
||||
eagleGameState: GameState,
|
||||
playerToUserMap: Map[FactionId, String]
|
||||
): PlayerSetupInfo =
|
||||
PlayerSetupInfo(
|
||||
`type` = if playerToUserMap.contains(shardokPlayer.eagleFid) then HUMAN else AI,
|
||||
eagleFactionId = shardokPlayer.eagleFid,
|
||||
userName = playerToUserMap.getOrElse(shardokPlayer.eagleFid, ""),
|
||||
units = toCommon(
|
||||
shardokPlayer.getArmyGroup.armies.toVector,
|
||||
eagleGameState
|
||||
),
|
||||
defender = shardokPlayer.isDefender,
|
||||
victoryConditions = shardokPlayer.victoryConditions,
|
||||
food = shardokPlayer.food,
|
||||
allies = eagleGameState
|
||||
.factions(shardokPlayer.eagleFid)
|
||||
.factionRelationships
|
||||
.filter(_.relationshipLevel != HOSTILE)
|
||||
.map(_.targetFactionId)
|
||||
.map(fid => AlliedPlayer(eagleFactionId = fid))
|
||||
)
|
||||
|
||||
private def gameStatusRunner(
|
||||
eagleGameId: GameId,
|
||||
shardokGameId: ShardokGameId,
|
||||
newGameRequest: Option[NewGameRequest]
|
||||
): Unit =
|
||||
retrier
|
||||
.withRetries(
|
||||
_.getGameStatus(
|
||||
GameStatusRequest(
|
||||
gameId = shardokGameId,
|
||||
gameSetupInfo = Some(
|
||||
GameSetupInfo(
|
||||
knownResultCount = resolutionReceiver.knownResultCount(
|
||||
eagleGameId = eagleGameId,
|
||||
shardokGameId = shardokGameId
|
||||
),
|
||||
currentGameState = resolutionReceiver.currentGameState(
|
||||
eagleGameId = eagleGameId,
|
||||
shardokGameId = shardokGameId
|
||||
),
|
||||
newGameRequest = newGameRequest
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
.onComplete {
|
||||
case Success(gameStatus) => handleBattleResponse(gameStatus)
|
||||
case Failure(e) =>
|
||||
print(s"Caught exception $e\n")
|
||||
gameStatusRunner(eagleGameId, shardokGameId, newGameRequest)
|
||||
}
|
||||
|
||||
override def resolveNewGameRequest(
|
||||
battle: ShardokBattle,
|
||||
newGameRequest: NewGameRequest
|
||||
@@ -189,7 +220,7 @@ class ShardokInterfaceGrpcClient(
|
||||
pendingBattles(newGameRequest.gameId) = (battle, newGameRequest)
|
||||
)
|
||||
|
||||
gameStatusRunner(
|
||||
subscribeToGame(
|
||||
eagleGameId = newGameRequest.eagleGameId,
|
||||
shardokGameId = newGameRequest.gameId,
|
||||
newGameRequest = Some(newGameRequest)
|
||||
@@ -264,10 +295,10 @@ class ShardokInterfaceGrpcClient(
|
||||
)
|
||||
)
|
||||
.onComplete {
|
||||
case Success(gameStatusResponse) =>
|
||||
handleBattleResponse(gameStatusResponse)
|
||||
case Failure(e) =>
|
||||
print("failed to post a command\n")
|
||||
case Success(_) =>
|
||||
// Response acknowledged - actual updates come via stream
|
||||
case Failure(e) =>
|
||||
println(s"Failed to post command: $e")
|
||||
throw e
|
||||
}
|
||||
else print("Invalid token\n")
|
||||
@@ -311,10 +342,10 @@ class ShardokInterfaceGrpcClient(
|
||||
)
|
||||
)
|
||||
.onComplete {
|
||||
case Success(gameStatusResponse) =>
|
||||
handleBattleResponse(gameStatusResponse)
|
||||
case Failure(e) =>
|
||||
print("failed to post a placement command")
|
||||
case Success(_) =>
|
||||
// Response acknowledged - actual updates come via stream
|
||||
case Failure(e) =>
|
||||
println(s"Failed to post placement command: $e")
|
||||
throw e
|
||||
}
|
||||
else print("Invalid token\n")
|
||||
|
||||
@@ -8,7 +8,7 @@ import net.eagle0.eagle.common.recruitment_info.RecruitmentInfo
|
||||
import net.eagle0.eagle.common.recruitment_info.RecruitmentStatus.*
|
||||
import net.eagle0.eagle.common.round_phase.RoundPhase
|
||||
import net.eagle0.eagle.common.round_phase.RoundPhase.PROVINCE_EVENTS
|
||||
import net.eagle0.eagle.common.unaffiliated_hero_quest.Quest
|
||||
import net.eagle0.eagle.common.unaffiliated_hero_quest.{Quest, WealthQuest}
|
||||
import net.eagle0.eagle.common.unaffiliated_hero_type.UnaffiliatedHeroType.{
|
||||
UNAFFILIATED_HERO_OUTLAW,
|
||||
UNAFFILIATED_HERO_PRISONER,
|
||||
@@ -113,7 +113,13 @@ class GameStateViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with M
|
||||
recruitmentInfo = Some(
|
||||
RecruitmentInfo(
|
||||
status = RECRUITMENT_STATUS_HAS_QUEST,
|
||||
quest = Some(Quest())
|
||||
quest = Some(
|
||||
Quest(
|
||||
componentCount = 1,
|
||||
componentsFulfilled = 0,
|
||||
details = WealthQuest(gold = 100, food = 50)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
@@ -293,7 +299,8 @@ class GameStateViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with M
|
||||
recruitmentInfo = Some(
|
||||
RecruitmentInfo(
|
||||
status = RECRUITMENT_STATUS_HAS_QUEST,
|
||||
quest = Some(Quest())
|
||||
// Note: componentCount is 0 because Scala WealthQuest doesn't preserve it
|
||||
quest = Some(Quest(details = WealthQuest(gold = 100, food = 50)))
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
@@ -34,13 +34,21 @@ scala_test(
|
||||
name = "province_view_filter_test",
|
||||
srcs = ["ProvinceViewFilterTest.scala"],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:combat_unit_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:faction_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/views:province_view_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:base_resource_limit",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:per_development_resource_limit",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:idable",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/view_filters:province_view_filter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type_id",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/battalion",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/incoming_army:incoming_army_view",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/province:full_province_info",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/province:province_view",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/province:unaffiliated_hero_basics",
|
||||
"//src/test/scala/net/eagle0/common:proto_matchers",
|
||||
],
|
||||
)
|
||||
|
||||
+169
-151
@@ -1,7 +1,6 @@
|
||||
package net.eagle0.eagle.library.util.view_filters
|
||||
|
||||
import net.eagle0.common.ProtoMatchers.equalProto
|
||||
import net.eagle0.eagle.common.battalion_type.BattalionTypeId
|
||||
import net.eagle0.eagle.common.battalion_type.BattalionTypeId as BattalionTypeIdProto
|
||||
import net.eagle0.eagle.common.combat_unit.CombatUnit
|
||||
import net.eagle0.eagle.common.date.Date
|
||||
import net.eagle0.eagle.internal.army.{Army, MovingArmy}
|
||||
@@ -16,9 +15,17 @@ import net.eagle0.eagle.internal.supplies.{MovingSupplies, Supplies}
|
||||
import net.eagle0.eagle.internal.unaffiliated_hero.UnaffiliatedHero
|
||||
import net.eagle0.eagle.library.settings.{BaseResourceLimit, PerDevelopmentResourceLimit}
|
||||
import net.eagle0.eagle.library.util.IDable.{mapifyFactions, mapifyHeroes, mapifyProvinces}
|
||||
import net.eagle0.eagle.views.battalion_view.BattalionView
|
||||
import net.eagle0.eagle.views.incoming_army_view.IncomingArmyView
|
||||
import net.eagle0.eagle.views.province_view.{FullProvinceInfo, ProvinceView, UnaffiliatedHeroBasics}
|
||||
import net.eagle0.eagle.model.state.hero.Profession
|
||||
import net.eagle0.eagle.model.state.unaffiliated_hero.{RecruitmentInfo, UnaffiliatedHeroType}
|
||||
import net.eagle0.eagle.model.state.BattalionTypeId
|
||||
import net.eagle0.eagle.model.view.battalion.BattalionView
|
||||
import net.eagle0.eagle.model.view.incoming_army.{IncomingArmyView, UnitDetails}
|
||||
import net.eagle0.eagle.model.view.province.{FullProvinceInfo, ProvinceView, UnaffiliatedHeroBasics}
|
||||
import net.eagle0.eagle.views.province_view.{
|
||||
FullProvinceInfo as FullProvinceInfoProto,
|
||||
ProvinceView as ProvinceViewProto,
|
||||
UnaffiliatedHeroBasics as UnaffiliatedHeroBasicsProto
|
||||
}
|
||||
import net.eagle0.eagle.HeroId
|
||||
import org.scalatest.flatspec.AnyFlatSpec
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
@@ -36,14 +43,14 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
|
||||
19 -> Battalion(
|
||||
id = 19,
|
||||
size = 253,
|
||||
`type` = BattalionTypeId.HEAVY_CAVALRY,
|
||||
`type` = BattalionTypeIdProto.HEAVY_CAVALRY,
|
||||
training = 51.0,
|
||||
armament = 75.0
|
||||
),
|
||||
21 -> Battalion(
|
||||
id = 21,
|
||||
size = 101,
|
||||
`type` = BattalionTypeId.LONGBOWMEN,
|
||||
`type` = BattalionTypeIdProto.LONGBOWMEN,
|
||||
training = 12.0,
|
||||
armament = 15.0
|
||||
)
|
||||
@@ -63,7 +70,7 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
|
||||
factionId = 3
|
||||
)
|
||||
|
||||
provinceView.getFullInfo.economyDevastation shouldBe 0
|
||||
provinceView.fullInfo.get.economyDevastation shouldBe 0
|
||||
}
|
||||
|
||||
it should "put a devastation of 2.1 at 2.1" in {
|
||||
@@ -80,7 +87,7 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
|
||||
factionId = 3
|
||||
)
|
||||
|
||||
provinceView.getFullInfo.economyDevastation shouldBe 2.1f
|
||||
provinceView.fullInfo.get.economyDevastation shouldBe 2.1f
|
||||
}
|
||||
|
||||
it should "put an economy of 0.1 at 0.1" in {
|
||||
@@ -97,7 +104,7 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
|
||||
factionId = 3
|
||||
)
|
||||
|
||||
provinceView.getFullInfo.economy shouldBe 0.1f
|
||||
provinceView.fullInfo.get.economy shouldBe 0.1f
|
||||
}
|
||||
|
||||
it should "show ruler is traveling if they are" in {
|
||||
@@ -114,7 +121,7 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
|
||||
factionId = 3
|
||||
)
|
||||
|
||||
provinceView.getFullInfo.rulerIsTraveling shouldBe true
|
||||
provinceView.fullInfo.get.rulerIsTraveling shouldBe true
|
||||
}
|
||||
|
||||
it should "not show ruler is traveling if they are not" in {
|
||||
@@ -131,7 +138,7 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
|
||||
factionId = 3
|
||||
)
|
||||
|
||||
provinceView.getFullInfo.rulerIsTraveling shouldBe false
|
||||
provinceView.fullInfo.get.rulerIsTraveling shouldBe false
|
||||
}
|
||||
|
||||
it should "show your own incoming armies into your own province" in {
|
||||
@@ -168,38 +175,37 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
|
||||
ProvinceViewFilter.filteredProvinceView(province, gameState, 3)
|
||||
|
||||
filtered.knownIncomingArmies should have size 1
|
||||
filtered.knownIncomingArmies.head should equalProto(
|
||||
IncomingArmyView(
|
||||
factionId = 3,
|
||||
heroCount = 2,
|
||||
troopCount = 354,
|
||||
originProvinceId = 1,
|
||||
destinationProvinceId = 5,
|
||||
arrivalRoundId = 4,
|
||||
unitDetails = Vector(
|
||||
IncomingArmyView
|
||||
.UnitDetails(
|
||||
heroId = 5,
|
||||
battalion = Some(
|
||||
BattalionView(
|
||||
id = 19,
|
||||
size = 253,
|
||||
`type` = BattalionTypeId.HEAVY_CAVALRY,
|
||||
training = Some(51),
|
||||
armament = Some(75)
|
||||
)
|
||||
)
|
||||
),
|
||||
IncomingArmyView.UnitDetails(
|
||||
heroId = 9,
|
||||
battalion = Some(
|
||||
BattalionView(
|
||||
id = 21,
|
||||
size = 101,
|
||||
`type` = BattalionTypeId.LONGBOWMEN,
|
||||
training = Some(12),
|
||||
armament = Some(15)
|
||||
)
|
||||
filtered.knownIncomingArmies.head shouldBe IncomingArmyView(
|
||||
factionId = 3,
|
||||
heroCount = 2,
|
||||
troopCount = 354,
|
||||
originProvinceId = 1,
|
||||
destinationProvinceId = 5,
|
||||
arrivalRoundId = 4,
|
||||
unitDetails = Vector(
|
||||
UnitDetails(
|
||||
heroId = 5,
|
||||
battalion = Some(
|
||||
BattalionView(
|
||||
id = 19,
|
||||
typeId = BattalionTypeId.HeavyCavalry,
|
||||
size = 253,
|
||||
training = Some(51),
|
||||
armament = Some(75),
|
||||
name = ""
|
||||
)
|
||||
)
|
||||
),
|
||||
UnitDetails(
|
||||
heroId = 9,
|
||||
battalion = Some(
|
||||
BattalionView(
|
||||
id = 21,
|
||||
typeId = BattalionTypeId.Longbowmen,
|
||||
size = 101,
|
||||
training = Some(12),
|
||||
armament = Some(15),
|
||||
name = ""
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -277,15 +283,13 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
|
||||
ProvinceViewFilter.filteredProvinceView(province, gameState, 3)
|
||||
|
||||
filtered.knownIncomingArmies should have size 1
|
||||
filtered.knownIncomingArmies.head should equalProto(
|
||||
IncomingArmyView(
|
||||
factionId = 8,
|
||||
heroCount = 2,
|
||||
troopCount = 354,
|
||||
originProvinceId = 1,
|
||||
destinationProvinceId = 5,
|
||||
arrivalRoundId = 4
|
||||
)
|
||||
filtered.knownIncomingArmies.head shouldBe IncomingArmyView(
|
||||
factionId = 8,
|
||||
heroCount = 2,
|
||||
troopCount = 354,
|
||||
originProvinceId = 1,
|
||||
destinationProvinceId = 5,
|
||||
arrivalRoundId = 4
|
||||
)
|
||||
}
|
||||
|
||||
@@ -323,38 +327,37 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
|
||||
ProvinceViewFilter.filteredProvinceView(province, gameState, 3)
|
||||
|
||||
filtered.knownIncomingArmies should have size 1
|
||||
filtered.knownIncomingArmies.head should equalProto(
|
||||
IncomingArmyView(
|
||||
factionId = 3,
|
||||
heroCount = 2,
|
||||
troopCount = 354,
|
||||
originProvinceId = 1,
|
||||
destinationProvinceId = 5,
|
||||
arrivalRoundId = 4,
|
||||
unitDetails = Vector(
|
||||
IncomingArmyView
|
||||
.UnitDetails(
|
||||
heroId = 5,
|
||||
battalion = Some(
|
||||
BattalionView(
|
||||
id = 19,
|
||||
size = 253,
|
||||
`type` = BattalionTypeId.HEAVY_CAVALRY,
|
||||
training = Some(51),
|
||||
armament = Some(75)
|
||||
)
|
||||
)
|
||||
),
|
||||
IncomingArmyView.UnitDetails(
|
||||
heroId = 9,
|
||||
battalion = Some(
|
||||
BattalionView(
|
||||
id = 21,
|
||||
size = 101,
|
||||
`type` = BattalionTypeId.LONGBOWMEN,
|
||||
training = Some(12),
|
||||
armament = Some(15)
|
||||
)
|
||||
filtered.knownIncomingArmies.head shouldBe IncomingArmyView(
|
||||
factionId = 3,
|
||||
heroCount = 2,
|
||||
troopCount = 354,
|
||||
originProvinceId = 1,
|
||||
destinationProvinceId = 5,
|
||||
arrivalRoundId = 4,
|
||||
unitDetails = Vector(
|
||||
UnitDetails(
|
||||
heroId = 5,
|
||||
battalion = Some(
|
||||
BattalionView(
|
||||
id = 19,
|
||||
typeId = BattalionTypeId.HeavyCavalry,
|
||||
size = 253,
|
||||
training = Some(51),
|
||||
armament = Some(75),
|
||||
name = ""
|
||||
)
|
||||
)
|
||||
),
|
||||
UnitDetails(
|
||||
heroId = 9,
|
||||
battalion = Some(
|
||||
BattalionView(
|
||||
id = 21,
|
||||
typeId = BattalionTypeId.Longbowmen,
|
||||
size = 101,
|
||||
training = Some(12),
|
||||
armament = Some(15),
|
||||
name = ""
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -432,38 +435,37 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
|
||||
ProvinceViewFilter.filteredProvinceView(province, gameState, 3)
|
||||
|
||||
filtered.knownIncomingArmies should have size 1
|
||||
filtered.knownIncomingArmies.head should equalProto(
|
||||
IncomingArmyView(
|
||||
factionId = 3,
|
||||
heroCount = 2,
|
||||
troopCount = 354,
|
||||
originProvinceId = 1,
|
||||
destinationProvinceId = 5,
|
||||
arrivalRoundId = 4,
|
||||
unitDetails = Vector(
|
||||
IncomingArmyView
|
||||
.UnitDetails(
|
||||
heroId = 5,
|
||||
battalion = Some(
|
||||
BattalionView(
|
||||
id = 19,
|
||||
size = 253,
|
||||
`type` = BattalionTypeId.HEAVY_CAVALRY,
|
||||
training = Some(51),
|
||||
armament = Some(75)
|
||||
)
|
||||
)
|
||||
),
|
||||
IncomingArmyView.UnitDetails(
|
||||
heroId = 9,
|
||||
battalion = Some(
|
||||
BattalionView(
|
||||
id = 21,
|
||||
size = 101,
|
||||
`type` = BattalionTypeId.LONGBOWMEN,
|
||||
training = Some(12),
|
||||
armament = Some(15)
|
||||
)
|
||||
filtered.knownIncomingArmies.head shouldBe IncomingArmyView(
|
||||
factionId = 3,
|
||||
heroCount = 2,
|
||||
troopCount = 354,
|
||||
originProvinceId = 1,
|
||||
destinationProvinceId = 5,
|
||||
arrivalRoundId = 4,
|
||||
unitDetails = Vector(
|
||||
UnitDetails(
|
||||
heroId = 5,
|
||||
battalion = Some(
|
||||
BattalionView(
|
||||
id = 19,
|
||||
typeId = BattalionTypeId.HeavyCavalry,
|
||||
size = 253,
|
||||
training = Some(51),
|
||||
armament = Some(75),
|
||||
name = ""
|
||||
)
|
||||
)
|
||||
),
|
||||
UnitDetails(
|
||||
heroId = 9,
|
||||
battalion = Some(
|
||||
BattalionView(
|
||||
id = 21,
|
||||
typeId = BattalionTypeId.Longbowmen,
|
||||
size = 101,
|
||||
training = Some(12),
|
||||
armament = Some(15),
|
||||
name = ""
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -531,16 +533,24 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
|
||||
val filtered =
|
||||
ProvinceViewFilter.filteredProvinceView(province, gameState, 11)
|
||||
|
||||
inside(filtered.getFullInfo.unaffiliatedHeroes) {
|
||||
inside(filtered.fullInfo.get.unaffiliatedHeroes) {
|
||||
case uhs: Vector[UnaffiliatedHeroBasics] =>
|
||||
uhs should contain theSameElementsAs Vector(
|
||||
UnaffiliatedHeroBasics(
|
||||
heroId = 1,
|
||||
nameTextId = "hn_1"
|
||||
nameTextId = "hn_1",
|
||||
profession = Profession.NoProfession,
|
||||
status = UnaffiliatedHeroType.Unknown,
|
||||
isFactionLeader = false,
|
||||
recruitmentInfo = RecruitmentInfo.Unknown
|
||||
),
|
||||
UnaffiliatedHeroBasics(
|
||||
heroId = 2,
|
||||
nameTextId = "hn_2"
|
||||
nameTextId = "hn_2",
|
||||
profession = Profession.NoProfession,
|
||||
status = UnaffiliatedHeroType.Unknown,
|
||||
isFactionLeader = false,
|
||||
recruitmentInfo = RecruitmentInfo.Unknown
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -625,10 +635,10 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
|
||||
val askingFaction = Faction(
|
||||
id = 4,
|
||||
reconnedProvinces = Vector(
|
||||
ProvinceView(
|
||||
ProvinceViewProto(
|
||||
id = 5,
|
||||
fullInfo = Some(
|
||||
FullProvinceInfo(
|
||||
FullProvinceInfoProto(
|
||||
rulingFactionHeroIds = Vector(2, 3)
|
||||
)
|
||||
)
|
||||
@@ -647,7 +657,7 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
|
||||
val filtered =
|
||||
ProvinceViewFilter.filteredProvinceView(province, gameState, 4)
|
||||
|
||||
inside(filtered.getFullInfo.rulingFactionHeroIds) {
|
||||
inside(filtered.fullInfo.get.rulingFactionHeroIds) {
|
||||
case ids: Vector[HeroId] =>
|
||||
ids should contain theSameElementsAs Vector(2, 3)
|
||||
}
|
||||
@@ -683,11 +693,11 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
|
||||
)
|
||||
),
|
||||
reconnedProvinces = Vector(
|
||||
ProvinceView(
|
||||
ProvinceViewProto(
|
||||
id = 5,
|
||||
asOf = Some(Date(year = 2024, month = 1)),
|
||||
fullInfo = Some(
|
||||
FullProvinceInfo(
|
||||
FullProvinceInfoProto(
|
||||
rulingFactionHeroIds = Vector(2, 3)
|
||||
)
|
||||
)
|
||||
@@ -706,7 +716,7 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
|
||||
val filtered =
|
||||
ProvinceViewFilter.filteredProvinceView(province, gameState, 4)
|
||||
|
||||
inside(filtered.getFullInfo.rulingFactionHeroIds) {
|
||||
inside(filtered.fullInfo.get.rulingFactionHeroIds) {
|
||||
case ids: Vector[HeroId] =>
|
||||
ids should contain theSameElementsAs Vector(2, 3)
|
||||
}
|
||||
@@ -730,11 +740,11 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
|
||||
)
|
||||
),
|
||||
reconnedProvinces = Vector(
|
||||
ProvinceView(
|
||||
ProvinceViewProto(
|
||||
id = 5,
|
||||
asOf = Some(Date(year = 2024, month = 4)),
|
||||
fullInfo = Some(
|
||||
FullProvinceInfo(
|
||||
FullProvinceInfoProto(
|
||||
rulingFactionHeroIds = Vector(2, 3, 4)
|
||||
)
|
||||
)
|
||||
@@ -753,11 +763,11 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
|
||||
)
|
||||
),
|
||||
reconnedProvinces = Vector(
|
||||
ProvinceView(
|
||||
ProvinceViewProto(
|
||||
id = 5,
|
||||
asOf = Some(Date(year = 2024, month = 1)),
|
||||
fullInfo = Some(
|
||||
FullProvinceInfo(
|
||||
FullProvinceInfoProto(
|
||||
rulingFactionHeroIds = Vector(2, 3)
|
||||
)
|
||||
)
|
||||
@@ -776,7 +786,7 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
|
||||
val filtered =
|
||||
ProvinceViewFilter.filteredProvinceView(province, gameState, 4)
|
||||
|
||||
inside(filtered.getFullInfo.rulingFactionHeroIds) {
|
||||
inside(filtered.fullInfo.get.rulingFactionHeroIds) {
|
||||
case ids: Vector[HeroId] =>
|
||||
ids should contain theSameElementsAs Vector(2, 3, 4)
|
||||
}
|
||||
@@ -800,11 +810,11 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
|
||||
)
|
||||
),
|
||||
reconnedProvinces = Vector(
|
||||
ProvinceView(
|
||||
ProvinceViewProto(
|
||||
id = 5,
|
||||
asOf = Some(Date(year = 2024, month = 4)),
|
||||
fullInfo = Some(
|
||||
FullProvinceInfo(
|
||||
FullProvinceInfoProto(
|
||||
rulingFactionHeroIds = Vector(2, 3, 4)
|
||||
)
|
||||
)
|
||||
@@ -823,11 +833,11 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
|
||||
)
|
||||
),
|
||||
reconnedProvinces = Vector(
|
||||
ProvinceView(
|
||||
ProvinceViewProto(
|
||||
id = 5,
|
||||
asOf = Some(Date(year = 2024, month = 6)),
|
||||
fullInfo = Some(
|
||||
FullProvinceInfo(
|
||||
FullProvinceInfoProto(
|
||||
rulingFactionHeroIds = Vector(2, 3)
|
||||
)
|
||||
)
|
||||
@@ -846,7 +856,7 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
|
||||
val filtered =
|
||||
ProvinceViewFilter.filteredProvinceView(province, gameState, 4)
|
||||
|
||||
inside(filtered.getFullInfo.rulingFactionHeroIds) {
|
||||
inside(filtered.fullInfo.get.rulingFactionHeroIds) {
|
||||
case ids: Vector[HeroId] =>
|
||||
ids should contain theSameElementsAs Vector(2, 3)
|
||||
}
|
||||
@@ -862,10 +872,10 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
|
||||
val askingFaction = Faction(
|
||||
id = 4,
|
||||
reconnedProvinces = Vector(
|
||||
ProvinceView(
|
||||
ProvinceViewProto(
|
||||
id = 5,
|
||||
fullInfo = Some(
|
||||
FullProvinceInfo(
|
||||
FullProvinceInfoProto(
|
||||
rulingFactionHeroIds = Vector(2, 3)
|
||||
)
|
||||
)
|
||||
@@ -885,7 +895,7 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
|
||||
val filtered =
|
||||
ProvinceViewFilter.filteredProvinceView(province, gameState, 4)
|
||||
|
||||
inside(filtered.getFullInfo.rulingFactionHeroIds) {
|
||||
inside(filtered.fullInfo.get.rulingFactionHeroIds) {
|
||||
case ids: Vector[HeroId] =>
|
||||
ids should contain theSameElementsAs Vector(3)
|
||||
}
|
||||
@@ -906,16 +916,16 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
|
||||
val askingFaction = Faction(
|
||||
id = 4,
|
||||
reconnedProvinces = Vector(
|
||||
ProvinceView(
|
||||
ProvinceViewProto(
|
||||
id = 5,
|
||||
fullInfo = Some(
|
||||
FullProvinceInfo(
|
||||
FullProvinceInfoProto(
|
||||
unaffiliatedHeroes = Vector(
|
||||
UnaffiliatedHeroBasics(
|
||||
UnaffiliatedHeroBasicsProto(
|
||||
heroId = 4,
|
||||
nameTextId = "hn_4"
|
||||
),
|
||||
UnaffiliatedHeroBasics(
|
||||
UnaffiliatedHeroBasicsProto(
|
||||
heroId = 6,
|
||||
nameTextId = "hn_6"
|
||||
)
|
||||
@@ -941,16 +951,24 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
|
||||
val filtered =
|
||||
ProvinceViewFilter.filteredProvinceView(province, gameState, 4)
|
||||
|
||||
inside(filtered.getFullInfo.unaffiliatedHeroes) {
|
||||
inside(filtered.fullInfo.get.unaffiliatedHeroes) {
|
||||
case uhs: Vector[UnaffiliatedHeroBasics] =>
|
||||
uhs should contain theSameElementsAs Vector(
|
||||
UnaffiliatedHeroBasics(
|
||||
heroId = 4,
|
||||
nameTextId = "hn_4"
|
||||
nameTextId = "hn_4",
|
||||
profession = Profession.NoProfession,
|
||||
status = UnaffiliatedHeroType.Unknown,
|
||||
isFactionLeader = false,
|
||||
recruitmentInfo = RecruitmentInfo.Unknown
|
||||
),
|
||||
UnaffiliatedHeroBasics(
|
||||
heroId = 6,
|
||||
nameTextId = "hn_6"
|
||||
nameTextId = "hn_6",
|
||||
profession = Profession.NoProfession,
|
||||
status = UnaffiliatedHeroType.Unknown,
|
||||
isFactionLeader = false,
|
||||
recruitmentInfo = RecruitmentInfo.Unknown
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -971,13 +989,13 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
|
||||
val askingFaction = Faction(
|
||||
id = 4,
|
||||
reconnedProvinces = Vector(
|
||||
ProvinceView(
|
||||
ProvinceViewProto(
|
||||
id = 5,
|
||||
fullInfo = Some(
|
||||
FullProvinceInfo(
|
||||
FullProvinceInfoProto(
|
||||
unaffiliatedHeroes = Vector(
|
||||
UnaffiliatedHeroBasics(heroId = 4),
|
||||
UnaffiliatedHeroBasics(heroId = 6)
|
||||
UnaffiliatedHeroBasicsProto(heroId = 4),
|
||||
UnaffiliatedHeroBasicsProto(heroId = 6)
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -997,7 +1015,7 @@ class ProvinceViewFilterTest extends AnyFlatSpec with BeforeAndAfterEach with Ma
|
||||
val filtered =
|
||||
ProvinceViewFilter.filteredProvinceView(province, gameState, 4)
|
||||
|
||||
inside(filtered.getFullInfo.unaffiliatedHeroes) {
|
||||
inside(filtered.fullInfo.get.unaffiliatedHeroes) {
|
||||
case uhs: Vector[UnaffiliatedHeroBasics] =>
|
||||
uhs.map(_.heroId) should contain theSameElementsAs Vector(6)
|
||||
}
|
||||
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
package net.eagle0.eagle.model.proto_converters.view.army
|
||||
|
||||
import net.eagle0.eagle.common.combat_unit.CombatUnit as CombatUnitProto
|
||||
import net.eagle0.eagle.model.state.CombatUnit
|
||||
import net.eagle0.eagle.model.view.army.ArmyView
|
||||
import net.eagle0.eagle.views.army_view.ArmyView as ArmyViewProto
|
||||
import org.scalatest.flatspec.AnyFlatSpec
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
class ArmyViewConverterTest extends AnyFlatSpec with Matchers {
|
||||
|
||||
"toProto" should "convert all fields correctly" in {
|
||||
val scala = ArmyView(
|
||||
factionId = 5,
|
||||
units = Vector(
|
||||
CombatUnit(factionId = 5, heroId = 10, battalionId = Some(20)),
|
||||
CombatUnit(factionId = 5, heroId = 11, battalionId = None)
|
||||
),
|
||||
unitCount = 2,
|
||||
troopCount = 150
|
||||
)
|
||||
|
||||
val proto = ArmyViewConverter.toProto(scala)
|
||||
|
||||
// Match on full case class to catch new fields
|
||||
proto match {
|
||||
case ArmyViewProto(
|
||||
factionId,
|
||||
units,
|
||||
unitCount,
|
||||
troopCount,
|
||||
_ /* unknownFields */
|
||||
) =>
|
||||
factionId shouldBe 5
|
||||
units.size shouldBe 2
|
||||
unitCount shouldBe 2
|
||||
troopCount shouldBe 150
|
||||
|
||||
// Verify unit conversion
|
||||
units.head match {
|
||||
case CombatUnitProto(cuFactionId, cuHeroId, cuBattalionId, _) =>
|
||||
cuFactionId shouldBe 5
|
||||
cuHeroId shouldBe 10
|
||||
cuBattalionId shouldBe Some(20)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"fromProto" should "convert all fields correctly" in {
|
||||
val proto = ArmyViewProto(
|
||||
factionId = 7,
|
||||
units = Seq(
|
||||
CombatUnitProto(factionId = 7, heroId = 15, battalionId = Some(25))
|
||||
),
|
||||
unitCount = 1,
|
||||
troopCount = 75
|
||||
)
|
||||
|
||||
val scala = ArmyViewConverter.fromProto(proto)
|
||||
|
||||
// Match on full case class to catch new fields
|
||||
scala match {
|
||||
case ArmyView(
|
||||
factionId,
|
||||
units,
|
||||
unitCount,
|
||||
troopCount
|
||||
) =>
|
||||
factionId shouldBe 7
|
||||
units.size shouldBe 1
|
||||
unitCount shouldBe 1
|
||||
troopCount shouldBe 75
|
||||
|
||||
// Verify unit conversion
|
||||
units.head match {
|
||||
case CombatUnit(cuFactionId, cuHeroId, cuBattalionId) =>
|
||||
cuFactionId shouldBe 7
|
||||
cuHeroId shouldBe 15
|
||||
cuBattalionId shouldBe Some(25)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"round trip" should "preserve all values" in {
|
||||
val original = ArmyView(
|
||||
factionId = 3,
|
||||
units = Vector(
|
||||
CombatUnit(factionId = 3, heroId = 100, battalionId = Some(200)),
|
||||
CombatUnit(factionId = 3, heroId = 101, battalionId = None)
|
||||
),
|
||||
unitCount = 2,
|
||||
troopCount = 500
|
||||
)
|
||||
|
||||
val roundTripped = ArmyViewConverter.fromProto(
|
||||
ArmyViewConverter.toProto(original)
|
||||
)
|
||||
|
||||
roundTripped shouldBe original
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
load("@rules_scala//scala:scala.bzl", "scala_test")
|
||||
|
||||
scala_test(
|
||||
name = "army_view_converter_test",
|
||||
srcs = ["ArmyViewConverterTest.scala"],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:combat_unit_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/views:army_view_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/view/army",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:combat_unit",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/army:army_view",
|
||||
"@rules_scala//scala/scalatest",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,17 @@
|
||||
load("@rules_scala//scala:scala.bzl", "scala_test")
|
||||
|
||||
scala_test(
|
||||
name = "incoming_army_view_converter_test",
|
||||
srcs = ["IncomingArmyViewConverterTest.scala"],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:battalion_type_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/views:battalion_view_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/views:incoming_army_view_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/view/incoming_army",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type_id",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/battalion",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/incoming_army:incoming_army_view",
|
||||
"@rules_scala//scala/scalatest",
|
||||
],
|
||||
)
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
package net.eagle0.eagle.model.proto_converters.view.incoming_army
|
||||
|
||||
import net.eagle0.eagle.{BattalionId, FactionId, HeroId, ProvinceId, RoundId}
|
||||
import net.eagle0.eagle.common.battalion_type.BattalionTypeId as BattalionTypeIdProto
|
||||
import net.eagle0.eagle.model.state.BattalionTypeId
|
||||
import net.eagle0.eagle.model.view.battalion.BattalionView
|
||||
import net.eagle0.eagle.model.view.incoming_army.{IncomingArmyView, UnitDetails}
|
||||
import net.eagle0.eagle.views.battalion_view.BattalionView as BattalionViewProto
|
||||
import net.eagle0.eagle.views.incoming_army_view.IncomingArmyView as IncomingArmyViewProto
|
||||
import org.scalatest.flatspec.AnyFlatSpec
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
class IncomingArmyViewConverterTest extends AnyFlatSpec with Matchers {
|
||||
|
||||
"toProto" should "convert all fields correctly" in {
|
||||
val scala = IncomingArmyView(
|
||||
factionId = 1: FactionId,
|
||||
heroCount = 3,
|
||||
troopCount = 500,
|
||||
originProvinceId = 10: ProvinceId,
|
||||
destinationProvinceId = 20: ProvinceId,
|
||||
arrivalRoundId = 15: RoundId,
|
||||
food = 100,
|
||||
gold = 200,
|
||||
unitDetails = Vector(
|
||||
UnitDetails(
|
||||
heroId = 50: HeroId,
|
||||
battalion = Some(
|
||||
BattalionView(
|
||||
id = 100: BattalionId,
|
||||
typeId = BattalionTypeId.LightInfantry,
|
||||
size = 50,
|
||||
training = Some(0.8),
|
||||
armament = Some(0.9),
|
||||
name = "Test Battalion"
|
||||
)
|
||||
)
|
||||
),
|
||||
UnitDetails(heroId = 51: HeroId, battalion = None)
|
||||
)
|
||||
)
|
||||
|
||||
val proto = IncomingArmyViewConverter.toProto(scala)
|
||||
|
||||
// Match on full case class to catch new fields
|
||||
proto match {
|
||||
case IncomingArmyViewProto(
|
||||
factionId,
|
||||
heroCount,
|
||||
troopCount,
|
||||
originProvinceId,
|
||||
destinationProvinceId,
|
||||
arrivalRoundId,
|
||||
food,
|
||||
gold,
|
||||
unitDetails,
|
||||
_ /* unknownFields */
|
||||
) =>
|
||||
factionId shouldBe 1
|
||||
heroCount shouldBe 3
|
||||
troopCount shouldBe 500
|
||||
originProvinceId shouldBe 10
|
||||
destinationProvinceId shouldBe 20
|
||||
arrivalRoundId shouldBe 15
|
||||
food shouldBe 100
|
||||
gold shouldBe 200
|
||||
unitDetails.size shouldBe 2
|
||||
|
||||
// Verify unit details conversion
|
||||
unitDetails.head match {
|
||||
case IncomingArmyViewProto.UnitDetails(heroId, battalion, _) =>
|
||||
heroId shouldBe 50
|
||||
battalion shouldBe defined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"fromProto" should "convert all fields correctly" in {
|
||||
val proto = IncomingArmyViewProto(
|
||||
factionId = 2,
|
||||
heroCount = 5,
|
||||
troopCount = 750,
|
||||
originProvinceId = 30,
|
||||
destinationProvinceId = 40,
|
||||
arrivalRoundId = 25,
|
||||
food = 300,
|
||||
gold = 400,
|
||||
unitDetails = Seq(
|
||||
IncomingArmyViewProto.UnitDetails(
|
||||
heroId = 60,
|
||||
battalion = Some(
|
||||
BattalionViewProto(
|
||||
id = 200,
|
||||
`type` = BattalionTypeIdProto.HEAVY_CAVALRY,
|
||||
size = 30,
|
||||
training = Some(0.7),
|
||||
armament = Some(0.6),
|
||||
name = "Proto Battalion"
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val scala = IncomingArmyViewConverter.fromProto(proto)
|
||||
|
||||
// Match on full case class to catch new fields
|
||||
scala match {
|
||||
case IncomingArmyView(
|
||||
factionId,
|
||||
heroCount,
|
||||
troopCount,
|
||||
originProvinceId,
|
||||
destinationProvinceId,
|
||||
arrivalRoundId,
|
||||
food,
|
||||
gold,
|
||||
unitDetails
|
||||
) =>
|
||||
factionId shouldBe (2: FactionId)
|
||||
heroCount shouldBe 5
|
||||
troopCount shouldBe 750
|
||||
originProvinceId shouldBe (30: ProvinceId)
|
||||
destinationProvinceId shouldBe (40: ProvinceId)
|
||||
arrivalRoundId shouldBe (25: RoundId)
|
||||
food shouldBe 300
|
||||
gold shouldBe 400
|
||||
unitDetails.size shouldBe 1
|
||||
|
||||
// Verify unit details conversion
|
||||
unitDetails.head match {
|
||||
case UnitDetails(heroId, battalion) =>
|
||||
heroId shouldBe (60: HeroId)
|
||||
battalion shouldBe defined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"round trip" should "preserve all values" in {
|
||||
val original = IncomingArmyView(
|
||||
factionId = 3: FactionId,
|
||||
heroCount = 2,
|
||||
troopCount = 100,
|
||||
originProvinceId = 5: ProvinceId,
|
||||
destinationProvinceId = 6: ProvinceId,
|
||||
arrivalRoundId = 7: RoundId,
|
||||
food = 50,
|
||||
gold = 75,
|
||||
unitDetails = Vector(
|
||||
UnitDetails(
|
||||
heroId = 70: HeroId,
|
||||
battalion = Some(
|
||||
BattalionView(
|
||||
id = 300: BattalionId,
|
||||
typeId = BattalionTypeId.Longbowmen,
|
||||
size = 25,
|
||||
training = Some(0.5),
|
||||
armament = Some(0.4),
|
||||
name = "Round Trip Battalion"
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val roundTripped = IncomingArmyViewConverter.fromProto(
|
||||
IncomingArmyViewConverter.toProto(original)
|
||||
)
|
||||
|
||||
roundTripped shouldBe original
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
load("@rules_scala//scala:scala.bzl", "scala_test")
|
||||
|
||||
scala_test(
|
||||
name = "unaffiliated_hero_basics_converter_test",
|
||||
srcs = ["UnaffiliatedHeroBasicsConverterTest.scala"],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:profession_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:recruitment_info_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:unaffiliated_hero_type_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/views:province_view_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/view/province:unaffiliated_hero_basics",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero:profession",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/unaffiliated_hero",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/province:unaffiliated_hero_basics",
|
||||
"@rules_scala//scala/scalatest",
|
||||
],
|
||||
)
|
||||
|
||||
scala_test(
|
||||
name = "full_province_info_converter_test",
|
||||
srcs = ["FullProvinceInfoConverterTest.scala"],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:province_order_type_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/views:province_view_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/view/province:full_province_info",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type_id",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:combat_unit",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero:profession",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/unaffiliated_hero",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/army:army_view",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/battalion",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/province:full_province_info",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/province:unaffiliated_hero_basics",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/stat_with_condition",
|
||||
"@rules_scala//scala/scalatest",
|
||||
],
|
||||
)
|
||||
|
||||
scala_test(
|
||||
name = "province_view_converter_test",
|
||||
srcs = ["ProvinceViewConverterTest.scala"],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:province_event_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:province_order_type_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/views:province_view_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/view/province:province_view",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type_id",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:combat_unit",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/date",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero:profession",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/unaffiliated_hero",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/army:army_view",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/battalion",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/incoming_army:incoming_army_view",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/province:full_province_info",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/province:province_view",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/province:unaffiliated_hero_basics",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/stat_with_condition",
|
||||
"@rules_scala//scala/scalatest",
|
||||
],
|
||||
)
|
||||
+251
@@ -0,0 +1,251 @@
|
||||
package net.eagle0.eagle.model.proto_converters.view.province
|
||||
|
||||
import net.eagle0.eagle.{FactionId, HeroId}
|
||||
import net.eagle0.eagle.common.province_order_type.ProvinceOrderType
|
||||
import net.eagle0.eagle.model.state.hero.Profession
|
||||
import net.eagle0.eagle.model.state.unaffiliated_hero.{RecruitmentInfo, UnaffiliatedHeroType}
|
||||
import net.eagle0.eagle.model.state.BattalionTypeId
|
||||
import net.eagle0.eagle.model.state.CombatUnit
|
||||
import net.eagle0.eagle.model.view.army.ArmyView
|
||||
import net.eagle0.eagle.model.view.battalion.BattalionView
|
||||
import net.eagle0.eagle.model.view.province.{FullProvinceInfo, UnaffiliatedHeroBasics}
|
||||
import net.eagle0.eagle.model.view.stat_with_condition.{Condition, StatWithCondition}
|
||||
import net.eagle0.eagle.views.province_view.FullProvinceInfo as FullProvinceInfoProto
|
||||
import org.scalatest.flatspec.AnyFlatSpec
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
class FullProvinceInfoConverterTest extends AnyFlatSpec with Matchers {
|
||||
|
||||
private val sampleBattalion = BattalionView(
|
||||
id = 100,
|
||||
typeId = BattalionTypeId.LightInfantry,
|
||||
size = 50,
|
||||
training = Some(0.8),
|
||||
armament = Some(0.9),
|
||||
name = "Test Battalion"
|
||||
)
|
||||
|
||||
private val sampleArmy = ArmyView(
|
||||
factionId = 1: FactionId,
|
||||
units = Vector(CombatUnit(factionId = 1: FactionId, heroId = 10: HeroId, battalionId = Some(100))),
|
||||
unitCount = 1,
|
||||
troopCount = 50
|
||||
)
|
||||
|
||||
private val sampleUnaffiliatedHero = UnaffiliatedHeroBasics(
|
||||
heroId = 42: HeroId,
|
||||
nameTextId = "hero_42",
|
||||
profession = Profession.Paladin,
|
||||
status = UnaffiliatedHeroType.Resident,
|
||||
isFactionLeader = false,
|
||||
recruitmentInfo = RecruitmentInfo.WouldJoin
|
||||
)
|
||||
|
||||
private val sampleSupport = StatWithCondition(stat = 75, condition = Condition.High)
|
||||
|
||||
"toProto" should "convert all fields correctly" in {
|
||||
val scala = FullProvinceInfo(
|
||||
rulingHeroId = Some(5: HeroId),
|
||||
rulingFactionHeroIds = Vector(5: HeroId, 10: HeroId, 15: HeroId),
|
||||
battalions = Vector(sampleBattalion),
|
||||
defendingArmy = Some(sampleArmy),
|
||||
economy = 100.0f,
|
||||
agriculture = 80.0f,
|
||||
infrastructure = 60.0f,
|
||||
economyDevastation = 0.1f,
|
||||
agricultureDevastation = 0.2f,
|
||||
infrastructureDevastation = 0.05f,
|
||||
gold = 500,
|
||||
food = 300,
|
||||
priceIndex = 1.5,
|
||||
goldCap = 1000,
|
||||
foodCap = 800,
|
||||
support = Some(sampleSupport),
|
||||
foodConsumption = 25,
|
||||
provinceOrders = ProvinceOrderType.DEVELOP,
|
||||
hexMapName = "test_map",
|
||||
castleCount = 2,
|
||||
heroCap = 5,
|
||||
unaffiliatedHeroes = Vector(sampleUnaffiliatedHero),
|
||||
rulerIsTraveling = true
|
||||
)
|
||||
|
||||
val proto = FullProvinceInfoConverter.toProto(scala)
|
||||
|
||||
// Verify proto fields using named access
|
||||
proto.rulingHeroId shouldBe Some(5)
|
||||
proto.rulingFactionHeroIds shouldBe Seq(5, 10, 15)
|
||||
proto.battalions.size shouldBe 1
|
||||
proto.defendingArmy shouldBe defined
|
||||
proto.economy shouldBe 100.0f
|
||||
proto.agriculture shouldBe 80.0f
|
||||
proto.infrastructure shouldBe 60.0f
|
||||
proto.economyDevastation shouldBe 0.1f
|
||||
proto.agricultureDevastation shouldBe 0.2f
|
||||
proto.infrastructureDevastation shouldBe 0.05f
|
||||
proto.gold shouldBe 500
|
||||
proto.food shouldBe 300
|
||||
proto.priceIndex shouldBe 1.5
|
||||
proto.goldCap shouldBe 1000
|
||||
proto.foodCap shouldBe 800
|
||||
proto.support shouldBe defined
|
||||
proto.foodConsumption shouldBe 25
|
||||
proto.provinceOrders shouldBe ProvinceOrderType.DEVELOP
|
||||
proto.hexMapName shouldBe "test_map"
|
||||
proto.castleCount shouldBe 2
|
||||
proto.heroCap shouldBe 5
|
||||
proto.unaffiliatedHeroes.size shouldBe 1
|
||||
proto.rulerIsTraveling shouldBe true
|
||||
}
|
||||
|
||||
"fromProto" should "convert all fields correctly" in {
|
||||
val original = FullProvinceInfo(
|
||||
rulingHeroId = Some(7: HeroId),
|
||||
rulingFactionHeroIds = Vector(7: HeroId, 8: HeroId),
|
||||
battalions = Vector(sampleBattalion),
|
||||
defendingArmy = Some(sampleArmy),
|
||||
economy = 90.0f,
|
||||
agriculture = 70.0f,
|
||||
infrastructure = 50.0f,
|
||||
economyDevastation = 0.0f,
|
||||
agricultureDevastation = 0.0f,
|
||||
infrastructureDevastation = 0.0f,
|
||||
gold = 400,
|
||||
food = 250,
|
||||
priceIndex = 1.2,
|
||||
goldCap = 900,
|
||||
foodCap = 700,
|
||||
support = Some(sampleSupport),
|
||||
foodConsumption = 20,
|
||||
provinceOrders = ProvinceOrderType.EXPAND,
|
||||
hexMapName = "another_map",
|
||||
castleCount = 1,
|
||||
heroCap = 3,
|
||||
unaffiliatedHeroes = Vector(sampleUnaffiliatedHero),
|
||||
rulerIsTraveling = false
|
||||
)
|
||||
|
||||
val proto = FullProvinceInfoConverter.toProto(original)
|
||||
val result = FullProvinceInfoConverter.fromProto(proto)
|
||||
|
||||
// Match on full Scala case class to catch new fields
|
||||
result match {
|
||||
case FullProvinceInfo(
|
||||
rulingHeroId,
|
||||
rulingFactionHeroIds,
|
||||
battalions,
|
||||
defendingArmy,
|
||||
economy,
|
||||
agriculture,
|
||||
infrastructure,
|
||||
economyDevastation,
|
||||
agricultureDevastation,
|
||||
infrastructureDevastation,
|
||||
gold,
|
||||
food,
|
||||
priceIndex,
|
||||
goldCap,
|
||||
foodCap,
|
||||
support,
|
||||
foodConsumption,
|
||||
provinceOrders,
|
||||
hexMapName,
|
||||
castleCount,
|
||||
heroCap,
|
||||
unaffiliatedHeroes,
|
||||
rulerIsTraveling
|
||||
) =>
|
||||
rulingHeroId shouldBe Some(7: HeroId)
|
||||
rulingFactionHeroIds shouldBe Vector(7: HeroId, 8: HeroId)
|
||||
battalions.size shouldBe 1
|
||||
defendingArmy shouldBe defined
|
||||
economy shouldBe 90.0f
|
||||
agriculture shouldBe 70.0f
|
||||
infrastructure shouldBe 50.0f
|
||||
economyDevastation shouldBe 0.0f
|
||||
agricultureDevastation shouldBe 0.0f
|
||||
infrastructureDevastation shouldBe 0.0f
|
||||
gold shouldBe 400
|
||||
food shouldBe 250
|
||||
priceIndex shouldBe 1.2
|
||||
goldCap shouldBe 900
|
||||
foodCap shouldBe 700
|
||||
support shouldBe defined
|
||||
foodConsumption shouldBe 20
|
||||
provinceOrders shouldBe ProvinceOrderType.EXPAND
|
||||
hexMapName shouldBe "another_map"
|
||||
castleCount shouldBe 1
|
||||
heroCap shouldBe 3
|
||||
unaffiliatedHeroes.size shouldBe 1
|
||||
rulerIsTraveling shouldBe false
|
||||
}
|
||||
}
|
||||
|
||||
"round trip" should "preserve all values" in {
|
||||
val original = FullProvinceInfo(
|
||||
rulingHeroId = None,
|
||||
rulingFactionHeroIds = Vector.empty,
|
||||
battalions = Vector.empty,
|
||||
defendingArmy = None,
|
||||
economy = 50.0f,
|
||||
agriculture = 50.0f,
|
||||
infrastructure = 50.0f,
|
||||
economyDevastation = 0.0f,
|
||||
agricultureDevastation = 0.0f,
|
||||
infrastructureDevastation = 0.0f,
|
||||
gold = 0,
|
||||
food = 0,
|
||||
priceIndex = 1.0,
|
||||
goldCap = 500,
|
||||
foodCap = 500,
|
||||
support = None,
|
||||
foodConsumption = 0,
|
||||
provinceOrders = ProvinceOrderType.UNKNOWN_ORDERS,
|
||||
hexMapName = "empty_map",
|
||||
castleCount = 0,
|
||||
heroCap = 0,
|
||||
unaffiliatedHeroes = Vector.empty,
|
||||
rulerIsTraveling = false
|
||||
)
|
||||
|
||||
val roundTripped = FullProvinceInfoConverter.fromProto(
|
||||
FullProvinceInfoConverter.toProto(original)
|
||||
)
|
||||
|
||||
roundTripped shouldBe original
|
||||
}
|
||||
|
||||
"round trip with all optional fields populated" should "preserve all values" in {
|
||||
val original = FullProvinceInfo(
|
||||
rulingHeroId = Some(5: HeroId),
|
||||
rulingFactionHeroIds = Vector(5: HeroId, 10: HeroId, 15: HeroId),
|
||||
battalions = Vector(sampleBattalion),
|
||||
defendingArmy = Some(sampleArmy),
|
||||
economy = 100.0f,
|
||||
agriculture = 80.0f,
|
||||
infrastructure = 60.0f,
|
||||
economyDevastation = 0.1f,
|
||||
agricultureDevastation = 0.2f,
|
||||
infrastructureDevastation = 0.05f,
|
||||
gold = 500,
|
||||
food = 300,
|
||||
priceIndex = 1.5,
|
||||
goldCap = 1000,
|
||||
foodCap = 800,
|
||||
support = Some(sampleSupport),
|
||||
foodConsumption = 25,
|
||||
provinceOrders = ProvinceOrderType.DEVELOP,
|
||||
hexMapName = "test_map",
|
||||
castleCount = 2,
|
||||
heroCap = 5,
|
||||
unaffiliatedHeroes = Vector(sampleUnaffiliatedHero),
|
||||
rulerIsTraveling = true
|
||||
)
|
||||
|
||||
val roundTripped = FullProvinceInfoConverter.fromProto(
|
||||
FullProvinceInfoConverter.toProto(original)
|
||||
)
|
||||
|
||||
roundTripped shouldBe original
|
||||
}
|
||||
}
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
package net.eagle0.eagle.model.proto_converters.view.province
|
||||
|
||||
import net.eagle0.eagle.{FactionId, HeroId, ProvinceId}
|
||||
import net.eagle0.eagle.common.province_order_type.ProvinceOrderType
|
||||
import net.eagle0.eagle.model.state.date.Date
|
||||
import net.eagle0.eagle.model.state.hero.Profession
|
||||
import net.eagle0.eagle.model.state.unaffiliated_hero.{RecruitmentInfo, UnaffiliatedHeroType}
|
||||
import net.eagle0.eagle.model.state.BattalionTypeId
|
||||
import net.eagle0.eagle.model.state.CombatUnit
|
||||
import net.eagle0.eagle.model.view.army.ArmyView
|
||||
import net.eagle0.eagle.model.view.battalion.BattalionView
|
||||
import net.eagle0.eagle.model.view.incoming_army.{IncomingArmyView, UnitDetails}
|
||||
import net.eagle0.eagle.model.view.province.{FullProvinceInfo, ProvinceView, UnaffiliatedHeroBasics}
|
||||
import net.eagle0.eagle.model.view.stat_with_condition.{Condition, StatWithCondition}
|
||||
import net.eagle0.eagle.views.province_view.ProvinceView as ProvinceViewProto
|
||||
import org.scalatest.flatspec.AnyFlatSpec
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
class ProvinceViewConverterTest extends AnyFlatSpec with Matchers {
|
||||
|
||||
private val sampleBattalion = BattalionView(
|
||||
id = 100,
|
||||
typeId = BattalionTypeId.LightInfantry,
|
||||
size = 50,
|
||||
training = Some(0.8),
|
||||
armament = Some(0.9),
|
||||
name = "Test Battalion"
|
||||
)
|
||||
|
||||
private val sampleArmy = ArmyView(
|
||||
factionId = 1: FactionId,
|
||||
units = Vector(CombatUnit(factionId = 1: FactionId, heroId = 10: HeroId, battalionId = Some(100))),
|
||||
unitCount = 1,
|
||||
troopCount = 50
|
||||
)
|
||||
|
||||
private val sampleUnaffiliatedHero = UnaffiliatedHeroBasics(
|
||||
heroId = 42: HeroId,
|
||||
nameTextId = "hero_42",
|
||||
profession = Profession.Paladin,
|
||||
status = UnaffiliatedHeroType.Resident,
|
||||
isFactionLeader = false,
|
||||
recruitmentInfo = RecruitmentInfo.WouldJoin
|
||||
)
|
||||
|
||||
private val sampleSupport = StatWithCondition(stat = 75, condition = Condition.High)
|
||||
|
||||
private val sampleFullInfo = FullProvinceInfo(
|
||||
rulingHeroId = Some(5: HeroId),
|
||||
rulingFactionHeroIds = Vector(5: HeroId, 10: HeroId),
|
||||
battalions = Vector(sampleBattalion),
|
||||
defendingArmy = Some(sampleArmy),
|
||||
economy = 100.0f,
|
||||
agriculture = 80.0f,
|
||||
infrastructure = 60.0f,
|
||||
economyDevastation = 0.1f,
|
||||
agricultureDevastation = 0.2f,
|
||||
infrastructureDevastation = 0.05f,
|
||||
gold = 500,
|
||||
food = 300,
|
||||
priceIndex = 1.5,
|
||||
goldCap = 1000,
|
||||
foodCap = 800,
|
||||
support = Some(sampleSupport),
|
||||
foodConsumption = 25,
|
||||
provinceOrders = ProvinceOrderType.DEVELOP,
|
||||
hexMapName = "test_map",
|
||||
castleCount = 2,
|
||||
heroCap = 5,
|
||||
unaffiliatedHeroes = Vector(sampleUnaffiliatedHero),
|
||||
rulerIsTraveling = false
|
||||
)
|
||||
|
||||
private val sampleIncomingArmy = IncomingArmyView(
|
||||
factionId = 2: FactionId,
|
||||
heroCount = 1,
|
||||
troopCount = 100,
|
||||
originProvinceId = 5: ProvinceId,
|
||||
destinationProvinceId = 10: ProvinceId,
|
||||
arrivalRoundId = 15,
|
||||
food = 50,
|
||||
gold = 100,
|
||||
unitDetails = Vector(UnitDetails(heroId = 20: HeroId, battalion = None))
|
||||
)
|
||||
|
||||
private val sampleDate = Date(year = 1000, month = Date.Month.March)
|
||||
|
||||
"toProto" should "convert all fields correctly" in {
|
||||
val scala = ProvinceView(
|
||||
id = 42: ProvinceId,
|
||||
name = "Test Province",
|
||||
rulingFactionId = Some(1: FactionId),
|
||||
fullInfo = Some(sampleFullInfo),
|
||||
knownEvents = Vector.empty,
|
||||
knownIncomingArmies = Vector(sampleIncomingArmy),
|
||||
asOf = Some(sampleDate)
|
||||
)
|
||||
|
||||
val proto = ProvinceViewConverter.toProto(scala)
|
||||
|
||||
// Match on full case class to catch new fields
|
||||
proto match {
|
||||
case ProvinceViewProto(
|
||||
id,
|
||||
name,
|
||||
rulingFactionId,
|
||||
fullInfo,
|
||||
knownEvents,
|
||||
knownIncomingArmies,
|
||||
asOf,
|
||||
_ /* unknownFields */
|
||||
) =>
|
||||
id shouldBe 42
|
||||
name shouldBe "Test Province"
|
||||
rulingFactionId shouldBe Some(1)
|
||||
fullInfo shouldBe defined
|
||||
knownEvents.size shouldBe 0
|
||||
knownIncomingArmies.size shouldBe 1
|
||||
asOf shouldBe defined
|
||||
}
|
||||
}
|
||||
|
||||
"fromProto" should "convert all fields correctly" in {
|
||||
val original = ProvinceView(
|
||||
id = 55: ProvinceId,
|
||||
name = "Another Province",
|
||||
rulingFactionId = Some(3: FactionId),
|
||||
fullInfo = Some(sampleFullInfo),
|
||||
knownEvents = Vector.empty,
|
||||
knownIncomingArmies = Vector(sampleIncomingArmy),
|
||||
asOf = Some(sampleDate)
|
||||
)
|
||||
|
||||
val proto = ProvinceViewConverter.toProto(original)
|
||||
val result = ProvinceViewConverter.fromProto(proto)
|
||||
|
||||
// Match on full case class to catch new fields
|
||||
result match {
|
||||
case ProvinceView(
|
||||
id,
|
||||
name,
|
||||
rulingFactionId,
|
||||
fullInfo,
|
||||
knownEvents,
|
||||
knownIncomingArmies,
|
||||
asOf
|
||||
) =>
|
||||
id shouldBe (55: ProvinceId)
|
||||
name shouldBe "Another Province"
|
||||
rulingFactionId shouldBe Some(3: FactionId)
|
||||
fullInfo shouldBe defined
|
||||
knownEvents.size shouldBe 0
|
||||
knownIncomingArmies.size shouldBe 1
|
||||
asOf shouldBe defined
|
||||
}
|
||||
}
|
||||
|
||||
"round trip" should "preserve all values with empty collections" in {
|
||||
val original = ProvinceView(
|
||||
id = 1: ProvinceId,
|
||||
name = "Empty Province",
|
||||
rulingFactionId = None,
|
||||
fullInfo = None,
|
||||
knownEvents = Vector.empty,
|
||||
knownIncomingArmies = Vector.empty,
|
||||
asOf = None
|
||||
)
|
||||
|
||||
val roundTripped = ProvinceViewConverter.fromProto(
|
||||
ProvinceViewConverter.toProto(original)
|
||||
)
|
||||
|
||||
roundTripped shouldBe original
|
||||
}
|
||||
|
||||
"round trip" should "preserve all values with populated collections" in {
|
||||
val original = ProvinceView(
|
||||
id = 42: ProvinceId,
|
||||
name = "Test Province",
|
||||
rulingFactionId = Some(1: FactionId),
|
||||
fullInfo = Some(sampleFullInfo),
|
||||
knownEvents = Vector.empty,
|
||||
knownIncomingArmies = Vector(sampleIncomingArmy),
|
||||
asOf = Some(sampleDate)
|
||||
)
|
||||
|
||||
val roundTripped = ProvinceViewConverter.fromProto(
|
||||
ProvinceViewConverter.toProto(original)
|
||||
)
|
||||
|
||||
roundTripped shouldBe original
|
||||
}
|
||||
|
||||
"round trip" should "preserve province without fullInfo but with other data" in {
|
||||
val original = ProvinceView(
|
||||
id = 77: ProvinceId,
|
||||
name = "Partial Province",
|
||||
rulingFactionId = Some(2: FactionId),
|
||||
fullInfo = None,
|
||||
knownEvents = Vector.empty,
|
||||
knownIncomingArmies = Vector(sampleIncomingArmy),
|
||||
asOf = Some(sampleDate)
|
||||
)
|
||||
|
||||
val roundTripped = ProvinceViewConverter.fromProto(
|
||||
ProvinceViewConverter.toProto(original)
|
||||
)
|
||||
|
||||
roundTripped shouldBe original
|
||||
}
|
||||
}
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
package net.eagle0.eagle.model.proto_converters.view.province
|
||||
|
||||
import net.eagle0.eagle.common.profession.Profession as ProfessionProto
|
||||
import net.eagle0.eagle.common.recruitment_info.{
|
||||
RecruitmentInfo as RecruitmentInfoProto,
|
||||
RecruitmentStatus as RecruitmentStatusProto
|
||||
}
|
||||
import net.eagle0.eagle.common.unaffiliated_hero_type.UnaffiliatedHeroType as UnaffiliatedHeroTypeProto
|
||||
import net.eagle0.eagle.model.state.hero.Profession
|
||||
import net.eagle0.eagle.model.state.unaffiliated_hero.{RecruitmentInfo, UnaffiliatedHeroType}
|
||||
import net.eagle0.eagle.model.view.province.UnaffiliatedHeroBasics
|
||||
import net.eagle0.eagle.views.province_view.UnaffiliatedHeroBasics as UnaffiliatedHeroBasicsProto
|
||||
import net.eagle0.eagle.HeroId
|
||||
import org.scalatest.flatspec.AnyFlatSpec
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
class UnaffiliatedHeroBasicsConverterTest extends AnyFlatSpec with Matchers {
|
||||
|
||||
"toProto" should "convert all fields correctly" in {
|
||||
val scala = UnaffiliatedHeroBasics(
|
||||
heroId = 42: HeroId,
|
||||
nameTextId = "hero_name_42",
|
||||
profession = Profession.Paladin,
|
||||
status = UnaffiliatedHeroType.Resident,
|
||||
isFactionLeader = true,
|
||||
recruitmentInfo = RecruitmentInfo.WouldJoin
|
||||
)
|
||||
|
||||
val proto = UnaffiliatedHeroBasicsConverter.toProto(scala)
|
||||
|
||||
// Match on full case class to catch new fields
|
||||
proto match {
|
||||
case UnaffiliatedHeroBasicsProto(
|
||||
heroId,
|
||||
nameTextId,
|
||||
profession,
|
||||
status,
|
||||
isFactionLeader,
|
||||
recruitmentInfo,
|
||||
_ /* unknownFields */
|
||||
) =>
|
||||
heroId shouldBe 42
|
||||
nameTextId shouldBe "hero_name_42"
|
||||
profession shouldBe ProfessionProto.PALADIN
|
||||
status shouldBe UnaffiliatedHeroTypeProto.UNAFFILIATED_HERO_RESIDENT
|
||||
isFactionLeader shouldBe true
|
||||
recruitmentInfo shouldBe defined
|
||||
}
|
||||
}
|
||||
|
||||
"fromProto" should "convert all fields correctly" in {
|
||||
val proto = UnaffiliatedHeroBasicsProto(
|
||||
heroId = 55,
|
||||
nameTextId = "hero_name_55",
|
||||
profession = ProfessionProto.MAGE,
|
||||
status = UnaffiliatedHeroTypeProto.UNAFFILIATED_HERO_TRAVELER,
|
||||
isFactionLeader = false,
|
||||
recruitmentInfo = Some(
|
||||
RecruitmentInfoProto(status = RecruitmentStatusProto.RECRUITMENT_STATUS_TRAVELER)
|
||||
)
|
||||
)
|
||||
|
||||
val scala = UnaffiliatedHeroBasicsConverter.fromProto(proto)
|
||||
|
||||
// Match on full case class to catch new fields
|
||||
scala match {
|
||||
case UnaffiliatedHeroBasics(
|
||||
heroId,
|
||||
nameTextId,
|
||||
profession,
|
||||
status,
|
||||
isFactionLeader,
|
||||
recruitmentInfo
|
||||
) =>
|
||||
heroId shouldBe (55: HeroId)
|
||||
nameTextId shouldBe "hero_name_55"
|
||||
profession shouldBe Profession.Mage
|
||||
status shouldBe UnaffiliatedHeroType.Traveler
|
||||
isFactionLeader shouldBe false
|
||||
recruitmentInfo shouldBe RecruitmentInfo.Traveler
|
||||
}
|
||||
}
|
||||
|
||||
"round trip" should "preserve all values" in {
|
||||
val original = UnaffiliatedHeroBasics(
|
||||
heroId = 77: HeroId,
|
||||
nameTextId = "hero_name_77",
|
||||
profession = Profession.Champion,
|
||||
status = UnaffiliatedHeroType.Outlaw,
|
||||
isFactionLeader = false,
|
||||
recruitmentInfo = RecruitmentInfo.Outlaw
|
||||
)
|
||||
|
||||
val roundTripped = UnaffiliatedHeroBasicsConverter.fromProto(
|
||||
UnaffiliatedHeroBasicsConverter.toProto(original)
|
||||
)
|
||||
|
||||
roundTripped shouldBe original
|
||||
}
|
||||
|
||||
"status conversion" should "handle all status types" in {
|
||||
val statuses = List(
|
||||
UnaffiliatedHeroType.Unknown -> UnaffiliatedHeroTypeProto.UNAFFILIATED_HERO_UNKNOWN,
|
||||
UnaffiliatedHeroType.Prisoner -> UnaffiliatedHeroTypeProto.UNAFFILIATED_HERO_PRISONER,
|
||||
UnaffiliatedHeroType.Traveler -> UnaffiliatedHeroTypeProto.UNAFFILIATED_HERO_TRAVELER,
|
||||
UnaffiliatedHeroType.Resident -> UnaffiliatedHeroTypeProto.UNAFFILIATED_HERO_RESIDENT,
|
||||
UnaffiliatedHeroType.Outlaw -> UnaffiliatedHeroTypeProto.UNAFFILIATED_HERO_OUTLAW,
|
||||
UnaffiliatedHeroType.MovingPrisoner -> UnaffiliatedHeroTypeProto.UNAFFILIATED_HERO_MOVING_PRISONER,
|
||||
UnaffiliatedHeroType.ReturningPrisoner -> UnaffiliatedHeroTypeProto.UNAFFILIATED_HERO_RETURNING_PRISONER
|
||||
)
|
||||
|
||||
statuses.foreach {
|
||||
case (scalaStatus, protoStatus) =>
|
||||
val scala = UnaffiliatedHeroBasics(
|
||||
heroId = 1: HeroId,
|
||||
nameTextId = "test",
|
||||
profession = Profession.NoProfession,
|
||||
status = scalaStatus,
|
||||
isFactionLeader = false,
|
||||
recruitmentInfo = RecruitmentInfo.Unknown
|
||||
)
|
||||
val proto = UnaffiliatedHeroBasicsConverter.toProto(scala)
|
||||
proto.status shouldBe protoStatus
|
||||
}
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
load("@rules_scala//scala:scala.bzl", "scala_test")
|
||||
|
||||
scala_test(
|
||||
name = "stat_with_condition_converter_test",
|
||||
srcs = ["StatWithConditionConverterTest.scala"],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/views:stat_with_condition_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/view/stat_with_condition",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/stat_with_condition",
|
||||
"@rules_scala//scala/scalatest",
|
||||
],
|
||||
)
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
package net.eagle0.eagle.model.proto_converters.view.stat_with_condition
|
||||
|
||||
import net.eagle0.eagle.model.view.stat_with_condition.{Condition, StatWithCondition}
|
||||
import net.eagle0.eagle.views.stat_with_condition.StatWithCondition.Condition as ConditionProto
|
||||
import net.eagle0.eagle.views.stat_with_condition.StatWithCondition as StatWithConditionProto
|
||||
import org.scalatest.flatspec.AnyFlatSpec
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
class StatWithConditionConverterTest extends AnyFlatSpec with Matchers {
|
||||
|
||||
"toProto" should "convert all fields correctly" in {
|
||||
val scala = StatWithCondition(
|
||||
stat = 75,
|
||||
condition = Condition.High
|
||||
)
|
||||
|
||||
val proto = StatWithConditionConverter.toProto(scala)
|
||||
|
||||
// Match on full case class to catch new fields
|
||||
proto match {
|
||||
case StatWithConditionProto(
|
||||
stat,
|
||||
condition,
|
||||
_ /* unknownFields */
|
||||
) =>
|
||||
stat shouldBe 75
|
||||
condition shouldBe ConditionProto.HIGH
|
||||
}
|
||||
}
|
||||
|
||||
"fromProto" should "convert all fields correctly" in {
|
||||
val proto = StatWithConditionProto(
|
||||
stat = 42,
|
||||
condition = ConditionProto.VERY_LOW
|
||||
)
|
||||
|
||||
val scala = StatWithConditionConverter.fromProto(proto)
|
||||
|
||||
// Match on full case class to catch new fields
|
||||
scala match {
|
||||
case StatWithCondition(
|
||||
stat,
|
||||
condition
|
||||
) =>
|
||||
stat shouldBe 42
|
||||
condition shouldBe Condition.VeryLow
|
||||
}
|
||||
}
|
||||
|
||||
"round trip" should "preserve all values" in {
|
||||
val original = StatWithCondition(
|
||||
stat = 100,
|
||||
condition = Condition.Medium
|
||||
)
|
||||
|
||||
val roundTripped = StatWithConditionConverter.fromProto(
|
||||
StatWithConditionConverter.toProto(original)
|
||||
)
|
||||
|
||||
roundTripped shouldBe original
|
||||
}
|
||||
|
||||
"condition conversion" should "handle all condition values" in {
|
||||
val conditions = List(
|
||||
Condition.Unknown -> ConditionProto.UNKNOWN,
|
||||
Condition.VeryHigh -> ConditionProto.VERY_HIGH,
|
||||
Condition.High -> ConditionProto.HIGH,
|
||||
Condition.Medium -> ConditionProto.MEDIUM,
|
||||
Condition.Low -> ConditionProto.LOW,
|
||||
Condition.VeryLow -> ConditionProto.VERY_LOW
|
||||
)
|
||||
|
||||
conditions.foreach {
|
||||
case (scalaCondition, protoCondition) =>
|
||||
val scala = StatWithCondition(stat = 50, condition = scalaCondition)
|
||||
val proto = StatWithConditionConverter.toProto(scala)
|
||||
proto.condition shouldBe protoCondition
|
||||
|
||||
val backToScala = StatWithConditionConverter.fromProto(proto)
|
||||
backToScala.condition shouldBe scalaCondition
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user