mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-29 10:35:42 +00:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0d35bc409 | ||
|
|
72898e5761 | ||
|
|
a96ac6bc8a | ||
|
|
e30fec13d4 | ||
|
|
ec1b300a1c |
@@ -13,10 +13,19 @@ option objc_class_prefix = "E0G";
|
||||
|
||||
message RunningGame {
|
||||
int64 game_id = 1;
|
||||
// DEPRECATED: Use user_id_to_pid instead. This maps displayName -> factionId
|
||||
// Kept for backward compatibility during migration.
|
||||
map<string, int32> user_to_pid = 2;
|
||||
bool ongoing = 3;
|
||||
// DEPRECATED: Use last_played_by_user_id instead. This maps displayName -> timestamp
|
||||
// Timestamp (millis since epoch) of when each user last took a turn
|
||||
map<string, int64> last_played_by_user = 4;
|
||||
|
||||
// New UUID-based fields (preferred)
|
||||
// Maps userId (UUID) -> factionId. Takes precedence over user_to_pid when present.
|
||||
map<string, int32> user_id_to_pid = 5;
|
||||
// Maps userId (UUID) -> timestamp. Takes precedence over last_played_by_user when present.
|
||||
map<string, int64> last_played_by_user_id = 6;
|
||||
}
|
||||
|
||||
message RunningGames {
|
||||
|
||||
@@ -82,3 +82,12 @@ scala_library(
|
||||
deps = [
|
||||
],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "user_id",
|
||||
srcs = ["UserId.scala"],
|
||||
visibility = [
|
||||
":__subpackages__",
|
||||
"//src/test:__subpackages__",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -128,8 +128,14 @@ object Main {
|
||||
ShardokSecurityConfig(useTls = useTls, authToken = authToken)
|
||||
}
|
||||
|
||||
// Create local UserService (used by GamesManager for user lookups)
|
||||
// This is always created for GamesManager, regardless of external auth configuration
|
||||
val authPersister = LocalFilePersister(SaveDirectory.saveDirectory)
|
||||
val userServiceForGames = new UserServiceImpl(authPersister)
|
||||
|
||||
val gamesManager = newGamesManager(
|
||||
shardokInterfaceAddress = shardokInterfaceAddress,
|
||||
userService = userServiceForGames,
|
||||
securityConfig = shardokSecurityConfig
|
||||
)
|
||||
gamesManager.begin()
|
||||
@@ -153,11 +159,10 @@ object Main {
|
||||
ExternalAuthClient(url)(ExecutionContext.global)
|
||||
}
|
||||
|
||||
// Create local UserService only if NOT using external auth service
|
||||
// Use the same local UserService for auth if NOT using external auth service
|
||||
// (external Go auth service handles user persistence)
|
||||
val userService: Option[UserService] = if authServiceUrl.isEmpty then {
|
||||
val authPersister = LocalFilePersister(SaveDirectory.saveDirectory)
|
||||
Some(new UserServiceImpl(authPersister))
|
||||
Some(userServiceForGames)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -177,6 +182,7 @@ object Main {
|
||||
SeededRandom(random.nextLong()),
|
||||
oauthService,
|
||||
userService,
|
||||
userServiceForGames,
|
||||
externalAuthClient
|
||||
).start
|
||||
SimpleTimedLogger.printLogger.logLine(
|
||||
@@ -192,7 +198,8 @@ object Main {
|
||||
grpcPort: Int,
|
||||
functionalRandom: FunctionalRandom,
|
||||
oauthService: OAuthService,
|
||||
userService: Option[UserService],
|
||||
userServiceForAuth: Option[UserService],
|
||||
userServiceForGames: UserService,
|
||||
externalAuthClient: Option[ExternalAuthClient]
|
||||
): Server = {
|
||||
val serverBuilder = ServerBuilder.forPort(grpcPort)
|
||||
@@ -213,7 +220,7 @@ object Main {
|
||||
serverBuilder.addService(
|
||||
GameAdminGrpc
|
||||
.bindService(
|
||||
GameAdminServiceImpl(gamesManager),
|
||||
GameAdminServiceImpl(gamesManager, userServiceForGames),
|
||||
ExecutionContext.global
|
||||
)
|
||||
)
|
||||
@@ -223,7 +230,7 @@ object Main {
|
||||
serverBuilder.addService(
|
||||
AuthGrpc
|
||||
.bindService(
|
||||
AuthServiceImpl(oauthService, userService, jwt, externalAuthClient),
|
||||
AuthServiceImpl(oauthService, userServiceForAuth, jwt, externalAuthClient),
|
||||
ExecutionContext.global
|
||||
)
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@ package net.eagle0.eagle
|
||||
|
||||
import scala.util.Random
|
||||
|
||||
import net.eagle0.eagle.auth.NoOpUserService
|
||||
import net.eagle0.eagle.service.new_game_creation.GameParametersUtils
|
||||
import net.eagle0.eagle.service.ServerSetupHelpers.newGamesManager
|
||||
|
||||
@@ -13,7 +14,7 @@ object TestAIGame {
|
||||
if args.length > 0 then args.head.toInt
|
||||
else 1
|
||||
|
||||
val gamesManager = newGamesManager("eagle0.net:443")
|
||||
val gamesManager = newGamesManager("eagle0.net:443", new NoOpUserService)
|
||||
gamesManager.begin()
|
||||
|
||||
(1 to gameCount).foreach { _ =>
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package net.eagle0.eagle
|
||||
|
||||
/**
|
||||
* Opaque type for user IDs (UUIDs). This provides compile-time safety to prevent accidentally using a display name
|
||||
* where a user ID is expected, or vice versa.
|
||||
*/
|
||||
opaque type UserId = String
|
||||
|
||||
object UserId:
|
||||
/** Create a UserId from a string (typically a UUID) */
|
||||
def apply(value: String): UserId = value
|
||||
|
||||
/** Create a UserId from a raw string - use when you know the string is a valid user ID */
|
||||
def fromString(value: String): UserId = value
|
||||
|
||||
extension (userId: UserId)
|
||||
/** Get the underlying string value */
|
||||
def value: String = userId
|
||||
@@ -49,8 +49,13 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle:__subpackages__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:user_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:user_id",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:user_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:user_id",
|
||||
"//src/main/scala/net/eagle0/eagle/service/persistence:persister",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -9,6 +9,7 @@ import scala.util.{Success, Using}
|
||||
import com.google.protobuf.timestamp.Timestamp
|
||||
import net.eagle0.eagle.internal.user.user.{OAuthIdentity, User, UserDatabase}
|
||||
import net.eagle0.eagle.service.persistence.Persister
|
||||
import net.eagle0.eagle.UserId
|
||||
|
||||
/** Service for managing user records */
|
||||
trait UserService {
|
||||
@@ -22,13 +23,13 @@ trait UserService {
|
||||
): User
|
||||
|
||||
/** Find user by internal user ID */
|
||||
def findByUserId(userId: String): Option[User]
|
||||
def findByUserId(userId: UserId): Option[User]
|
||||
|
||||
/** Find user by display name (case-insensitive) */
|
||||
def findByDisplayName(displayName: String): Option[User]
|
||||
|
||||
/** Set or update display name */
|
||||
def setDisplayName(userId: String, displayName: String): Either[String, User]
|
||||
def setDisplayName(userId: UserId, displayName: String): Either[String, User]
|
||||
|
||||
/** Check if display name is available */
|
||||
def isDisplayNameAvailable(displayName: String): Boolean
|
||||
@@ -110,8 +111,8 @@ class UserServiceImpl(persister: Persister) extends UserService {
|
||||
}
|
||||
}
|
||||
|
||||
override def findByUserId(userId: String): Option[User] = lock.synchronized {
|
||||
findByUserIdInternal(userId)
|
||||
override def findByUserId(userId: UserId): Option[User] = lock.synchronized {
|
||||
findByUserIdInternal(userId.value)
|
||||
}
|
||||
|
||||
private def findByUserIdInternal(userId: String): Option[User] =
|
||||
@@ -119,10 +120,15 @@ class UserServiceImpl(persister: Persister) extends UserService {
|
||||
|
||||
override def findByDisplayName(displayName: String): Option[User] = lock.synchronized {
|
||||
val lowerName = displayName.toLowerCase
|
||||
database.displayNameIndex.get(lowerName).flatMap(findByUserIdInternal)
|
||||
// Try index first, then fall back to direct search (for users whose
|
||||
// display names were set before the index was fully populated)
|
||||
database.displayNameIndex
|
||||
.get(lowerName)
|
||||
.flatMap(findByUserIdInternal)
|
||||
.orElse(database.users.find(_.displayNameLower == lowerName))
|
||||
}
|
||||
|
||||
override def setDisplayName(userId: String, displayName: String): Either[String, User] =
|
||||
override def setDisplayName(userId: UserId, displayName: String): Either[String, User] =
|
||||
lock.synchronized {
|
||||
// Validate: 3-20 chars, alphanumeric + underscore
|
||||
val validPattern = "^[a-zA-Z0-9_]{3,20}$".r
|
||||
@@ -134,10 +140,10 @@ class UserServiceImpl(persister: Persister) extends UserService {
|
||||
|
||||
// Check uniqueness
|
||||
database.displayNameIndex.get(lowerName) match {
|
||||
case Some(existingId) if existingId != userId =>
|
||||
case Some(existingId) if existingId != userId.value =>
|
||||
Left("Display name already taken")
|
||||
case _ =>
|
||||
findByUserIdInternal(userId) match {
|
||||
case _ =>
|
||||
findByUserIdInternal(userId.value) match {
|
||||
case Some(user) =>
|
||||
// Remove old name from index if exists
|
||||
val newIndex = user.displayNameLower match {
|
||||
@@ -151,8 +157,8 @@ class UserServiceImpl(persister: Persister) extends UserService {
|
||||
)
|
||||
|
||||
database = database.copy(
|
||||
users = database.users.filterNot(_.userId == userId) :+ updated,
|
||||
displayNameIndex = newIndex + (lowerName -> userId)
|
||||
users = database.users.filterNot(_.userId == userId.value) :+ updated,
|
||||
displayNameIndex = newIndex + (lowerName -> userId.value)
|
||||
)
|
||||
saveDatabase()
|
||||
Right(updated)
|
||||
@@ -174,3 +180,22 @@ class UserServiceImpl(persister: Persister) extends UserService {
|
||||
saveDatabase()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A no-op UserService for testing or when user persistence is not available. All lookup methods return None, and
|
||||
* mutation methods do nothing.
|
||||
*/
|
||||
class NoOpUserService extends UserService {
|
||||
override def findOrCreateUser(
|
||||
provider: String,
|
||||
providerUserId: String,
|
||||
email: String,
|
||||
avatarUrl: String
|
||||
): User = User(userId = UUID.randomUUID().toString)
|
||||
|
||||
override def findByUserId(userId: UserId): Option[User] = None
|
||||
override def findByDisplayName(displayName: String): Option[User] = None
|
||||
override def setDisplayName(userId: UserId, displayName: String): Either[String, User] =
|
||||
Left("UserService not available")
|
||||
override def isDisplayNameAvailable(displayName: String): Boolean = true
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import net.eagle0.eagle.auth.{
|
||||
UserService
|
||||
}
|
||||
import net.eagle0.eagle.internal.user.user.User
|
||||
import net.eagle0.eagle.UserId
|
||||
|
||||
/**
|
||||
* gRPC service implementation for OAuth authentication.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package net.eagle0.eagle.service
|
||||
|
||||
import io.grpc.Context
|
||||
import net.eagle0.eagle.UserId
|
||||
|
||||
object AuthorizationUtils {
|
||||
// Username key - set by JWT auth for backwards compatibility with game management code
|
||||
@@ -16,9 +17,9 @@ object AuthorizationUtils {
|
||||
def userName: String = userNameCtxKey.get(Context.current)
|
||||
|
||||
// JWT auth accessors
|
||||
def userId: String = {
|
||||
def userId: UserId = {
|
||||
val id = userIdCtxKey.get(Context.current)
|
||||
if id != null && id.nonEmpty then id else userName
|
||||
UserId(if id != null && id.nonEmpty then id else userName)
|
||||
}
|
||||
|
||||
def displayName: String = {
|
||||
|
||||
@@ -12,6 +12,7 @@ scala_library(
|
||||
":external_auth_client",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:auth_scala_grpc",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:user_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:user_id",
|
||||
"//src/main/scala/net/eagle0/eagle/auth:jwt_service",
|
||||
"//src/main/scala/net/eagle0/eagle/auth:oauth_service",
|
||||
"//src/main/scala/net/eagle0/eagle/auth:user_service",
|
||||
@@ -38,6 +39,8 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/common/llm_integration:open_ai_chat_completions_service_impl",
|
||||
"//src/main/scala/net/eagle0/common/llm_integration:streaming_text_results",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle:user_id",
|
||||
"//src/main/scala/net/eagle0/eagle/auth:user_service",
|
||||
"//src/main/scala/net/eagle0/eagle/library:eagle_client_exception",
|
||||
"//src/main/scala/net/eagle0/eagle/library:engine",
|
||||
"//src/main/scala/net/eagle0/eagle/library:game_history",
|
||||
@@ -105,6 +108,7 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/common:functional_random",
|
||||
"//src/main/scala/net/eagle0/common:simple_timed_logger",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle:user_id",
|
||||
"//src/main/scala/net/eagle0/eagle/library:eagle_client_exception",
|
||||
"//src/main/scala/net/eagle0/eagle/library:engine",
|
||||
"//src/main/scala/net/eagle0/eagle/library:game_history",
|
||||
@@ -141,6 +145,7 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/scala/net/eagle0/eagle:user_id",
|
||||
"@maven//:io_grpc_grpc_api",
|
||||
],
|
||||
)
|
||||
@@ -234,11 +239,14 @@ scala_library(
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:llm_response_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:running_games_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:user_scala_proto",
|
||||
"//src/main/scala/net/eagle0/common:functional_random",
|
||||
"//src/main/scala/net/eagle0/common:proto_parser",
|
||||
"//src/main/scala/net/eagle0/common:simple_timed_logger",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle:user_id",
|
||||
"//src/main/scala/net/eagle0/eagle/ai:ai_client",
|
||||
"//src/main/scala/net/eagle0/eagle/auth:user_service",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:client_text_store",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:client_text_store_impl",
|
||||
@@ -542,6 +550,7 @@ scala_library(
|
||||
":games_manager",
|
||||
"//src/main/protobuf/net/eagle0/common:shardok_internal_interface_scala_grpc",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/auth:user_service",
|
||||
"//src/main/scala/net/eagle0/eagle/library:engine",
|
||||
"//src/main/scala/net/eagle0/eagle/service:game_persister_creation",
|
||||
"//src/main/scala/net/eagle0/eagle/service:local_game_persister_creation",
|
||||
|
||||
@@ -17,44 +17,45 @@ import net.eagle0.eagle.library.settings.MaxSupportedPlayers
|
||||
import net.eagle0.eagle.library.EagleClientException
|
||||
import net.eagle0.eagle.service.new_game_creation.GameParametersUtils
|
||||
import net.eagle0.eagle.service.CustomBattleManager.CreateCustomBattleResponse
|
||||
import net.eagle0.eagle.UserId
|
||||
|
||||
class EagleServiceImpl(
|
||||
gamesManager: GamesManager,
|
||||
customBattleManager: CustomBattleManager,
|
||||
var functionalRandom: FunctionalRandom
|
||||
) extends Eagle {
|
||||
private var lobbyUsers = Map[String, SyncResponseObserver]()
|
||||
private var lobbyUsers = Map[UserId, SyncResponseObserver]()
|
||||
|
||||
private def lockAndDoWithUserName[T](f: String => T): Future[T] = {
|
||||
val un = AuthorizationUtils.userName
|
||||
private def lockAndDoWithUserId[T](f: UserId => T): Future[T] = {
|
||||
val uid = AuthorizationUtils.userId
|
||||
Future.fromTry(
|
||||
this.synchronized {
|
||||
Try(f(un))
|
||||
Try(f(uid))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private def lockAndUpdateGamesWith[T](f: String => T): Future[T] =
|
||||
lockAndDoWithUserName { un =>
|
||||
val t = f(un)
|
||||
private def lockAndUpdateGamesWith[T](f: UserId => T): Future[T] =
|
||||
lockAndDoWithUserId { uid =>
|
||||
val t = f(uid)
|
||||
lobbyUsers.keys.foreach(k => lockedSendLobbyUpdate(k))
|
||||
t
|
||||
}
|
||||
|
||||
def joinGame(request: JoinGameRequest): Future[JoinGameResponse] =
|
||||
lockAndUpdateGamesWith(un =>
|
||||
lockAndUpdateGamesWith(uid =>
|
||||
gamesManager
|
||||
.joinGame(
|
||||
userName = un,
|
||||
userId = uid,
|
||||
gameId = request.gameId,
|
||||
desiredLeaderTextId = request.desiredLeaderTextId
|
||||
)
|
||||
)
|
||||
|
||||
def dropGame(request: DropGameRequest): Future[DropGameResponse] =
|
||||
lockAndUpdateGamesWith(un =>
|
||||
lockAndUpdateGamesWith(uid =>
|
||||
gamesManager.dropGame(
|
||||
userName = un,
|
||||
userId = uid,
|
||||
gameId = request.gameId
|
||||
)
|
||||
)
|
||||
@@ -62,10 +63,10 @@ class EagleServiceImpl(
|
||||
def createGame(
|
||||
request: CreateGameRequest
|
||||
): Future[CreateGameResponse] =
|
||||
lockAndUpdateGamesWith(un =>
|
||||
lockAndUpdateGamesWith(uid =>
|
||||
gamesManager.createGameForUser(
|
||||
expandedGameParameters = GameParametersUtils.defaultExpandedGameParameters,
|
||||
userName = un,
|
||||
userId = uid,
|
||||
desiredLeaderTextId = request.desiredLeaderTextId,
|
||||
maxHumanPlayers = request.humanPlayerCount,
|
||||
totalPlayers = request.totalPlayerCount
|
||||
@@ -93,7 +94,7 @@ class EagleServiceImpl(
|
||||
def postCommand(
|
||||
request: PostCommandRequest
|
||||
): Future[PostCommandResponse] =
|
||||
lockAndDoWithUserName { un =>
|
||||
lockAndDoWithUserId { uid =>
|
||||
request.command match {
|
||||
case ShardokCommand(
|
||||
shardokGameId,
|
||||
@@ -105,7 +106,7 @@ class EagleServiceImpl(
|
||||
) =>
|
||||
if gamesManager.isEagleGame(request.gameId) then
|
||||
gamesManager.postShardokCommand(
|
||||
name = un,
|
||||
userId = uid,
|
||||
eagleGameId = request.gameId,
|
||||
shardokGameId = shardokGameId,
|
||||
shardokPlayerId = playerId,
|
||||
@@ -115,7 +116,7 @@ class EagleServiceImpl(
|
||||
)
|
||||
else
|
||||
customBattleManager.postShardokCommand(
|
||||
name = un,
|
||||
name = uid.value,
|
||||
eagleGameId = request.gameId,
|
||||
shardokGameId = shardokGameId,
|
||||
shardokPlayerId = playerId,
|
||||
@@ -130,7 +131,7 @@ class EagleServiceImpl(
|
||||
_ /* unknownFieldSet */
|
||||
) =>
|
||||
gamesManager.postCommand(
|
||||
name = un,
|
||||
userId = uid,
|
||||
gameId = request.gameId,
|
||||
selectedProvinceId = provinceId,
|
||||
command = eagleCommand,
|
||||
@@ -145,7 +146,7 @@ class EagleServiceImpl(
|
||||
) =>
|
||||
if gamesManager.isEagleGame(request.gameId) then
|
||||
gamesManager.postPlacementCommands(
|
||||
name = un,
|
||||
userId = uid,
|
||||
eagleGameId = request.gameId,
|
||||
shardokGameId = gameId,
|
||||
shardokPlayerId = playerId,
|
||||
@@ -154,7 +155,7 @@ class EagleServiceImpl(
|
||||
)
|
||||
else
|
||||
customBattleManager.postPlacementCommands(
|
||||
name = un,
|
||||
name = uid.value,
|
||||
eagleGameId = request.gameId,
|
||||
shardokGameId = gameId,
|
||||
shardokPlayerId = playerId,
|
||||
@@ -186,9 +187,9 @@ class EagleServiceImpl(
|
||||
case UpdateStreamRequest.RequestDetails.HeartbeatRequest(value) =>
|
||||
handleHeartbeat(value, responseObserver)
|
||||
case UpdateStreamRequest.RequestDetails.EnterLobbyRequest(_) =>
|
||||
lockAndDoWithUserName { userName =>
|
||||
lobbyUsers = lobbyUsers + (userName -> responseObserver)
|
||||
lockedSendLobbyUpdate(userName)
|
||||
lockAndDoWithUserId { userId =>
|
||||
lobbyUsers = lobbyUsers + (userId -> responseObserver)
|
||||
lockedSendLobbyUpdate(userId)
|
||||
}
|
||||
()
|
||||
|
||||
@@ -347,8 +348,8 @@ class EagleServiceImpl(
|
||||
}
|
||||
|
||||
override def onCompleted(): Unit = {
|
||||
lockAndDoWithUserName { userName =>
|
||||
lobbyUsers = lobbyUsers - userName
|
||||
lockAndDoWithUserId { userId =>
|
||||
lobbyUsers = lobbyUsers - userId
|
||||
}
|
||||
responseObserver.onCompleted()
|
||||
()
|
||||
@@ -358,14 +359,15 @@ class EagleServiceImpl(
|
||||
def streamUpdates(
|
||||
responseObserver: StreamObserver[UpdateStreamResponse]
|
||||
): StreamObserver[UpdateStreamRequest] = new SyncStreamObserver(
|
||||
new SyncResponseObserver(AuthorizationUtils.userName, responseObserver)
|
||||
// Note: userName is kept for logging purposes (displayName is more readable in logs)
|
||||
new SyncResponseObserver(AuthorizationUtils.displayName, responseObserver)
|
||||
)
|
||||
|
||||
private def handleHeartbeat(
|
||||
request: HeartbeatRequest,
|
||||
responseObserver: SyncResponseObserver
|
||||
): Unit = {
|
||||
lockAndDoWithUserName { userName =>
|
||||
lockAndDoWithUserId { userId =>
|
||||
// Synchronize on gamesManager to ensure heartbeat response is sent AFTER
|
||||
// any pending game updates have been queued. Without this, there's a race
|
||||
// condition where the heartbeat response can interleave with game updates,
|
||||
@@ -373,14 +375,14 @@ class EagleServiceImpl(
|
||||
// This particularly affects slower/remote connections (e.g., Windows clients).
|
||||
gamesManager.synchronized {
|
||||
println(
|
||||
s"got heartbeat from $userName. Client timestamp ${request.clientTimestamp}, server timestamp ${System.currentTimeMillis()}"
|
||||
s"got heartbeat from $userId. Client timestamp ${request.clientTimestamp}, server timestamp ${System.currentTimeMillis()}"
|
||||
)
|
||||
|
||||
// Verify sync status for each game the client reports
|
||||
val gameSyncResults = request.gameSyncStatuses.flatMap { clientGameStatus =>
|
||||
gamesManager.gameControllerInfos.get(clientGameStatus.gameId).flatMap { controllerInfo =>
|
||||
val controller = controllerInfo.controller
|
||||
controller.userNameToFactionId.get(userName).map { factionId =>
|
||||
controller.userIdToFactionId.get(userId).map { factionId =>
|
||||
val serverUnfilteredCount = controller.engine.history.count
|
||||
|
||||
// Check Shardok sync status
|
||||
@@ -432,7 +434,7 @@ class EagleServiceImpl(
|
||||
if shardokDetails.nonEmpty then ", " + shardokDetails.mkString(", ") else ""
|
||||
}"
|
||||
}
|
||||
println(s"[HEARTBEAT] Detected sync mismatches for $userName: ${mismatchDetails.mkString("; ")}")
|
||||
println(s"[HEARTBEAT] Detected sync mismatches for $userId: ${mismatchDetails.mkString("; ")}")
|
||||
}
|
||||
|
||||
val _ = responseObserver.onNext(
|
||||
@@ -454,11 +456,11 @@ class EagleServiceImpl(
|
||||
request: StreamGameRequest,
|
||||
responseObserver: SyncResponseObserver
|
||||
): Unit = {
|
||||
lockAndDoWithUserName { userName =>
|
||||
lockAndDoWithUserId { userId =>
|
||||
try {
|
||||
if gamesManager.isEagleGame(request.gameId) then
|
||||
gamesManager.streamUpdates(
|
||||
name = userName,
|
||||
userId = userId,
|
||||
gameId = request.gameId,
|
||||
knownResultCount = request.unfilteredResultCount,
|
||||
knownShardokResultCounts = request.shardokViewStatuses.toVector,
|
||||
@@ -467,7 +469,7 @@ class EagleServiceImpl(
|
||||
)
|
||||
else
|
||||
customBattleManager.streamUpdates(
|
||||
name = userName,
|
||||
name = userId.value,
|
||||
knownShardokResultCounts = request.shardokViewStatuses.toVector,
|
||||
responseObserver = responseObserver
|
||||
)
|
||||
@@ -537,17 +539,17 @@ class EagleServiceImpl(
|
||||
()
|
||||
}
|
||||
|
||||
private def lockedSendLobbyUpdate(userName: String): Unit =
|
||||
private def lockedSendLobbyUpdate(userId: UserId): Unit =
|
||||
try
|
||||
lobbyUsers
|
||||
.get(userName)
|
||||
.get(userId)
|
||||
.foreach(
|
||||
_.onNext(
|
||||
UpdateStreamResponse(
|
||||
responseDetails = UpdateStreamResponse.ResponseDetails.LobbyResponse(
|
||||
LobbyResponse(
|
||||
runningGames = gamesManager
|
||||
.gamesFor(userName)
|
||||
.gamesFor(userId)
|
||||
.map(gamePlayerInfos =>
|
||||
GameInfo(
|
||||
gameId = gamePlayerInfos.gameId,
|
||||
@@ -556,8 +558,8 @@ class EagleServiceImpl(
|
||||
lastPlayedTimestampMillis = gamePlayerInfos.lastPlayedMillis.getOrElse(0L)
|
||||
)
|
||||
),
|
||||
availableGames = gamesManager.availableGamesFor(userName),
|
||||
waitingGames = gamesManager.waitingGamesFor(userName),
|
||||
availableGames = gamesManager.availableGamesFor(userId),
|
||||
waitingGames = gamesManager.waitingGamesFor(userId),
|
||||
newGameOptions = Some(
|
||||
NewGameOptions(
|
||||
maxSupportedPlayers = MaxSupportedPlayers.intValue,
|
||||
|
||||
@@ -21,18 +21,21 @@ import net.eagle0.common.SimpleTimedLogger
|
||||
import net.eagle0.eagle.*
|
||||
import net.eagle0.eagle.admin.game_admin.*
|
||||
import net.eagle0.eagle.admin.game_admin.GameAdminGrpc.GameAdmin
|
||||
import net.eagle0.eagle.auth.UserService
|
||||
import net.eagle0.eagle.library.actions.applier.ActionResultWithResultingState
|
||||
import net.eagle0.eagle.library.settings.loaders.SettingsLoader
|
||||
import net.eagle0.eagle.library.EagleClientException
|
||||
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
|
||||
import net.eagle0.eagle.model.proto_converters.ActionResultProtoConverter
|
||||
import net.eagle0.eagle.service.persistence.SaveDirectory
|
||||
import net.eagle0.eagle.UserId
|
||||
import org.json4s.*
|
||||
import org.json4s.native.Serialization.writePretty
|
||||
import scalapb.json4s.JsonFormat
|
||||
|
||||
class GameAdminServiceImpl(
|
||||
gamesManager: GamesManager
|
||||
gamesManager: GamesManager,
|
||||
userService: UserService
|
||||
) extends GameAdmin {
|
||||
|
||||
override def getRunningGames(
|
||||
@@ -48,16 +51,18 @@ class GameAdminServiceImpl(
|
||||
|
||||
val players = gameState.factions.map {
|
||||
case (factionId, faction) =>
|
||||
val leader = gameState.heroes.get(faction.factionHeadId)
|
||||
val userName = controller.userNameToFactionId.find { case (_, fid) => fid == factionId }
|
||||
val leader = gameState.heroes.get(faction.factionHeadId)
|
||||
// Find userId for this faction and derive displayName
|
||||
val userIdOpt = controller.userIdToFactionId.find { case (_, fid) => fid == factionId }
|
||||
.map(_._1)
|
||||
val displayName = userIdOpt.flatMap(uid => userService.findByUserId(uid).map(_.displayName))
|
||||
|
||||
RunningGamePlayerInfo(
|
||||
factionId = factionId,
|
||||
factionName = Option(faction.name).filter(_.nonEmpty).getOrElse("[MISSING FACTION NAME]"),
|
||||
leaderName = leader.map(_.nameTextId).flatMap(Option(_)).filter(_.nonEmpty).getOrElse("[MISSING LEADER]"),
|
||||
isHuman = userName.isDefined,
|
||||
userName = userName.getOrElse("")
|
||||
isHuman = userIdOpt.isDefined,
|
||||
userName = displayName.getOrElse("")
|
||||
)
|
||||
}.toSeq
|
||||
|
||||
@@ -69,14 +74,19 @@ class GameAdminServiceImpl(
|
||||
runStatus = gameState.runStatus.toString
|
||||
)
|
||||
} else {
|
||||
val players = summary.userToPid.map {
|
||||
case (userName, factionId) =>
|
||||
// For unloaded games, userIdToPid is always in userId format (migrated at startup)
|
||||
val players = summary.userIdToPid.map {
|
||||
case (userId, factionId) =>
|
||||
val displayName = userService
|
||||
.findByUserId(userId)
|
||||
.map(_.displayName)
|
||||
.getOrElse("[Unknown user]")
|
||||
RunningGamePlayerInfo(
|
||||
factionId = factionId,
|
||||
factionName = "[Not loaded]",
|
||||
leaderName = "[Not loaded]",
|
||||
isHuman = true,
|
||||
userName = userName
|
||||
userName = displayName
|
||||
)
|
||||
}.toSeq
|
||||
|
||||
@@ -389,11 +399,22 @@ class GameAdminServiceImpl(
|
||||
override def convertAiToHuman(
|
||||
request: ConvertAiToHumanRequest
|
||||
): Future[ConvertAiToHumanResponse] = Future.successful {
|
||||
gamesManager.convertAiToHuman(
|
||||
gameId = request.gameId,
|
||||
factionId = request.factionId,
|
||||
newUserName = request.newUsername
|
||||
)
|
||||
// Accept either userId or displayName as input
|
||||
// If it looks like a UUID, use it directly; otherwise look up by displayName
|
||||
val resolvedUserId = resolveToUserId(request.newUsername)
|
||||
resolvedUserId match {
|
||||
case Some(userId) =>
|
||||
gamesManager.convertAiToHuman(
|
||||
gameId = request.gameId,
|
||||
factionId = request.factionId,
|
||||
newUserId = userId
|
||||
)
|
||||
case None =>
|
||||
ConvertAiToHumanResponse(
|
||||
result = ConvertAiToHumanResponse.Result.INVALID_USERNAME,
|
||||
errorMessage = s"User '${request.newUsername}' not found"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override def convertHumanToAi(
|
||||
@@ -408,13 +429,39 @@ class GameAdminServiceImpl(
|
||||
override def reassignFaction(
|
||||
request: ReassignFactionRequest
|
||||
): Future[ReassignFactionResponse] = Future.successful {
|
||||
gamesManager.reassignFaction(
|
||||
gameId = request.gameId,
|
||||
factionId = request.factionId,
|
||||
newUserName = request.newUsername
|
||||
)
|
||||
// Accept either userId or displayName as input
|
||||
val resolvedUserId = resolveToUserId(request.newUsername)
|
||||
resolvedUserId match {
|
||||
case Some(userId) =>
|
||||
gamesManager.reassignFaction(
|
||||
gameId = request.gameId,
|
||||
factionId = request.factionId,
|
||||
newUserId = userId
|
||||
)
|
||||
case None =>
|
||||
ReassignFactionResponse(
|
||||
result = ReassignFactionResponse.Result.INVALID_USERNAME,
|
||||
errorMessage = s"User '${request.newUsername}' not found"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves admin input to a UserId. Accepts either:
|
||||
* - A userId (UUID format) directly
|
||||
* - A displayName (looks up the userId via UserService)
|
||||
*
|
||||
* This is for convenience in the admin UI - actual storage is always by userId.
|
||||
*/
|
||||
private def resolveToUserId(input: String): Option[UserId] =
|
||||
// Try as userId first (check if user exists with this ID)
|
||||
userService.findByUserId(UserId(input)) match {
|
||||
case Some(user) => Some(UserId(user.userId))
|
||||
case None =>
|
||||
// Not a valid userId - try as displayName
|
||||
userService.findByDisplayName(input).map(u => UserId(u.userId))
|
||||
}
|
||||
|
||||
override def rewindGame(
|
||||
request: RewindGameRequest
|
||||
): Future[RewindGameResponse] = Future.successful {
|
||||
@@ -543,6 +590,6 @@ class GameAdminServiceImpl(
|
||||
}
|
||||
|
||||
object GameAdminServiceImpl {
|
||||
def apply(gamesManager: GamesManager): GameAdminServiceImpl =
|
||||
new GameAdminServiceImpl(gamesManager)
|
||||
def apply(gamesManager: GamesManager, userService: UserService): GameAdminServiceImpl =
|
||||
new GameAdminServiceImpl(gamesManager, userService)
|
||||
}
|
||||
|
||||
@@ -5,13 +5,14 @@ import scala.util.{Failure, Random, Success, Try}
|
||||
import io.sentry.Sentry
|
||||
import net.eagle0.common.{ProtoParser, SeededRandom, SimpleTimedLogger}
|
||||
import net.eagle0.common.shardok_internal_interface.ShardokInternalInterfaceGrpc.ShardokInternalInterfaceStub
|
||||
import net.eagle0.eagle.{FactionId, GameId, ProvinceId, ShardokGameId}
|
||||
import net.eagle0.eagle.{FactionId, GameId, ProvinceId, ShardokGameId, UserId}
|
||||
import net.eagle0.eagle.admin.game_admin.{ConvertAiToHumanResponse, ConvertHumanToAiResponse, ReassignFactionResponse}
|
||||
import net.eagle0.eagle.api.eagle.*
|
||||
import net.eagle0.eagle.api.eagle.JoinGameResult.INVALID_OPTIONS_RESULT
|
||||
import net.eagle0.eagle.api.eagle.ShardokPlacementCommands.ShardokPlacementCommand
|
||||
import net.eagle0.eagle.api.eagle.UpdateStreamRequest.StreamGameRequest.{ShardokViewStatus, StreamingTextStatus}
|
||||
import net.eagle0.eagle.api.selected_command.SelectedCommand
|
||||
import net.eagle0.eagle.auth.UserService
|
||||
import net.eagle0.eagle.client_text.{
|
||||
ClientTextStore,
|
||||
ClientTextStoreImpl,
|
||||
@@ -23,6 +24,7 @@ import net.eagle0.eagle.internal.action_result.ActionResult
|
||||
import net.eagle0.eagle.internal.hero.Hero
|
||||
import net.eagle0.eagle.internal.running_games.{RunningGame, RunningGames}
|
||||
import net.eagle0.eagle.internal.shardok_battle as sb_proto
|
||||
import net.eagle0.eagle.internal.user.user.User
|
||||
import net.eagle0.eagle.library.*
|
||||
import net.eagle0.eagle.library.settings.loaders.{BattalionTypeLoader, SettingsLoader}
|
||||
import net.eagle0.eagle.library.settings.MaxSupportedPlayers
|
||||
@@ -61,7 +63,7 @@ case class GamePlayerInfo(
|
||||
)
|
||||
|
||||
case class WaitingGamePlayerInfo(
|
||||
user: String,
|
||||
userId: UserId,
|
||||
leaderNameTextId: String
|
||||
)
|
||||
|
||||
@@ -96,7 +98,7 @@ case class ControllerInfo(
|
||||
var controller: GameController,
|
||||
isAiGame: Boolean,
|
||||
maxPlayers: Int,
|
||||
var lastPlayedByUser: Map[String, Long] = Map.empty
|
||||
var lastPlayedByUserId: Map[UserId, Long] = Map.empty
|
||||
)
|
||||
|
||||
object GamesManager {
|
||||
@@ -165,7 +167,8 @@ object GamesManager {
|
||||
persister: Persister,
|
||||
gameCreation: NewGameCreation,
|
||||
gamePersisterCreation: GamePersisterCreation,
|
||||
hexMaps: Map[String, HexMap]
|
||||
hexMaps: Map[String, HexMap],
|
||||
userService: UserService
|
||||
): GamesManager =
|
||||
// Lazy loading: Don't load any games at startup.
|
||||
// Games are loaded on-demand when a user subscribes via streamUpdates().
|
||||
@@ -184,7 +187,8 @@ object GamesManager {
|
||||
gameCreation = gameCreation,
|
||||
gamePersisterCreation = gamePersisterCreation,
|
||||
pregeneratedClientText = pregeneratedTexts,
|
||||
hexMaps = hexMaps
|
||||
hexMaps = hexMaps,
|
||||
userService = userService
|
||||
)
|
||||
|
||||
private def gameHistoryFromFiles(
|
||||
@@ -206,7 +210,7 @@ object GamesManager {
|
||||
gameId: GameId,
|
||||
gamePersister: Persister,
|
||||
history: FullGameHistory,
|
||||
userToFid: Map[String, FactionId],
|
||||
userToFid: Map[UserId, FactionId],
|
||||
pregeneratedHeroes: Vector[HeroWithName]
|
||||
): GameController = {
|
||||
val loadedTextStore = ClientTextStoreImpl
|
||||
@@ -260,7 +264,7 @@ object GamesManager {
|
||||
GameController.advancedFrom(
|
||||
engine = preEngine,
|
||||
fullHistory = history,
|
||||
userNameToFactionId = userToFid,
|
||||
userIdToFactionId = userToFid,
|
||||
aiFids = aiFids.toVector,
|
||||
clientTextStore = clientTextStore
|
||||
) match {
|
||||
@@ -283,7 +287,7 @@ object GamesManager {
|
||||
gameId = runningGame.gameId,
|
||||
gamePersister = persister,
|
||||
history = gh,
|
||||
userToFid = runningGame.userToPid,
|
||||
userToFid = runningGame.userIdToPid.map { case (id, fid) => UserId(id) -> fid }.toMap,
|
||||
pregeneratedHeroes = GameParametersUtils.allPregeneratedHeroes
|
||||
)
|
||||
)
|
||||
@@ -306,7 +310,8 @@ class GamesManager(
|
||||
gameCreation: NewGameCreation,
|
||||
gamePersisterCreation: GamePersisterCreation,
|
||||
pregeneratedClientText: PregeneratedClientTextStore,
|
||||
val hexMaps: Map[String, HexMap]
|
||||
val hexMaps: Map[String, HexMap],
|
||||
val userService: UserService
|
||||
) extends BattleUpdateReceiver
|
||||
with LlmUpdateReceiver {
|
||||
|
||||
@@ -334,7 +339,7 @@ class GamesManager(
|
||||
def getHexMap(mapName: String): HexMap = hexMaps(mapName)
|
||||
|
||||
def begin(): Unit = this.synchronized {
|
||||
// Just connect to Shardok - games are loaded lazily on first subscribe
|
||||
// Connect to Shardok
|
||||
shardokClient.connect()
|
||||
}
|
||||
|
||||
@@ -543,8 +548,22 @@ class GamesManager(
|
||||
case Some(runningGame) =>
|
||||
val gamePersister = gamePersisterCreation.persisterForGame(gameId)
|
||||
|
||||
// Use userId-based mapping (migration happens at startup)
|
||||
val userIdToFid: Map[UserId, FactionId] =
|
||||
runningGame.userIdToPid.map { case (id, fid) => UserId(id) -> fid }.toMap
|
||||
val lastPlayedByUserId: Map[UserId, Long] =
|
||||
runningGame.lastPlayedByUserId.map { case (id, ts) => UserId(id) -> ts }.toMap
|
||||
|
||||
val controllerOpt = SimpleTimedLogger.printLogger.withStopwatch(s"loading game history ${gameId.toHexString}") {
|
||||
GamesManager.gameController(runningGame, gamePersister)
|
||||
GamesManager.gameHistoryFromFiles(gameId, gamePersister).map { history =>
|
||||
GamesManager.gameController(
|
||||
gameId = gameId,
|
||||
gamePersister = gamePersister,
|
||||
history = history,
|
||||
userToFid = userIdToFid,
|
||||
pregeneratedHeroes = GameParametersUtils.allPregeneratedHeroes
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
controllerOpt match {
|
||||
@@ -559,7 +578,7 @@ class GamesManager(
|
||||
controller = controller,
|
||||
isAiGame = false,
|
||||
maxPlayers = 2,
|
||||
lastPlayedByUser = runningGame.lastPlayedByUser.toMap
|
||||
lastPlayedByUserId = lastPlayedByUserId
|
||||
)
|
||||
|
||||
// Initialize the game (handle incomplete/unrequested texts, resume battles)
|
||||
@@ -583,7 +602,7 @@ class GamesManager(
|
||||
controller = initializedController,
|
||||
isAiGame = false,
|
||||
maxPlayers = 2,
|
||||
lastPlayedByUser = runningGame.lastPlayedByUser.toMap
|
||||
lastPlayedByUserId = lastPlayedByUserId
|
||||
)
|
||||
|
||||
// Resume any outstanding battles
|
||||
@@ -725,14 +744,14 @@ class GamesManager(
|
||||
}
|
||||
}
|
||||
|
||||
def gamesFor(name: String): Vector[GamePlayerInfo] = this.synchronized {
|
||||
def gamesFor(userId: UserId): Vector[GamePlayerInfo] = this.synchronized {
|
||||
// Read games.e0es fresh from storage to find games for this user.
|
||||
// This handles the case where a game was created just before deployment
|
||||
// and the client lost connection before getting the game ID.
|
||||
val runningGames = readRunningGamesFromStorage()
|
||||
val userGameIds = runningGames
|
||||
.filter(_.userToPid.contains(name))
|
||||
.map(_.gameId)
|
||||
val userGameIds = runningGames.filter { rg =>
|
||||
rg.userIdToPid.contains(userId.value)
|
||||
}.map(_.gameId)
|
||||
|
||||
// Load any games that aren't already in memory
|
||||
userGameIds.foreach(ensureGameLoaded)
|
||||
@@ -740,8 +759,8 @@ class GamesManager(
|
||||
// Now return info for all loaded games
|
||||
gameControllerInfos.flatMap {
|
||||
case (gameId, c) =>
|
||||
c.controller.userNameToFactionId
|
||||
.get(name)
|
||||
c.controller.userIdToFactionId
|
||||
.get(userId)
|
||||
.map { fid =>
|
||||
val resolveName: String => Option[String] = textId =>
|
||||
c.controller.clientTextStore.getText(textId) match {
|
||||
@@ -759,17 +778,17 @@ class GamesManager(
|
||||
),
|
||||
resolveName
|
||||
),
|
||||
lastPlayedMillis = c.lastPlayedByUser.get(name)
|
||||
lastPlayedMillis = c.lastPlayedByUserId.get(userId)
|
||||
)
|
||||
}
|
||||
}.toVector
|
||||
}
|
||||
|
||||
private def availableOrNewWaitingGames(
|
||||
userName: String
|
||||
userId: UserId
|
||||
): Vector[GameAwaitingPlayers] =
|
||||
gamesAwaitingPlayers
|
||||
.filterNot(gap => gap.existingHumanPlayers.map(_.user).contains(userName))
|
||||
.filterNot(gap => gap.existingHumanPlayers.map(_.userId).contains(userId))
|
||||
|
||||
private def availableLeader(hero: HeroT, resolveName: String => Option[String]): AvailableLeader =
|
||||
AvailableLeader(
|
||||
@@ -793,9 +812,9 @@ class GamesManager(
|
||||
expandedGameParameters.greatPeopleHeroProtos
|
||||
.map(availableLeaderFromProto)
|
||||
|
||||
def availableGamesFor(userName: String): Vector[AvailableNewGame] =
|
||||
def availableGamesFor(userId: UserId): Vector[AvailableNewGame] =
|
||||
this.synchronized {
|
||||
availableOrNewWaitingGames(userName).map { gap =>
|
||||
availableOrNewWaitingGames(userId).map { gap =>
|
||||
AvailableNewGame(
|
||||
gameId = gap.gameId,
|
||||
gameType = EagleGameType.GAME_TYPE_MULTI_PLAYER,
|
||||
@@ -812,15 +831,15 @@ class GamesManager(
|
||||
}
|
||||
}
|
||||
|
||||
def waitingGamesFor(userName: String): Vector[WaitingGame] =
|
||||
def waitingGamesFor(userId: UserId): Vector[WaitingGame] =
|
||||
this.synchronized {
|
||||
gamesAwaitingPlayers
|
||||
.filter(gap => gap.existingHumanPlayers.map(_.user).contains(userName))
|
||||
.filter(gap => gap.existingHumanPlayers.map(_.userId).contains(userId))
|
||||
.map { gap =>
|
||||
WaitingGame(
|
||||
gameId = gap.gameId,
|
||||
selectedLeader = gap.existingHumanPlayers
|
||||
.find(_.user == userName)
|
||||
.find(_.userId == userId)
|
||||
.map(_.leaderNameTextId)
|
||||
.map(leaderNameTextId =>
|
||||
GameParametersUtils.allPregeneratedHeroProtos
|
||||
@@ -849,11 +868,16 @@ class GamesManager(
|
||||
// Add in-memory games (these have the freshest state)
|
||||
val loadedGames = gameControllerInfos.map {
|
||||
case (gameId, gc) =>
|
||||
// Convert UserId to String for proto serialization
|
||||
val userIdToFid = gc.controller.userIdToFactionId.map { case (uid, fid) => uid.value -> fid }
|
||||
val lastPlayedByUserId = gc.lastPlayedByUserId.map { case (uid, ts) => uid.value -> ts }
|
||||
|
||||
RunningGame(
|
||||
gameId = gameId,
|
||||
userToPid = gc.controller.userNameToFactionId,
|
||||
ongoing = true,
|
||||
lastPlayedByUser = gc.lastPlayedByUser
|
||||
// Use new userId-based format only
|
||||
userIdToPid = userIdToFid,
|
||||
lastPlayedByUserId = lastPlayedByUserId
|
||||
)
|
||||
}.toVector
|
||||
|
||||
@@ -888,20 +912,22 @@ class GamesManager(
|
||||
s"resolveBattle called for ${battleProto.shardokGameId}"
|
||||
)
|
||||
this.synchronized {
|
||||
// Derive factionId -> displayName from userIdToFactionId for Shardok UI
|
||||
val playerToUserMap = gameControllerInfos(battle.eagleGameId).controller.userIdToFactionId.flatMap {
|
||||
case (userId, fid) =>
|
||||
userService.findByUserId(userId).map(u => fid -> u.displayName)
|
||||
}
|
||||
shardokClient
|
||||
.resolveBattle(
|
||||
battle = battleProto,
|
||||
eagleGameState = GameStateConverter.toProto(gameState),
|
||||
playerToUserMap = gameControllerInfos(
|
||||
battle.eagleGameId
|
||||
).controller.userNameToFactionId
|
||||
.map(_.swap)
|
||||
playerToUserMap = playerToUserMap
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
def postCommand(
|
||||
name: String,
|
||||
userId: UserId,
|
||||
gameId: GameId,
|
||||
selectedProvinceId: ProvinceId,
|
||||
command: SelectedCommand,
|
||||
@@ -916,7 +942,7 @@ class GamesManager(
|
||||
val _ = synchronizedHandlePostResults(
|
||||
gameControllerInfos(gameId).controller
|
||||
.postHumanCommand(
|
||||
userName = name,
|
||||
userId = userId,
|
||||
selectedProvinceId = selectedProvinceId,
|
||||
command = command,
|
||||
token = token
|
||||
@@ -924,12 +950,12 @@ class GamesManager(
|
||||
)
|
||||
// Record last played time for this user
|
||||
val ci = gameControllerInfos(gameId)
|
||||
ci.lastPlayedByUser = ci.lastPlayedByUser.updated(name, System.currentTimeMillis())
|
||||
ci.lastPlayedByUserId = ci.lastPlayedByUserId.updated(userId, System.currentTimeMillis())
|
||||
save()
|
||||
}
|
||||
|
||||
def postShardokCommand(
|
||||
name: String,
|
||||
userId: UserId,
|
||||
eagleGameId: GameId,
|
||||
shardokGameId: String,
|
||||
shardokPlayerId: Int,
|
||||
@@ -942,7 +968,7 @@ class GamesManager(
|
||||
ensureGameLoaded(eagleGameId),
|
||||
s"No game found with ID ${eagleGameId.toHexString}"
|
||||
)
|
||||
// FIXME: need to verify username match
|
||||
// FIXME: need to verify userId match
|
||||
SimpleTimedLogger
|
||||
.fileLogger(eagleGameId, "timings")
|
||||
.withStopwatch("postShardokCommand") {
|
||||
@@ -954,17 +980,17 @@ class GamesManager(
|
||||
index = index,
|
||||
roll = roll,
|
||||
eagleFactionId = gameControllerInfos(eagleGameId).controller
|
||||
.userNameToFactionId(name)
|
||||
.userIdToFactionId(userId)
|
||||
)
|
||||
}
|
||||
// Record last played time for this user
|
||||
val ci = gameControllerInfos(eagleGameId)
|
||||
ci.lastPlayedByUser = ci.lastPlayedByUser.updated(name, System.currentTimeMillis())
|
||||
ci.lastPlayedByUserId = ci.lastPlayedByUserId.updated(userId, System.currentTimeMillis())
|
||||
save()
|
||||
}
|
||||
|
||||
def postPlacementCommands(
|
||||
name: String,
|
||||
userId: UserId,
|
||||
eagleGameId: GameId,
|
||||
shardokGameId: String,
|
||||
shardokPlayerId: Int,
|
||||
@@ -976,7 +1002,7 @@ class GamesManager(
|
||||
ensureGameLoaded(eagleGameId),
|
||||
s"No game found with ID ${eagleGameId.toHexString}"
|
||||
)
|
||||
// FIXME: need to verify username match
|
||||
// FIXME: need to verify userId match
|
||||
shardokClient.postPlacementCommands(
|
||||
eagleGameId = eagleGameId,
|
||||
shardokGameId = shardokGameId,
|
||||
@@ -985,7 +1011,7 @@ class GamesManager(
|
||||
PlacementCommand(unitId = pc.unitId, row = pc.row, column = pc.column)
|
||||
},
|
||||
token = shardokToken,
|
||||
eagleFactionId = gameControllerInfos(eagleGameId).controller.userNameToFactionId(name)
|
||||
eagleFactionId = gameControllerInfos(eagleGameId).controller.userIdToFactionId(userId)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1035,7 +1061,7 @@ class GamesManager(
|
||||
}
|
||||
|
||||
def streamUpdates(
|
||||
name: String,
|
||||
userId: UserId,
|
||||
gameId: GameId,
|
||||
knownResultCount: Int,
|
||||
knownShardokResultCounts: Vector[ShardokViewStatus],
|
||||
@@ -1054,13 +1080,13 @@ class GamesManager(
|
||||
val loadEnd = System.currentTimeMillis()
|
||||
if loadEnd - loadStart > 500 then
|
||||
SimpleTimedLogger.printLogger.logLine(
|
||||
s"[TIMING] ensureGameLoaded for $name took ${loadEnd - loadStart}ms"
|
||||
s"[TIMING] ensureGameLoaded for $userId took ${loadEnd - loadStart}ms"
|
||||
)
|
||||
|
||||
val filterStart = System.currentTimeMillis()
|
||||
gameControllerInfos(gameId).controller = gameControllerInfos(gameId).controller
|
||||
.streamUpdates(
|
||||
userName = name,
|
||||
userId = userId,
|
||||
knownResultCount = knownResultCount,
|
||||
knownShardokResultCounts = knownShardokResultCounts,
|
||||
responseObserver = responseObserver,
|
||||
@@ -1069,27 +1095,27 @@ class GamesManager(
|
||||
val filterEnd = System.currentTimeMillis()
|
||||
if filterEnd - filterStart > 500 then
|
||||
SimpleTimedLogger.printLogger.logLine(
|
||||
s"[TIMING] streamUpdates filtering for $name took ${filterEnd - filterStart}ms"
|
||||
s"[TIMING] streamUpdates filtering for $userId took ${filterEnd - filterStart}ms"
|
||||
)
|
||||
|
||||
val totalTime = System.currentTimeMillis() - streamStart
|
||||
if totalTime > 500 then
|
||||
SimpleTimedLogger.printLogger.logLine(
|
||||
s"[TIMING] Total streamUpdates for $name took ${totalTime}ms (load: ${loadEnd - loadStart}ms, filter: ${filterEnd - filterStart}ms)"
|
||||
s"[TIMING] Total streamUpdates for $userId took ${totalTime}ms (load: ${loadEnd - loadStart}ms, filter: ${filterEnd - filterStart}ms)"
|
||||
)
|
||||
}
|
||||
|
||||
def stopStreaming(
|
||||
name: String,
|
||||
userId: UserId,
|
||||
gameId: GameId
|
||||
): Unit =
|
||||
this.synchronized {
|
||||
gameControllerInfos(gameId).controller = gameControllerInfos(gameId).controller
|
||||
.stopStreaming(userName = name)
|
||||
.stopStreaming(userId = userId)
|
||||
}
|
||||
|
||||
def joinGame(
|
||||
userName: String,
|
||||
userId: UserId,
|
||||
gameId: GameId,
|
||||
desiredLeaderTextId: String
|
||||
): JoinGameResponse = this.synchronized {
|
||||
@@ -1098,7 +1124,7 @@ class GamesManager(
|
||||
if gap.availableNameTextIds.contains(desiredLeaderTextId) then {
|
||||
val newGap = gap.withNewWaitingPlayerInfo(
|
||||
WaitingGamePlayerInfo(
|
||||
user = userName,
|
||||
userId = userId,
|
||||
leaderNameTextId = desiredLeaderTextId
|
||||
)
|
||||
)
|
||||
@@ -1146,7 +1172,7 @@ class GamesManager(
|
||||
|
||||
def createGameForUser(
|
||||
expandedGameParameters: ExpandedGameParameters,
|
||||
userName: String,
|
||||
userId: UserId,
|
||||
desiredLeaderTextId: String,
|
||||
maxHumanPlayers: Int,
|
||||
totalPlayers: Int
|
||||
@@ -1166,7 +1192,7 @@ class GamesManager(
|
||||
)
|
||||
|
||||
val result = joinGame(
|
||||
userName = userName,
|
||||
userId = userId,
|
||||
gameId = newGameId,
|
||||
desiredLeaderTextId = desiredLeaderTextId
|
||||
) match {
|
||||
@@ -1242,7 +1268,7 @@ class GamesManager(
|
||||
|
||||
val userToFid = humanPlayers
|
||||
.map(wgpi =>
|
||||
wgpi.user -> initialEar.engine.currentState.heroes.values
|
||||
wgpi.userId -> initialEar.engine.currentState.heroes.values
|
||||
.find(_.nameTextId == wgpi.leaderNameTextId)
|
||||
.get
|
||||
.factionId
|
||||
@@ -1279,7 +1305,7 @@ class GamesManager(
|
||||
GameController.advancedFrom(
|
||||
engine = initialEar.engine,
|
||||
fullHistory = GamesManager.extractFullHistory(initialEar.engine),
|
||||
userNameToFactionId = userToFid,
|
||||
userIdToFactionId = userToFid,
|
||||
aiFids = initialEar.engine.factionIds
|
||||
.filterNot(userToFid.values.toVector.contains),
|
||||
clientTextStore = textStoreWithPregenerated
|
||||
@@ -1318,14 +1344,14 @@ class GamesManager(
|
||||
}
|
||||
|
||||
def dropGame(
|
||||
userName: String,
|
||||
userId: UserId,
|
||||
gameId: GameId
|
||||
): DropGameResponse = this.synchronized {
|
||||
// Check waiting games first
|
||||
gamesAwaitingPlayers.find(_.gameId == gameId) match {
|
||||
case Some(gap) =>
|
||||
if gap.existingHumanPlayers.exists(_.user == userName) then {
|
||||
val remainingPlayers = gap.existingHumanPlayers.filterNot(_.user == userName)
|
||||
if gap.existingHumanPlayers.exists(_.userId == userId) then {
|
||||
val remainingPlayers = gap.existingHumanPlayers.filterNot(_.userId == userId)
|
||||
if remainingPlayers.isEmpty then
|
||||
// Remove the waiting game entirely
|
||||
gamesAwaitingPlayers = gamesAwaitingPlayers.filterNot(_.gameId == gameId)
|
||||
@@ -1352,7 +1378,7 @@ class GamesManager(
|
||||
gameControllerInfos.get(gameId) match {
|
||||
case Some(controllerInfo) =>
|
||||
val controller = controllerInfo.controller
|
||||
if !controller.userNameToFactionId.contains(userName) then
|
||||
if !controller.userIdToFactionId.contains(userId) then
|
||||
DropGameResponse(
|
||||
result = DropGameResult.USER_NOT_IN_GAME_DROP_GAME_RESULT,
|
||||
gameId = gameId
|
||||
@@ -1374,7 +1400,7 @@ class GamesManager(
|
||||
)
|
||||
} else {
|
||||
// Other humans remain - convert to AI
|
||||
val updatedController = controller.dropUser(userName)
|
||||
val updatedController = controller.dropUser(userId)
|
||||
gameControllerInfos(gameId).controller = updatedController
|
||||
|
||||
// Trigger AI commands if it's now their turn
|
||||
@@ -1410,14 +1436,14 @@ class GamesManager(
|
||||
def convertAiToHuman(
|
||||
gameId: GameId,
|
||||
factionId: FactionId,
|
||||
newUserName: String
|
||||
newUserId: UserId
|
||||
): ConvertAiToHumanResponse = this.synchronized {
|
||||
import ConvertAiToHumanResponse.Result
|
||||
|
||||
if newUserName.isBlank then
|
||||
if newUserId.value.isBlank then
|
||||
ConvertAiToHumanResponse(
|
||||
result = Result.INVALID_USERNAME,
|
||||
errorMessage = "Username cannot be blank"
|
||||
errorMessage = "User ID cannot be blank"
|
||||
)
|
||||
else
|
||||
gameControllerInfos.get(gameId) match {
|
||||
@@ -1436,18 +1462,18 @@ class GamesManager(
|
||||
result = Result.FACTION_NOT_FOUND,
|
||||
errorMessage = s"No faction found with ID $factionId"
|
||||
)
|
||||
else if controller.userNameToFactionId.values.toSet.contains(factionId) then
|
||||
else if controller.userIdToFactionId.values.toSet.contains(factionId) then
|
||||
ConvertAiToHumanResponse(
|
||||
result = Result.FACTION_ALREADY_HUMAN,
|
||||
errorMessage = s"Faction $factionId is already human-controlled"
|
||||
)
|
||||
else if controller.userNameToFactionId.contains(newUserName) then
|
||||
else if controller.userIdToFactionId.contains(newUserId) then
|
||||
ConvertAiToHumanResponse(
|
||||
result = Result.USERNAME_ALREADY_IN_USE,
|
||||
errorMessage = s"Username '$newUserName' is already controlling another faction"
|
||||
errorMessage = s"User '$newUserId' is already controlling another faction"
|
||||
)
|
||||
else {
|
||||
val updatedController = controller.promoteAiToHuman(factionId, newUserName)
|
||||
val updatedController = controller.promoteAiToHuman(factionId, newUserId)
|
||||
gameControllerInfos(gameId).controller = updatedController
|
||||
save()
|
||||
ConvertAiToHumanResponse(result = Result.SUCCESS)
|
||||
@@ -1511,14 +1537,14 @@ class GamesManager(
|
||||
def reassignFaction(
|
||||
gameId: GameId,
|
||||
factionId: FactionId,
|
||||
newUserName: String
|
||||
newUserId: UserId
|
||||
): ReassignFactionResponse = this.synchronized {
|
||||
import ReassignFactionResponse.Result
|
||||
|
||||
if newUserName.isBlank then
|
||||
if newUserId.value.isBlank then
|
||||
ReassignFactionResponse(
|
||||
result = Result.INVALID_USERNAME,
|
||||
errorMessage = "Username cannot be blank"
|
||||
errorMessage = "User ID cannot be blank"
|
||||
)
|
||||
else
|
||||
gameControllerInfos.get(gameId) match {
|
||||
@@ -1537,25 +1563,25 @@ class GamesManager(
|
||||
result = Result.FACTION_NOT_FOUND,
|
||||
errorMessage = s"No faction found with ID $factionId"
|
||||
)
|
||||
else if controller.userNameToFactionId.contains(newUserName) then
|
||||
else if controller.userIdToFactionId.contains(newUserId) then
|
||||
ReassignFactionResponse(
|
||||
result = Result.USERNAME_ALREADY_IN_USE,
|
||||
errorMessage = s"Username '$newUserName' is already controlling another faction"
|
||||
errorMessage = s"User '$newUserId' is already controlling another faction"
|
||||
)
|
||||
else
|
||||
controller.reassignFaction(factionId, newUserName) match {
|
||||
controller.reassignFaction(factionId, newUserId) match {
|
||||
case None =>
|
||||
ReassignFactionResponse(
|
||||
result = Result.FACTION_IS_AI,
|
||||
errorMessage = s"Faction $factionId is AI-controlled, not human"
|
||||
)
|
||||
|
||||
case Some((previousUsername, updatedController)) =>
|
||||
case Some((previousUserId, updatedController)) =>
|
||||
gameControllerInfos(gameId).controller = updatedController
|
||||
save()
|
||||
ReassignFactionResponse(
|
||||
result = Result.SUCCESS,
|
||||
previousUsername = previousUsername
|
||||
previousUsername = previousUserId.value
|
||||
)
|
||||
}
|
||||
end if
|
||||
@@ -1652,7 +1678,7 @@ class GamesManager(
|
||||
val isLoaded = gameControllerInfos.contains(rg.gameId)
|
||||
RunningGameSummary(
|
||||
gameId = rg.gameId,
|
||||
userToPid = rg.userToPid,
|
||||
userIdToPid = rg.userIdToPid.map { case (id, fid) => UserId(id) -> fid }.toMap,
|
||||
isLoaded = isLoaded
|
||||
)
|
||||
}
|
||||
@@ -1671,6 +1697,6 @@ class GamesManager(
|
||||
|
||||
case class RunningGameSummary(
|
||||
gameId: GameId,
|
||||
userToPid: Map[String, FactionId],
|
||||
userIdToPid: Map[UserId, FactionId],
|
||||
isLoaded: Boolean
|
||||
)
|
||||
|
||||
@@ -22,6 +22,7 @@ import io.grpc.{
|
||||
}
|
||||
import net.eagle0.common.shardok_internal_interface.ShardokInternalInterfaceGrpc
|
||||
import net.eagle0.common.shardok_internal_interface.ShardokInternalInterfaceGrpc.ShardokInternalInterfaceStub
|
||||
import net.eagle0.eagle.auth.UserService
|
||||
import net.eagle0.eagle.service.new_game_creation.FixedNewGameCreation
|
||||
import net.eagle0.eagle.service.persistence.{CompoundPersister, LocalFilePersister, S3Persister, S3Utils, SaveDirectory}
|
||||
import net.eagle0.eagle.service.persistence.credentials.S3Credentials
|
||||
@@ -56,6 +57,7 @@ object ServerSetupHelpers {
|
||||
|
||||
def newGamesManager(
|
||||
shardokInterfaceAddress: String,
|
||||
userService: UserService,
|
||||
securityConfig: ShardokSecurityConfig = ShardokSecurityConfig()
|
||||
): GamesManager = {
|
||||
implicit val ec = scala.concurrent.ExecutionContext.global
|
||||
@@ -102,7 +104,8 @@ object ServerSetupHelpers {
|
||||
persister = CompoundPersister(persisters),
|
||||
gameCreation = FixedNewGameCreation,
|
||||
gamePersisterCreation = LocalGamePersisterCreation,
|
||||
hexMaps = hexMaps
|
||||
hexMaps = hexMaps,
|
||||
userService = userService
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/common:functional_random",
|
||||
"//src/main/scala/net/eagle0/common:simple_timed_logger",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle:user_id",
|
||||
"//src/main/scala/net/eagle0/eagle/ai:ai_client",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:client_text_store",
|
||||
|
||||
@@ -8,7 +8,7 @@ import scala.collection.immutable.SortedMap
|
||||
|
||||
import io.grpc.{Status, StatusRuntimeException}
|
||||
import net.eagle0.common.{RandomState, SeededRandom, SimpleTimedLogger}
|
||||
import net.eagle0.eagle.{FactionId, GameId, ProvinceId}
|
||||
import net.eagle0.eagle.{FactionId, GameId, ProvinceId, UserId}
|
||||
import net.eagle0.eagle.ai.AIClient
|
||||
import net.eagle0.eagle.api.command.AvailableCommands
|
||||
import net.eagle0.eagle.api.eagle.ServerGameStatus
|
||||
@@ -64,7 +64,7 @@ object GameController {
|
||||
*/
|
||||
private def allHumansDefeated(gcwpr: GameControllerWithPostResults): Boolean = {
|
||||
val gc = gcwpr.gameController
|
||||
val humanFactionIds = gc.userNameToFactionId.values.toSet
|
||||
val humanFactionIds = gc.userIdToFactionId.values.toSet
|
||||
val gs = gc.engine.currentState
|
||||
|
||||
// If there are no human players, they're trivially "not defeated" (nothing to defeat)
|
||||
@@ -301,7 +301,7 @@ object GameController {
|
||||
def advancedFrom(
|
||||
engine: Engine,
|
||||
fullHistory: FullGameHistory,
|
||||
userNameToFactionId: Map[String, FactionId],
|
||||
userIdToFactionId: Map[UserId, FactionId],
|
||||
aiFids: Vector[FactionId],
|
||||
clientTextStore: ClientTextStore
|
||||
): GameControllerWithPostResults =
|
||||
@@ -310,7 +310,7 @@ object GameController {
|
||||
GameController(
|
||||
engine = engine,
|
||||
fullHistory = fullHistory,
|
||||
userNameToFactionId = userNameToFactionId,
|
||||
userIdToFactionId = userIdToFactionId,
|
||||
humanClients = Vector(),
|
||||
aiClients = Vector(),
|
||||
clientTextStore = clientTextStore
|
||||
@@ -324,10 +324,12 @@ object GameController {
|
||||
|
||||
// Controls a single Eagle game. Clients interact with the Engine through a GameClient interface, but this manages
|
||||
// new clients joining or creating AI clients as necessary.
|
||||
// Note: userIdToFactionId maps user IDs (UUIDs from the auth system) to faction IDs.
|
||||
// For display names, use UserService to look up the displayName from the userId.
|
||||
final case class GameController(
|
||||
engine: Engine,
|
||||
fullHistory: FullGameHistory,
|
||||
userNameToFactionId: Map[String, FactionId],
|
||||
userIdToFactionId: Map[UserId, FactionId],
|
||||
humanClients: Vector[HumanPlayerClientConnectionState],
|
||||
aiClients: Vector[RandomState[AIClient]],
|
||||
clientTextStore: ClientTextStore
|
||||
@@ -531,7 +533,7 @@ final case class GameController(
|
||||
GameController(
|
||||
engine = this.engine,
|
||||
fullHistory = this.fullHistory,
|
||||
userNameToFactionId = this.userNameToFactionId,
|
||||
userIdToFactionId = this.userIdToFactionId,
|
||||
humanClients = this.humanClients
|
||||
.filterNot(_.factionId == client.factionId) :+ client,
|
||||
aiClients = this.aiClients,
|
||||
@@ -542,7 +544,7 @@ final case class GameController(
|
||||
GameController(
|
||||
engine = this.engine,
|
||||
fullHistory = this.fullHistory,
|
||||
userNameToFactionId = this.userNameToFactionId,
|
||||
userIdToFactionId = this.userIdToFactionId,
|
||||
humanClients = this.humanClients,
|
||||
aiClients = this.aiClients :+ RandomState(
|
||||
client,
|
||||
@@ -621,7 +623,7 @@ final case class GameController(
|
||||
gameController = GameController(
|
||||
engine = engineAndResults.engine,
|
||||
fullHistory = newFullHistory,
|
||||
userNameToFactionId = userNameToFactionId.filter {
|
||||
userIdToFactionId = userIdToFactionId.filter {
|
||||
case (_, fid) =>
|
||||
engineAndResults.engine.factionIds.contains(fid)
|
||||
},
|
||||
@@ -657,14 +659,14 @@ final case class GameController(
|
||||
}
|
||||
|
||||
def streamUpdates(
|
||||
userName: String,
|
||||
userId: UserId,
|
||||
knownResultCount: Int,
|
||||
knownShardokResultCounts: Vector[ShardokViewStatus],
|
||||
streamingTextStatuses: Vector[StreamingTextStatus],
|
||||
responseObserver: SyncResponseObserver
|
||||
): GameController =
|
||||
userNameToFactionId
|
||||
.get(userName)
|
||||
userIdToFactionId
|
||||
.get(userId)
|
||||
.map { fid =>
|
||||
this.withHumanClient(
|
||||
clientTextStore.completeTexts.values
|
||||
@@ -708,25 +710,25 @@ final case class GameController(
|
||||
}
|
||||
.get
|
||||
|
||||
def stopStreaming(userName: String): GameController =
|
||||
def stopStreaming(userId: UserId): GameController =
|
||||
GameController(
|
||||
engine = this.engine,
|
||||
fullHistory = this.fullHistory,
|
||||
userNameToFactionId = this.userNameToFactionId,
|
||||
humanClients = this.humanClients.filterNot(hc => userNameToFactionId.get(userName).contains(hc.factionId)),
|
||||
userIdToFactionId = this.userIdToFactionId,
|
||||
humanClients = this.humanClients.filterNot(hc => userIdToFactionId.get(userId).contains(hc.factionId)),
|
||||
aiClients = this.aiClients,
|
||||
clientTextStore = this.clientTextStore
|
||||
)
|
||||
|
||||
def dropUser(userName: String): GameController =
|
||||
userNameToFactionId.get(userName) match {
|
||||
def dropUser(userId: UserId): GameController =
|
||||
userIdToFactionId.get(userId) match {
|
||||
case Some(factionId) =>
|
||||
val newAiClient = RandomState(
|
||||
AIClient(gameId = gameId, factionId = factionId),
|
||||
SeededRandom(GameController.aiSeed(engine.gameId, factionId))
|
||||
)
|
||||
copy(
|
||||
userNameToFactionId = userNameToFactionId - userName,
|
||||
userIdToFactionId = userIdToFactionId - userId,
|
||||
humanClients = humanClients.filterNot(_.factionId == factionId),
|
||||
aiClients = aiClients :+ newAiClient
|
||||
)
|
||||
@@ -734,36 +736,36 @@ final case class GameController(
|
||||
}
|
||||
|
||||
/** Convert an AI-controlled faction to human control */
|
||||
def promoteAiToHuman(factionId: FactionId, userName: String): GameController =
|
||||
def promoteAiToHuman(factionId: FactionId, userId: UserId): GameController =
|
||||
copy(
|
||||
userNameToFactionId = userNameToFactionId + (userName -> factionId),
|
||||
userIdToFactionId = userIdToFactionId + (userId -> factionId),
|
||||
aiClients = aiClients.filterNot(_.newValue.factionId == factionId)
|
||||
)
|
||||
|
||||
/** Convert a human-controlled faction to AI control. Returns None if faction is not human-controlled. */
|
||||
def demoteHumanToAi(factionId: FactionId): Option[GameController] =
|
||||
userNameToFactionId.find(_._2 == factionId).map {
|
||||
case (userName, _) =>
|
||||
dropUser(userName)
|
||||
userIdToFactionId.find(_._2 == factionId).map {
|
||||
case (userId, _) =>
|
||||
dropUser(userId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reassign a human-controlled faction to a new username. Returns previous username and new controller, or None if
|
||||
* faction is not human-controlled.
|
||||
* Reassign a human-controlled faction to a new userId. Returns previous userId and new controller, or None if faction
|
||||
* is not human-controlled.
|
||||
*/
|
||||
def reassignFaction(factionId: FactionId, newUserName: String): Option[(String, GameController)] =
|
||||
userNameToFactionId.find(_._2 == factionId).map {
|
||||
case (oldUserName, _) =>
|
||||
def reassignFaction(factionId: FactionId, newUserId: UserId): Option[(UserId, GameController)] =
|
||||
userIdToFactionId.find(_._2 == factionId).map {
|
||||
case (oldUserId, _) =>
|
||||
(
|
||||
oldUserName,
|
||||
oldUserId,
|
||||
copy(
|
||||
userNameToFactionId = userNameToFactionId - oldUserName + (newUserName -> factionId),
|
||||
userIdToFactionId = userIdToFactionId - oldUserId + (newUserId -> factionId),
|
||||
humanClients = humanClients.filterNot(_.factionId == factionId)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
def remainingHumanCount: Int = userNameToFactionId.size
|
||||
def remainingHumanCount: Int = userIdToFactionId.size
|
||||
|
||||
/**
|
||||
* Rewinds the game to a previous state. This sends a full state resync to all connected human clients (so they don't
|
||||
@@ -798,7 +800,7 @@ final case class GameController(
|
||||
val newController = GameController(
|
||||
engine = rewoundEngine,
|
||||
fullHistory = extractFullHistory(rewoundEngine),
|
||||
userNameToFactionId = this.userNameToFactionId,
|
||||
userIdToFactionId = this.userIdToFactionId,
|
||||
humanClients = resyncedClients,
|
||||
aiClients = this.aiClients.map { rsClient =>
|
||||
// Reset AI clients to use the rewound state
|
||||
@@ -814,26 +816,26 @@ final case class GameController(
|
||||
}
|
||||
|
||||
def postHumanCommand(
|
||||
userName: String,
|
||||
userId: UserId,
|
||||
selectedProvinceId: ProvinceId,
|
||||
command: SelectedCommand,
|
||||
token: Long
|
||||
): GameControllerWithPostResults = {
|
||||
val client = humanClients.find(hpccs =>
|
||||
userNameToFactionId
|
||||
.get(userName)
|
||||
userIdToFactionId
|
||||
.get(userId)
|
||||
.contains(hpccs.factionId)
|
||||
)
|
||||
|
||||
clientRequire(
|
||||
client.isDefined,
|
||||
s"No client for $userName"
|
||||
s"No client for $userId"
|
||||
)
|
||||
|
||||
val expectedToken = engine.tokenForFaction(client.get.factionId)
|
||||
clientRequire(
|
||||
expectedToken == token,
|
||||
s"Token mismatch for $userName (expected $expectedToken, got $token)"
|
||||
s"Token mismatch for $userId (expected $expectedToken, got $token)"
|
||||
)
|
||||
|
||||
GameController.performAiCommands(
|
||||
@@ -865,7 +867,7 @@ final case class GameController(
|
||||
GameController(
|
||||
engine = this.engine,
|
||||
fullHistory = this.fullHistory,
|
||||
userNameToFactionId = this.userNameToFactionId,
|
||||
userIdToFactionId = this.userIdToFactionId,
|
||||
humanClients = this.humanClients,
|
||||
aiClients = newClients,
|
||||
clientTextStore = this.clientTextStore
|
||||
|
||||
@@ -58,7 +58,10 @@ scala_test(
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:eagle_scala_grpc",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:llm_response_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:user_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle:user_id",
|
||||
"//src/main/scala/net/eagle0/eagle/auth:user_service",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:pregenerated_text_store",
|
||||
"//src/main/scala/net/eagle0/eagle/library:engine",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
|
||||
|
||||
@@ -3,7 +3,7 @@ package net.eagle0.eagle.service
|
||||
import java.io.File
|
||||
|
||||
import scala.collection.immutable.SortedMap
|
||||
import scala.util.{Failure, Random}
|
||||
import scala.util.{Failure, Random, Success}
|
||||
|
||||
import io.grpc.Channel
|
||||
import net.eagle0.common.shardok_internal_interface.ShardokInternalInterfaceGrpc
|
||||
@@ -11,8 +11,10 @@ import net.eagle0.common.ProtoMatchers
|
||||
import net.eagle0.eagle.api.eagle.{CreateGameResponse, DropGameResponse, JoinGameResponse}
|
||||
import net.eagle0.eagle.api.eagle.DropGameResult.*
|
||||
import net.eagle0.eagle.api.eagle.JoinGameResult.*
|
||||
import net.eagle0.eagle.auth.{NoOpUserService, UserService}
|
||||
import net.eagle0.eagle.client_text.PregeneratedClientTextStore
|
||||
import net.eagle0.eagle.internal.game_parameters.GameParameters
|
||||
import net.eagle0.eagle.internal.user.user.User
|
||||
import net.eagle0.eagle.library.{Engine, EngineAndResults}
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.run_status.RunStatus
|
||||
@@ -25,6 +27,7 @@ import net.eagle0.eagle.service.new_game_creation.{
|
||||
NewGameCreation
|
||||
}
|
||||
import net.eagle0.eagle.service.persistence.Persister
|
||||
import net.eagle0.eagle.UserId
|
||||
import net.eagle0.util.hero_generation.LoadedHero
|
||||
import org.scalamock.scalatest.MockFactory
|
||||
import org.scalatest.flatspec.AnyFlatSpec
|
||||
@@ -114,7 +117,8 @@ class GamesManagerTest extends AnyFlatSpec with MockFactory with Matchers with B
|
||||
persister = mockPersister,
|
||||
gameCreation = mockGameCreation,
|
||||
gamePersisterCreation = mockGamePersisterCreation,
|
||||
hexMaps = Map()
|
||||
hexMaps = Map(),
|
||||
userService = new NoOpUserService
|
||||
)
|
||||
|
||||
// Helper: allow games.e0es reads to return empty (for save() merging)
|
||||
@@ -208,7 +212,7 @@ class GamesManagerTest extends AnyFlatSpec with MockFactory with Matchers with B
|
||||
val gamesManager = defaultGamesManager
|
||||
|
||||
gamesManager.joinGame(
|
||||
"bob",
|
||||
UserId("bob"),
|
||||
gameId = gameId,
|
||||
desiredLeaderTextId = greatPerson2.greatPerson.hero.nameTextId
|
||||
)
|
||||
@@ -216,7 +220,7 @@ class GamesManagerTest extends AnyFlatSpec with MockFactory with Matchers with B
|
||||
mockRandom.throwOnNextLong()
|
||||
|
||||
gamesManager.joinGame(
|
||||
"alice",
|
||||
UserId("alice"),
|
||||
gameId = gameId,
|
||||
desiredLeaderTextId = greatPerson1.greatPerson.hero.nameTextId
|
||||
) shouldBe
|
||||
@@ -229,7 +233,7 @@ class GamesManagerTest extends AnyFlatSpec with MockFactory with Matchers with B
|
||||
(mockPersister.save _).expects(*, *).returns(true).anyNumberOfTimes()
|
||||
|
||||
gamesManager.joinGame(
|
||||
"bob",
|
||||
UserId("bob"),
|
||||
gameId = gameId,
|
||||
desiredLeaderTextId = greatPerson2.greatPerson.hero.nameTextId
|
||||
)
|
||||
@@ -237,7 +241,7 @@ class GamesManagerTest extends AnyFlatSpec with MockFactory with Matchers with B
|
||||
mockRandom.throwOnNextLong()
|
||||
|
||||
gamesManager.joinGame(
|
||||
"alice",
|
||||
UserId("alice"),
|
||||
gameId = gameId,
|
||||
desiredLeaderTextId = greatPerson2.greatPerson.hero.nameTextId
|
||||
) shouldBe
|
||||
@@ -331,7 +335,7 @@ class GamesManagerTest extends AnyFlatSpec with MockFactory with Matchers with B
|
||||
val gamesManager = defaultGamesManager
|
||||
|
||||
gamesManager.joinGame(
|
||||
"bob",
|
||||
UserId("bob"),
|
||||
gameId = gameId,
|
||||
desiredLeaderTextId = greatPerson1.greatPerson.hero.nameTextId
|
||||
)
|
||||
@@ -339,13 +343,13 @@ class GamesManagerTest extends AnyFlatSpec with MockFactory with Matchers with B
|
||||
mockRandom.throwOnNextLong()
|
||||
|
||||
gamesManager.joinGame(
|
||||
"alice",
|
||||
UserId("alice"),
|
||||
gameId = gameId,
|
||||
desiredLeaderTextId = greatPerson2.greatPerson.hero.nameTextId
|
||||
)
|
||||
|
||||
gamesManager.joinGame(
|
||||
"fred",
|
||||
UserId("fred"),
|
||||
gameId = gameId,
|
||||
desiredLeaderTextId = greatPerson3.greatPerson.hero.nameTextId
|
||||
) shouldBe
|
||||
@@ -360,7 +364,7 @@ class GamesManagerTest extends AnyFlatSpec with MockFactory with Matchers with B
|
||||
val gamesManager = defaultGamesManager
|
||||
gamesManager.createGameForUser(
|
||||
expandedGameParameters = expandedGameParameters,
|
||||
userName = "charlie",
|
||||
userId = UserId("charlie"),
|
||||
desiredLeaderTextId = greatPerson2.greatPerson.hero.nameTextId,
|
||||
maxHumanPlayers = 3,
|
||||
totalPlayers = 8
|
||||
@@ -373,7 +377,7 @@ class GamesManagerTest extends AnyFlatSpec with MockFactory with Matchers with B
|
||||
gameId = 0xfeedface,
|
||||
existingHumanPlayers = Vector(
|
||||
WaitingGamePlayerInfo(
|
||||
user = "charlie",
|
||||
userId = UserId("charlie"),
|
||||
leaderNameTextId = greatPerson2.greatPerson.hero.nameTextId
|
||||
)
|
||||
),
|
||||
@@ -397,7 +401,7 @@ class GamesManagerTest extends AnyFlatSpec with MockFactory with Matchers with B
|
||||
val gamesManager = defaultGamesManager
|
||||
gamesManager.createGameForUser(
|
||||
expandedGameParameters = expandedGameParameters,
|
||||
userName = "charlie",
|
||||
userId = UserId("charlie"),
|
||||
desiredLeaderTextId = "franklin_id",
|
||||
maxHumanPlayers = 3,
|
||||
totalPlayers = 8
|
||||
@@ -414,7 +418,7 @@ class GamesManagerTest extends AnyFlatSpec with MockFactory with Matchers with B
|
||||
val gamesManager = defaultGamesManager
|
||||
gamesManager.createGameForUser(
|
||||
expandedGameParameters = expandedGameParameters,
|
||||
userName = "charlie",
|
||||
userId = UserId("charlie"),
|
||||
desiredLeaderTextId = greatPerson2.greatPerson.hero.nameTextId,
|
||||
maxHumanPlayers = 9,
|
||||
totalPlayers = 8
|
||||
@@ -428,7 +432,7 @@ class GamesManagerTest extends AnyFlatSpec with MockFactory with Matchers with B
|
||||
val gamesManager = defaultGamesManager
|
||||
gamesManager.createGameForUser(
|
||||
expandedGameParameters = expandedGameParameters,
|
||||
userName = "charlie",
|
||||
userId = UserId("charlie"),
|
||||
desiredLeaderTextId = greatPerson2.greatPerson.hero.nameTextId,
|
||||
maxHumanPlayers = 8,
|
||||
totalPlayers = 16
|
||||
@@ -444,7 +448,7 @@ class GamesManagerTest extends AnyFlatSpec with MockFactory with Matchers with B
|
||||
val gamesManager = defaultGamesManager
|
||||
gamesManager.createGameForUser(
|
||||
expandedGameParameters = expandedGameParameters,
|
||||
userName = "charlie",
|
||||
userId = UserId("charlie"),
|
||||
desiredLeaderTextId = "Sir not-appearing-in-this-film_id",
|
||||
maxHumanPlayers = 3,
|
||||
totalPlayers = 8
|
||||
@@ -468,7 +472,7 @@ class GamesManagerTest extends AnyFlatSpec with MockFactory with Matchers with B
|
||||
expandedGameParameters = expandedGameParameters,
|
||||
gameId = gameId,
|
||||
existingHumanPlayers = Vector(
|
||||
WaitingGamePlayerInfo(user = "alice", leaderNameTextId = greatPerson1.greatPerson.hero.nameTextId)
|
||||
WaitingGamePlayerInfo(userId = UserId("alice"), leaderNameTextId = greatPerson1.greatPerson.hero.nameTextId)
|
||||
),
|
||||
humanPlayerCount = 2,
|
||||
totalPlayerCount = 4,
|
||||
@@ -479,10 +483,11 @@ class GamesManagerTest extends AnyFlatSpec with MockFactory with Matchers with B
|
||||
persister = mockPersister,
|
||||
gameCreation = mockGameCreation,
|
||||
gamePersisterCreation = mockGamePersisterCreation,
|
||||
hexMaps = Map()
|
||||
hexMaps = Map(),
|
||||
userService = new NoOpUserService
|
||||
)
|
||||
|
||||
gamesManager.dropGame(userName = "alice", gameId = gameId) shouldBe
|
||||
gamesManager.dropGame(userId = UserId("alice"), gameId = gameId) shouldBe
|
||||
DropGameResponse(result = SUCCESS_DROP_GAME_RESULT, gameId = gameId)
|
||||
|
||||
gamesManager.gamesAwaitingPlayers shouldBe empty
|
||||
@@ -501,8 +506,11 @@ class GamesManagerTest extends AnyFlatSpec with MockFactory with Matchers with B
|
||||
expandedGameParameters = expandedGameParameters,
|
||||
gameId = gameId,
|
||||
existingHumanPlayers = Vector(
|
||||
WaitingGamePlayerInfo(user = "alice", leaderNameTextId = greatPerson1.greatPerson.hero.nameTextId),
|
||||
WaitingGamePlayerInfo(user = "bob", leaderNameTextId = greatPerson2.greatPerson.hero.nameTextId)
|
||||
WaitingGamePlayerInfo(
|
||||
userId = UserId("alice"),
|
||||
leaderNameTextId = greatPerson1.greatPerson.hero.nameTextId
|
||||
),
|
||||
WaitingGamePlayerInfo(userId = UserId("bob"), leaderNameTextId = greatPerson2.greatPerson.hero.nameTextId)
|
||||
),
|
||||
humanPlayerCount = 2,
|
||||
totalPlayerCount = 4,
|
||||
@@ -513,15 +521,16 @@ class GamesManagerTest extends AnyFlatSpec with MockFactory with Matchers with B
|
||||
persister = mockPersister,
|
||||
gameCreation = mockGameCreation,
|
||||
gamePersisterCreation = mockGamePersisterCreation,
|
||||
hexMaps = Map()
|
||||
hexMaps = Map(),
|
||||
userService = new NoOpUserService
|
||||
)
|
||||
|
||||
gamesManager.dropGame(userName = "alice", gameId = gameId) shouldBe
|
||||
gamesManager.dropGame(userId = UserId("alice"), gameId = gameId) shouldBe
|
||||
DropGameResponse(result = SUCCESS_DROP_GAME_RESULT, gameId = gameId)
|
||||
|
||||
gamesManager.gamesAwaitingPlayers should have size 1
|
||||
gamesManager.gamesAwaitingPlayers.head.existingHumanPlayers shouldBe Vector(
|
||||
WaitingGamePlayerInfo(user = "bob", leaderNameTextId = greatPerson2.greatPerson.hero.nameTextId)
|
||||
WaitingGamePlayerInfo(userId = UserId("bob"), leaderNameTextId = greatPerson2.greatPerson.hero.nameTextId)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -535,7 +544,7 @@ class GamesManagerTest extends AnyFlatSpec with MockFactory with Matchers with B
|
||||
expandedGameParameters = expandedGameParameters,
|
||||
gameId = gameId,
|
||||
existingHumanPlayers = Vector(
|
||||
WaitingGamePlayerInfo(user = "alice", leaderNameTextId = greatPerson1.greatPerson.hero.nameTextId)
|
||||
WaitingGamePlayerInfo(userId = UserId("alice"), leaderNameTextId = greatPerson1.greatPerson.hero.nameTextId)
|
||||
),
|
||||
humanPlayerCount = 2,
|
||||
totalPlayerCount = 4,
|
||||
@@ -546,10 +555,11 @@ class GamesManagerTest extends AnyFlatSpec with MockFactory with Matchers with B
|
||||
persister = mockPersister,
|
||||
gameCreation = mockGameCreation,
|
||||
gamePersisterCreation = mockGamePersisterCreation,
|
||||
hexMaps = Map()
|
||||
hexMaps = Map(),
|
||||
userService = new NoOpUserService
|
||||
)
|
||||
|
||||
gamesManager.dropGame(userName = "charlie", gameId = gameId) shouldBe
|
||||
gamesManager.dropGame(userId = UserId("charlie"), gameId = gameId) shouldBe
|
||||
DropGameResponse(result = USER_NOT_IN_GAME_DROP_GAME_RESULT, gameId = gameId)
|
||||
|
||||
// Game should still exist with alice
|
||||
@@ -566,13 +576,14 @@ class GamesManagerTest extends AnyFlatSpec with MockFactory with Matchers with B
|
||||
persister = mockPersister,
|
||||
gameCreation = mockGameCreation,
|
||||
gamePersisterCreation = mockGamePersisterCreation,
|
||||
hexMaps = Map()
|
||||
hexMaps = Map(),
|
||||
userService = new NoOpUserService
|
||||
)
|
||||
|
||||
// Lazy loading will try to read games.e0es from storage - return empty to indicate no games
|
||||
(mockPersister.retrieveAsStream _).expects("games.e0es").returning(Failure(new Exception("not found")))
|
||||
|
||||
gamesManager.dropGame(userName = "alice", gameId = 0xdeadbeef) shouldBe
|
||||
gamesManager.dropGame(userId = UserId("alice"), gameId = 0xdeadbeef) shouldBe
|
||||
DropGameResponse(result = GAME_NOT_FOUND_DROP_GAME_RESULT, gameId = 0xdeadbeef)
|
||||
}
|
||||
|
||||
@@ -588,7 +599,8 @@ class GamesManagerTest extends AnyFlatSpec with MockFactory with Matchers with B
|
||||
persister = mockPersister,
|
||||
gameCreation = mockGameCreation,
|
||||
gamePersisterCreation = mockGamePersisterCreation,
|
||||
hexMaps = Map()
|
||||
hexMaps = Map(),
|
||||
userService = new NoOpUserService
|
||||
)
|
||||
|
||||
// No calls to storage should have been made yet
|
||||
@@ -608,14 +620,15 @@ class GamesManagerTest extends AnyFlatSpec with MockFactory with Matchers with B
|
||||
persister = mockPersister,
|
||||
gameCreation = mockGameCreation,
|
||||
gamePersisterCreation = mockGamePersisterCreation,
|
||||
hexMaps = Map()
|
||||
hexMaps = Map(),
|
||||
userService = new NoOpUserService
|
||||
)
|
||||
|
||||
// Lazy loading will attempt to read games.e0es from storage
|
||||
(mockPersister.retrieveAsStream _).expects("games.e0es").returning(Failure(new Exception("not found")))
|
||||
|
||||
// Should return GAME_NOT_FOUND since the game doesn't exist in storage
|
||||
gamesManager.dropGame(userName = "alice", gameId = nonExistentGameId) shouldBe
|
||||
gamesManager.dropGame(userId = UserId("alice"), gameId = nonExistentGameId) shouldBe
|
||||
DropGameResponse(result = GAME_NOT_FOUND_DROP_GAME_RESULT, gameId = nonExistentGameId)
|
||||
}
|
||||
|
||||
@@ -629,17 +642,18 @@ class GamesManagerTest extends AnyFlatSpec with MockFactory with Matchers with B
|
||||
persister = mockPersister,
|
||||
gameCreation = mockGameCreation,
|
||||
gamePersisterCreation = mockGamePersisterCreation,
|
||||
hexMaps = Map()
|
||||
hexMaps = Map(),
|
||||
userService = new NoOpUserService
|
||||
)
|
||||
|
||||
// First call: games.e0es doesn't exist
|
||||
(mockPersister.retrieveAsStream _).expects("games.e0es").returning(Failure(new Exception("not found")))
|
||||
gamesManager.dropGame(userName = "alice", gameId = 0xaaa) shouldBe
|
||||
gamesManager.dropGame(userId = UserId("alice"), gameId = 0xaaa) shouldBe
|
||||
DropGameResponse(result = GAME_NOT_FOUND_DROP_GAME_RESULT, gameId = 0xaaa)
|
||||
|
||||
// Second call: games.e0es still doesn't exist (fresh read)
|
||||
(mockPersister.retrieveAsStream _).expects("games.e0es").returning(Failure(new Exception("not found")))
|
||||
gamesManager.dropGame(userName = "bob", gameId = 0xbbb) shouldBe
|
||||
gamesManager.dropGame(userId = UserId("bob"), gameId = 0xbbb) shouldBe
|
||||
DropGameResponse(result = GAME_NOT_FOUND_DROP_GAME_RESULT, gameId = 0xbbb)
|
||||
|
||||
// Verifies that games.e0es is read fresh each time, not cached
|
||||
|
||||
@@ -11,6 +11,7 @@ scala_test(
|
||||
"//src/main/protobuf/net/eagle0/eagle/views:action_result_view_scala_proto",
|
||||
"//src/main/scala/net/eagle0/common:functional_random",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle:user_id",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:client_text_store",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:text_generation_result",
|
||||
|
||||
@@ -16,6 +16,7 @@ import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.run_status.RunStatus
|
||||
import net.eagle0.eagle.model.state.RoundPhase
|
||||
import net.eagle0.eagle.service.{FullGameHistory, InMemoryHistory, SyncResponseObserver}
|
||||
import net.eagle0.eagle.UserId
|
||||
import org.scalamock.scalatest.MockFactory
|
||||
import org.scalatest.flatspec.AnyFlatSpec
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
@@ -66,7 +67,7 @@ class GameControllerTest extends AnyFlatSpec with MockFactory with Matchers with
|
||||
val controller = GameController(
|
||||
engine = mockEngine,
|
||||
fullHistory = mockFullHistory,
|
||||
userNameToFactionId = Map("Joe" -> 5),
|
||||
userIdToFactionId = Map(UserId("Joe") -> 5),
|
||||
humanClients = Vector(),
|
||||
aiClients = Vector(),
|
||||
clientTextStore = mockClientTextStore
|
||||
@@ -79,13 +80,13 @@ class GameControllerTest extends AnyFlatSpec with MockFactory with Matchers with
|
||||
val controller = GameController(
|
||||
engine = mockEngine,
|
||||
fullHistory = mockFullHistory,
|
||||
userNameToFactionId = Map("Mary" -> 5),
|
||||
userIdToFactionId = Map(UserId("Mary") -> 5),
|
||||
humanClients = Vector(),
|
||||
aiClients = Vector(),
|
||||
clientTextStore = mockClientTextStore
|
||||
)
|
||||
|
||||
controller.userNameToFactionId.get("Joe") shouldBe None
|
||||
controller.userIdToFactionId.get(UserId("Joe")) shouldBe None
|
||||
}
|
||||
|
||||
"PostCommand" should "throw if the token does not match" in {
|
||||
@@ -104,7 +105,7 @@ class GameControllerTest extends AnyFlatSpec with MockFactory with Matchers with
|
||||
val controller = GameController(
|
||||
engine = mockEngine,
|
||||
fullHistory = mockFullHistory,
|
||||
userNameToFactionId = Map("joe" -> 7),
|
||||
userIdToFactionId = Map(UserId("joe") -> 7),
|
||||
humanClients = Vector(client),
|
||||
aiClients = Vector(),
|
||||
clientTextStore = mockClientTextStore
|
||||
@@ -117,7 +118,7 @@ class GameControllerTest extends AnyFlatSpec with MockFactory with Matchers with
|
||||
|
||||
val ex = the[EagleClientException] thrownBy
|
||||
controller.postHumanCommand(
|
||||
userName = "joe",
|
||||
userId = UserId("joe"),
|
||||
selectedProvinceId = 0,
|
||||
command = ReturnSelectedCommand(),
|
||||
token = 41
|
||||
@@ -152,7 +153,7 @@ class GameControllerTest extends AnyFlatSpec with MockFactory with Matchers with
|
||||
val controller = GameController(
|
||||
engine = mockEngine,
|
||||
fullHistory = mockFullHistory,
|
||||
userNameToFactionId = Map("joe" -> 7),
|
||||
userIdToFactionId = Map(UserId("joe") -> 7),
|
||||
humanClients = Vector(client),
|
||||
aiClients = Vector(),
|
||||
clientTextStore = mockClientTextStore
|
||||
@@ -238,7 +239,7 @@ class GameControllerTest extends AnyFlatSpec with MockFactory with Matchers with
|
||||
|
||||
noException should be thrownBy
|
||||
controller.postHumanCommand(
|
||||
userName = "joe",
|
||||
userId = UserId("joe"),
|
||||
selectedProvinceId = 0,
|
||||
command = command,
|
||||
token = 5
|
||||
@@ -343,7 +344,7 @@ class GameControllerTest extends AnyFlatSpec with MockFactory with Matchers with
|
||||
val controller = GameController(
|
||||
engine = mockEngine,
|
||||
fullHistory = mockFullHistory,
|
||||
userNameToFactionId = Map("player1" -> 1, "player2" -> 2),
|
||||
userIdToFactionId = Map(UserId("player1") -> 1, UserId("player2") -> 2),
|
||||
humanClients = Vector(client1, client2),
|
||||
aiClients = Vector(),
|
||||
clientTextStore = mockClientTextStore
|
||||
|
||||
Reference in New Issue
Block a user