mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-28 22:35:42 +00:00
Track generated text in text client (#8424)
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
package net.eagle0.eagle.text_client
|
||||
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.nio.file.{Files, Paths}
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.TimeUnit
|
||||
@@ -216,6 +217,7 @@ final class EagleTextClient(config: Config) {
|
||||
case Vector("status") => println(state.describeStatus)
|
||||
case Vector("state") => println(state.describeState)
|
||||
case Vector("state-json") => println(state.stateJson)
|
||||
case Vector("text", textId) => println(state.describeText(textId))
|
||||
case Vector("commands") => println(state.describeCommands(rawJson = false))
|
||||
case Vector("commands-json") => println(state.describeCommands(rawJson = true))
|
||||
case Vector("shardok-commands") => println(state.describeShardokCommands(rawJson = false))
|
||||
@@ -303,6 +305,7 @@ final class EagleTextClient(config: Config) {
|
||||
UpdateStreamRequest.StreamGameRequest(
|
||||
gameId = gameId,
|
||||
unfilteredResultCount = state.unfilteredResultCount,
|
||||
streamingTextStatuses = state.streamingTextStatuses,
|
||||
clientId = UUID.randomUUID().toString
|
||||
)
|
||||
)
|
||||
@@ -472,6 +475,7 @@ final class ClientState {
|
||||
private var latestGameId: Option[Long] = None
|
||||
private var latestUnfilteredResultCount: Int = 0
|
||||
private var latestShardokBattles: Map[String, ShardokBattleState] = Map.empty
|
||||
private val textStore = new ClientTextStore
|
||||
|
||||
def handle(response: UpdateStreamResponse): Unit = synchronized {
|
||||
response.responseDetails match {
|
||||
@@ -518,7 +522,11 @@ final class ClientState {
|
||||
.mkString(", ")
|
||||
println(s"shardok-update: $summaries")
|
||||
case GameUpdate.GameUpdateDetails.StreamingTextResponse(value) =>
|
||||
println(s"text-update: ${value.llmIdentifier} bytes=${value.newText.length} complete=${value.completed}")
|
||||
val updatedText =
|
||||
textStore.update(value.llmIdentifier, value.newText, value.startingByteCount, value.completed)
|
||||
println(
|
||||
s"text-update: ${value.llmIdentifier} bytes=${value.newText.length} total-bytes=${updatedText.getBytes(StandardCharsets.UTF_8).length} complete=${value.completed}"
|
||||
)
|
||||
case GameUpdate.GameUpdateDetails.ErrorResponse(value) =>
|
||||
println(s"game-error: ${value.errorString}")
|
||||
case GameUpdate.GameUpdateDetails.ShardokBattleResetResponse(value) =>
|
||||
@@ -767,6 +775,7 @@ final class ClientState {
|
||||
.sortBy(entry => (entry.date.map(_.year).getOrElse(0), entry.date.map(_.month).getOrElse(0)))
|
||||
|
||||
def setCurrentGame(gameId: Long): Unit = synchronized {
|
||||
if latestGameId.exists(_ != gameId) then textStore.clear()
|
||||
latestGameId = Some(gameId)
|
||||
}
|
||||
|
||||
@@ -774,6 +783,10 @@ final class ClientState {
|
||||
|
||||
def unfilteredResultCount: Int = synchronized(latestUnfilteredResultCount)
|
||||
|
||||
def streamingTextStatuses: Seq[UpdateStreamRequest.StreamGameRequest.StreamingTextStatus] = synchronized {
|
||||
textStore.statuses
|
||||
}
|
||||
|
||||
def commandToken: Option[Long] = synchronized(latestCommands.map(_.token))
|
||||
|
||||
def marchWarnings(provinceId: Int, command: SelectedCommandProto): Vector[String] = synchronized {
|
||||
@@ -928,10 +941,17 @@ final class ClientState {
|
||||
latestState match {
|
||||
case Some(gs) =>
|
||||
val provinces = gs.provinces.values.toVector.sortBy(_.id).take(20).map { p =>
|
||||
val ruler = p.rulingFactionId.map(_.toString).getOrElse("-")
|
||||
val info =
|
||||
val ruler = p.rulingFactionId.map(_.toString).getOrElse("-")
|
||||
val leader =
|
||||
p.fullInfo
|
||||
.flatMap(_.rulingHeroId)
|
||||
.flatMap(gs.heroes.get)
|
||||
.map(describeHero)
|
||||
.map(name => s" leader=$name")
|
||||
.getOrElse("")
|
||||
val info =
|
||||
p.fullInfo.map(i => s" gold=${i.gold} food=${i.food} heroes=${i.rulingFactionHeroIds.size}").getOrElse("")
|
||||
s"province ${p.id}: ${p.name} ruler=$ruler$info"
|
||||
s"province ${p.id}: ${p.name} ruler=$ruler$leader$info"
|
||||
}
|
||||
val battles =
|
||||
gs.outstandingBattles.map(b => s"battle ${b.shardokGameId.getOrElse("-")} province=${b.defenderProvince}")
|
||||
@@ -947,6 +967,13 @@ final class ClientState {
|
||||
latestState.map(toJson).getOrElse("{}")
|
||||
}
|
||||
|
||||
def describeText(textId: String): String = synchronized {
|
||||
textStore.entry(textId) match {
|
||||
case Some(entry) => s"text $textId complete=${entry.completed}: ${entry.text}"
|
||||
case None => s"No streamed text for $textId."
|
||||
}
|
||||
}
|
||||
|
||||
def describeCommands(rawJson: Boolean): String = synchronized {
|
||||
latestCommands match {
|
||||
case Some(commands) if commands.commandsByProvince.nonEmpty =>
|
||||
@@ -1027,6 +1054,9 @@ final class ClientState {
|
||||
private def commandName(command: AvailableCommandProto): String =
|
||||
command.getClass.getSimpleName.stripSuffix("$")
|
||||
|
||||
private def describeHero(hero: HeroView): String =
|
||||
textStore.entry(hero.nameTextId).map(_.text).filter(_.nonEmpty).getOrElse(s"${hero.nameTextId}#${hero.id}")
|
||||
|
||||
private def commandJson(command: AvailableCommandProto): String =
|
||||
command match {
|
||||
case message: GeneratedMessage => toJson(message)
|
||||
@@ -1069,6 +1099,34 @@ final class ClientState {
|
||||
}
|
||||
}
|
||||
|
||||
final case class ClientTextEntry(text: String, completed: Boolean)
|
||||
|
||||
final class ClientTextStore {
|
||||
private var entries: Map[String, ClientTextEntry] = Map.empty
|
||||
|
||||
def clear(): Unit = entries = Map.empty
|
||||
|
||||
def entry(textId: String): Option[ClientTextEntry] = entries.get(textId)
|
||||
|
||||
def statuses: Seq[UpdateStreamRequest.StreamGameRequest.StreamingTextStatus] =
|
||||
entries.toVector.sortBy(_._1).map {
|
||||
case (textId, entry) =>
|
||||
UpdateStreamRequest.StreamGameRequest.StreamingTextStatus(
|
||||
llmIdentifier = textId,
|
||||
knownByteCount = entry.text.getBytes(StandardCharsets.UTF_8).length,
|
||||
knownComplete = entry.completed
|
||||
)
|
||||
}
|
||||
|
||||
def update(textId: String, newText: String, startingByteCount: Int, completed: Boolean): String = {
|
||||
val currentText = entries.get(textId).map(_.text).getOrElse("")
|
||||
val retainedBytes = currentText.getBytes(StandardCharsets.UTF_8).take(startingByteCount)
|
||||
val updatedText = String(retainedBytes, StandardCharsets.UTF_8) + newText
|
||||
entries += textId -> ClientTextEntry(updatedText, completed)
|
||||
updatedText
|
||||
}
|
||||
}
|
||||
|
||||
final case class ShardokUnitSnapshot(
|
||||
units: Map[Int, UnitView],
|
||||
reserveUnits: Map[Int, UnitView],
|
||||
@@ -1124,6 +1182,7 @@ object Help {
|
||||
| status
|
||||
| state
|
||||
| state-json
|
||||
| text <text-id>
|
||||
| commands
|
||||
| commands-json
|
||||
| shardok-commands
|
||||
|
||||
@@ -8,6 +8,18 @@ import org.scalatest.flatspec.AnyFlatSpec
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
class ClientStateTest extends AnyFlatSpec with Matchers {
|
||||
"ClientTextStore" should "resume UTF-8 streaming text from byte offsets" in {
|
||||
val store = new ClientTextStore
|
||||
|
||||
val _ = store.update("hero-name", "Cæ", startingByteCount = 0, completed = false)
|
||||
val _ = store.update("hero-name", "sar", startingByteCount = 4, completed = true)
|
||||
|
||||
store.entry("hero-name") shouldBe Some(ClientTextEntry("Cæsar", completed = true))
|
||||
store.statuses should have size 1
|
||||
store.statuses.head.knownByteCount shouldBe 6
|
||||
store.statuses.head.knownComplete shouldBe true
|
||||
}
|
||||
|
||||
"ClientState" should "apply Eagle game state diffs from action result updates" in {
|
||||
val state = new ClientState
|
||||
state.handle(
|
||||
|
||||
Reference in New Issue
Block a user