- Add filteredProvinceView(ProvinceT, ScalaGameState, FactionId) overload - Use protoless FactionUtils.hasAlliance, Visibility.hasFullVisibility, and ProvinceUtils.incomingOthers - Add helper methods: fullProvinceInfoScala, maybeIncomingAttackersScala, unaffiliatedHeroInfoScala - Handle reconned provinces directly from Scala FactionT.reconnedProvinces GameStateViewFilter improvements: - Eliminate GameStateConverter.toProto() call in Scala overload - Use new ProvinceViewFilter Scala overload for faction filtering - Convert battalionTypes and chronicleEntries to proto only at output boundary 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
10 KiB
Deproto Migration Plan
This document tracks the migration from protobuf types to native Scala models inside the Eagle game engine.
Architectural Decisions
- Keep proto for persistence: Yes - protobuf is used for persisting game state
- Keep proto for Shardok communication: Yes - protobuf is used for Eagle-Shardok gRPC communication
- Use Scala views inside the library: Yes - use native Scala types like
ProvinceView,FactionView,HeroView, etc. within the library code
Recent Completed Work
GameState Round-Trip Elimination (PRs #4913, #4914, #4915)
Eliminated wasteful Scala→proto→Scala conversions in the hot path:
-
LLM Pipeline (#4913):
LlmRequestWithGameStatenow uses ScalaGameStateinstead of proto. All ~38 prompt generators updated to use Scala model types (FactionT,HeroT,ProvinceT). -
ActionWithResultingState Caching (#4914): Added
precomputedScalaState: Option[GameState]to cache Scala state when available, avoidingfromProto()conversion instateAfter(). -
PostResults Simplification (#4915): Changed
PostResults.gameStatefrom proto toOption[GameState](Scala), eliminatingtoProto()calls when creating PostResults.
Migration Pattern
The codebase follows a Legacy pattern* for separating proto-dependent and protoless code:
- Protoless utilities:
FactionUtils,HeroUtils,ProvinceUtils,ProvinceDistances, etc. - Proto-dependent utilities:
LegacyFactionUtils,LegacyHeroUtils,LegacyProvinceUtils,LegacyProvinceDistances, etc.
When migrating a file:
- Create a
Legacy*version containing the proto-dependent methods - Keep the original file name for protoless methods
- Update callers to use the appropriate version based on their context
Migration Status
Fully Protoless (no proto imports)
Utilities:
FactionUtils- has protolessownedNeighborsmethodProvinceDistances- split into protoless +LegacyProvinceDistancesSwornBrotherChooser- fully protoless (removedbestChoiceProto)
Command Selectors (all use native GameState):
AllianceOfferCommandSelectorAlmsCommandSelectorAttackCommandChooserExpandCommandSelectorHeroGiftCommandSelectorImproveCommandSelectorMarchTowardProvinceCommandChooser- in AI folder, uses native GameState (callers convert)OrganizeCommandSelectorRansomOfferHelpersSeekMoreLeadersCommandChooser- in AI folder, uses native GameStateTruceOfferCommandSelectorTrustForDiplomacy
Quest Command Selectors (all protoless):
AllianceQuestCommandChooserAlmsAcrossRealmQuestCommandChooserAlmsToProvinceQuestCommandChooserDismissSpecificVassalCommandChooserGiveToHeroesAcrossRealmQuestCommandChooserGiveToHeroesInProvinceQuestCommandChooserImproveQuestCommandChooserQuestCommandChooserTruceCountQuestCommandChooserTruceWithFactionQuestCommandChooser
Fully Protoless
AIClientUtils- has protoless overloads (takenHeroIdsForMarchTowardFocus,mostPowerfulHeroes)AttackCommandChooser- uses ScalaGameStateandBattalionViewC(Scala view type)BattalionPower- hasestimatedPower(BattalionViewC)for recon data with optional statsCommandChoiceHelpers- fully protoless, uses ScalaGameStatethroughoutProvinceGoldSurplusCalculator- fully protoless (callers use converters)HeroSelector- fully protoless (removed deadminimallyFatiguedHeroesProto)
AI Layer ✅ COMPLETE
All AI and command chooser code is now fully protoless:
AttackDecisionCommandChooser- uses Scala GameStateCommandChooser- trait uses Scala GameStateFulfillQuestsCommandSelector- uses Scala GameStateMidGameAIClient- uses Scala GameState internally
Still Using Proto GameState (Boundary Code)
These files use proto GameState because they're at system boundaries:
View Filters (client projection):
view_filters/GameStateViewFilter- has Scala overload, uses Scala sub-filtersview_filters/ProvinceViewFilter- has Scala overloads for some methodsview_filters/FactionViewFilter- has Scala overloadview_filters/HeroViewFilter- has Scala overloadview_filters/BattalionNameFilter- has Scala overloadview_filters/BattleFilter- has Scala overload
Legacy Utilities (to be deprecated):
LegacyProvinceDistances,LegacyFactionUtils,LegacyHeroUtils, etc.- Used by code that still needs proto GameState
Persistence/Action System:
ActionResultTApplier,ActionResultProtoApplier- apply results to proto stateActionWithResultingState- caches both proto and Scala state
Shardok Interface (gRPC boundary):
ShardokInterfaceGrpcClient,ShardokInterfaceProxy- must use proto for C++ communication
Next Steps
Phase 1-3: AI Layer ✅ COMPLETE
The entire AI decision-making layer is now protoless.
Phase 4: View Filters ✅ COMPLETE
The view_filters package migration is complete:
Completed:
GameStateViewFilter- added Scala GameState overload (uses Scala sub-filters)ProvinceViewFilter- already has Scala overloads for server-side viewsHumanPlayerClientConnectionState- updated to pass Scala GameState directlyHeroViewFilter- added Scala overloadFactionViewFilter- added Scala overloadVisibility- added Scala overloads
Still Using Proto:
BattalionNameFilter- has Scala overloadBattleFilter- has Scala overloadActionResultFilter- uses proto internally (boundary code)
Strategy:
- Add Scala GameState overloads to view filter methods
- Update callers to pass Scala GameState where available
- Eventually deprecate proto versions
Phase 5: Legacy Utility Cleanup (IN PROGRESS)
Remove Legacy* utilities by migrating remaining callers:
- Identify callers of each Legacy* util
- Update callers to use protoless versions
- Delete Legacy* files when no longer needed
Deleted (no production callers):
LegacyProvinceDistances- deleted (no callers)LegacyBattalionSuitability- deleted (no callers)LegacyFoodConsumptionUtils- deleted (no callers)LegacyHandleRiotUtils- deleted (no callers)
Refactored to Thin Wrappers (delegating to protoless versions):
LegacyRansomValidity- already a thin wrapper delegating toRansomValidityLegacyRecruitmentOdds- refactored to delegate toRecruitmentOdds
Parallel Implementations (proto mirrors protoless):
FactionUtils/LegacyFactionUtils- both have matching APIs; LegacyFactionUtils used by boundary code (24+ callers)HeroUtils/LegacyHeroUtils- both have matching APIs; LegacyHeroUtils used by boundary code (10 callers)ProvinceUtils/LegacyProvinceUtils- both have matching APIs; LegacyProvinceUtils used by boundary code (20 callers: availability factories, view filters)
Parallel Implementations (awaiting migration of callers):
BattalionUtils/LegacyBattalionUtils- both have matching core methods; LegacyBattalionUtils used by boundary code (4 callers)BattalionViewFilter/LegacyBattalionViewFilter- protoless version exists; Legacy used by view filters, action appliers (3 callers)BattalionTypeFinder/LegacyBattalionTypeFinder- protoless version exists; Legacy used by validators (1 caller: RuntimeValidator)
Recent Caller Migration
CheckForFulfilledQuestsAction - migrated to use protoless BattalionTypeFinder:
- Changed
battalionTypesparameter from protoVector[BattalionType]to ScalaVector[BattalionType] - Updated callers (EngineImpl, EndVassalCommandsPhaseAction) to pass Scala types directly
- Eliminated wasteful
BattalionTypeConverter.toProto()conversions
ExpandedUnaffiliatedHeroUtils - added Scala overload:
- New overload takes Scala
GameStateandUnaffiliatedHeroTdirectly - Added
UnaffiliatedHeroConverter.unaffiliatedHeroTypeToProto()helper for efficient enum conversion - Proto overload retained for backward compatibility
AvailablePleaseRecruitMeCommandFactory - eliminated wasteful proto conversions:
- Now uses
ExpandedUnaffiliatedHeroUtils.expandedUnaffiliatedHero(ScalaGameState, UnaffiliatedHeroT)directly - Removed
GameStateConverter.toProto()andUnaffiliatedHeroConverter.toProto()calls - Factory is now fully protoless internally (still returns proto types for API boundary)
ProvinceViewFilter - added Scala overload with faction filtering:
- New
filteredProvinceView(province: ProvinceT, gs: ScalaGameState, factionId: FactionId)overload - Uses protoless
FactionUtils.hasAlliance,Visibility.hasFullVisibility, andProvinceUtils.incomingOthers - Handles reconned provinces directly from Scala
FactionT.reconnedProvinces(already Scala type) - Added helper methods:
fullProvinceInfoScala,maybeIncomingAttackersScala,unaffiliatedHeroInfoScala - Events still converted to proto at the end (ProvinceView.knownEvents uses proto events)
GameStateViewFilter - eliminated GameStateConverter.toProto() call:
- Scala overload now fully protoless internally
- Uses the new ProvinceViewFilter Scala overload with faction filtering
- Converts
battalionTypesandchronicleEntriesto proto only at output boundary
Key Files
Protoless Model Types
src/main/scala/net/eagle0/eagle/model/state/game_state/GameState.scala- native Scala GameStatesrc/main/scala/net/eagle0/eagle/model/state/province/ProvinceView.scala- province view typesrc/main/scala/net/eagle0/eagle/model/state/faction/FactionView.scala- faction view typesrc/main/scala/net/eagle0/eagle/model/state/hero/HeroView.scala- hero view type
Proto Converters
src/main/scala/net/eagle0/eagle/model/proto_converters/game_state/- converts between proto and Scala types
Notes
- The AI client code (
src/main/scala/net/eagle0/eagle/ai/) is now fully protoless - Proto GameState is still needed at boundaries: persistence, gRPC to Shardok
PerformUnaffiliatedHeroesActionand the LLM pipeline use protolessGameStateGameStateViewFilterScala overload is now fully protoless internally (converts to proto only at output)ProvinceViewFilterhas Scala overloads for all three modes: no filtering, faction filtering, and withdrawn-from view