Print selected command templates (#8454)

This commit is contained in:
2026-07-11 13:30:44 -07:00
committed by GitHub
parent c1c60c406a
commit 1cd7474864
3 changed files with 104 additions and 4 deletions
+8 -2
View File
@@ -37,7 +37,7 @@ commands
commands-json
```
`state` is the concise map and owned-province view. `state-json` prints the complete view. `commands-json` includes the exact fields, IDs, resource amounts, and command choices currently accepted by the server.
`state` is the concise map and owned-province view. `state-json` prints the complete view. `commands-json` includes the exact fields, IDs, resource amounts, and command choices currently accepted by the server. Each entry also includes a `post-json` template for its corresponding selected command.
Generated text is stored locally during the client process. Use `text <text-id>` for a text ID without spaces, such as `text hn_105`. Province leaders in `state` are resolved through this store when their names have been received.
@@ -49,6 +49,12 @@ Use the available-command data from `commands-json` to select a command, then po
post-json <province-id> <SelectedCommandType> <JSON>
```
Templates contain the accepted JSON field names and default values. Replace zero
IDs, `UNKNOWN` enum values, empty collections, and other placeholders with a legal
choice from the available-command payload before posting. Where a selected command
must echo server data, such as a ransom offer, the client copies that data into the
template automatically.
For example:
```text
@@ -57,7 +63,7 @@ post-rest 13
post-feast 13
```
The client has shortcuts for `post-rest`, `post-return`, `post-feast`, riot decisions, and battle aftermath. Other selected-command JSON schemas are defined in:
The client has shortcuts for `post-rest`, `post-return`, `post-feast`, riot decisions, and battle aftermath. The authoritative selected-command schemas are defined in:
```text
src/main/protobuf/net/eagle0/eagle/api/selected_command.proto
@@ -13,6 +13,7 @@ import io.grpc.{ManagedChannel, ManagedChannelBuilder, Metadata}
import io.grpc.stub.{MetadataUtils, StreamObserver}
import net.eagle0.eagle.api.available_command.AvailableCommand as AvailableCommandProto
import net.eagle0.eagle.api.available_command.MarchAvailableCommand
import net.eagle0.eagle.api.available_command as ac
import net.eagle0.eagle.api.command.AvailableCommands
import net.eagle0.eagle.api.eagle.*
import net.eagle0.eagle.api.eagle.EagleGrpc.EagleStub
@@ -28,7 +29,7 @@ import net.eagle0.eagle.views.province_view.{FullProvinceInfo, FullProvinceInfoD
import net.eagle0.shardok.api.command_descriptor.{AvailableCommands as ShardokAvailableCommands, CommandDescriptor}
import net.eagle0.shardok.api.unit_view.UnitView
import scalapb.{GeneratedMessage, GeneratedMessageCompanion}
import scalapb.json4s.JsonFormat
import scalapb.json4s.{JsonFormat, Printer}
object EagleTextClient {
private val AuthorizationKey: Metadata.Key[String] =
@@ -1030,7 +1031,11 @@ final class ClientState {
val commandLines = oneProvince.commands.zipWithIndex.map {
case (cmd, index) =>
val prefix = s" [$index] ${commandName(cmd)}"
if rawJson then s"$prefix ${commandJson(cmd)}" else prefix
if rawJson then
val (selectedType, json) = selectedCommandTemplate(cmd)
val template = s"\n post-json $provinceId $selectedType $json"
s"$prefix ${commandJson(cmd)}$template"
else prefix
}
s"province $provinceId suggested=${oneProvince.suggestedCommandIndex}" +: commandLines
}
@@ -1114,6 +1119,32 @@ final class ClientState {
private def toJson(message: GeneratedMessage): String =
JsonFormat.toJsonString(message)
private def selectedCommandTemplate(command: AvailableCommandProto): (String, String) = {
val selectedType = commandName(command).replace("AvailableCommand", "SelectedCommand")
val selected = command match {
case ransom: ac.ResolveRansomOfferAvailableCommand =>
ransom.offers.headOption
.map(offer =>
sc.ResolveRansomOfferSelectedCommand(
offeringFactionId = offer.originatingFactionId,
ransomOffer = Some(offer),
resolution = offer.status
)
)
.getOrElse(selectedCommandDefault(selectedType))
case _ => selectedCommandDefault(selectedType)
}
selectedType -> Printer().includingDefaultValueFields.print(selected)
}
private def selectedCommandDefault(selectedType: String): GeneratedMessage =
Try {
val companionClass = Class.forName(s"net.eagle0.eagle.api.selected_command.$selectedType$$")
val companion = companionClass.getField("MODULE$").get(null).asInstanceOf[GeneratedMessageCompanion[?]]
companion.defaultInstance.asInstanceOf[GeneratedMessage]
}.getOrElse(throw new IllegalArgumentException(s"No selected-command type found for $selectedType"))
private def shardokCommandSummary(command: CommandDescriptor, rawJson: Boolean): String = {
val actor = command.actor.map(_.toString).getOrElse("-")
val target = command.target.map(c => s"(${c.row},${c.column})").getOrElse("-")
@@ -1,6 +1,10 @@
package net.eagle0.eagle.text_client
import net.eagle0.eagle.api.available_command.{ImproveAvailableCommand, ResolveRansomOfferAvailableCommand}
import net.eagle0.eagle.api.command.{AvailableCommands as EagleAvailableCommands, OneProvinceAvailableCommands}
import net.eagle0.eagle.api.eagle.{ActionResultResponse, GameUpdate, ShardokActionResultResponse, UpdateStreamResponse}
import net.eagle0.eagle.common.diplomacy_offer.DiplomacyOffer
import net.eagle0.eagle.common.diplomacy_offer_status.DiplomacyOfferStatus
import net.eagle0.eagle.views.action_result_view.ActionResultView
import net.eagle0.eagle.views.game_state_view.{GameStateView, GameStateViewDiff}
import net.eagle0.eagle.views.province_view.{ProvinceView, ProvinceViewDiff}
@@ -52,6 +56,28 @@ class ClientStateTest extends AnyFlatSpec with Matchers {
currentCommand = Vector(CommandDescriptor(player = 3))
)
private def eagleCommandsUpdate(commands: Vector[net.eagle0.eagle.api.available_command.AvailableCommand]) =
UpdateStreamResponse(
UpdateStreamResponse.ResponseDetails.GameUpdate(
GameUpdate(
gameId = 1L,
gameUpdateDetails = GameUpdate.GameUpdateDetails.ActionResultResponse(
ActionResultResponse(
unfilteredResultCountAfter = 1,
availableCommands = Some(
EagleAvailableCommands(
token = 1L,
commandsByProvince = Map(
12 -> OneProvinceAvailableCommands(provinceId = 12, commands = commands)
)
)
)
)
)
)
)
)
"ClientTextStore" should "resume UTF-8 streaming text from byte offsets" in {
val store = new ClientTextStore
@@ -174,4 +200,41 @@ class ClientStateTest extends AnyFlatSpec with Matchers {
state.describeShardokCommands(rawJson = false) shouldBe "No Shardok battles yet."
state.shardokCommandPost(Some("battle-1"), index = 0) shouldBe None
}
it should "include selected command templates in JSON command output" in {
val state = new ClientState
state.handle(startingState())
state.handle(eagleCommandsUpdate(Vector(ImproveAvailableCommand(actingProvinceId = 12))))
val output = state.describeCommands(rawJson = true)
output should include("post-json 12 ImproveSelectedCommand")
output should include("\"actingHeroId\":0")
output should include("\"lockType\":false")
}
it should "copy a ransom offer into its selected command template" in {
val state = new ClientState
state.handle(startingState())
state.handle(
eagleCommandsUpdate(
Vector(
ResolveRansomOfferAvailableCommand(
offers = Vector(
DiplomacyOffer(
originatingFactionId = 2,
targetFactionId = 3,
status = DiplomacyOfferStatus.DIPLOMACY_OFFER_STATUS_UNRESOLVED
)
)
)
)
)
)
val output = state.describeCommands(rawJson = true)
output should include("post-json 12 ResolveRansomOfferSelectedCommand")
output should include("\"offeringFactionId\":2")
output should include("\"ransomOffer\":{")
output should include("\"originatingFactionId\":2")
}
}