304 lines
9.2 KiB
Scala
304 lines
9.2 KiB
Scala
package lila.simul
|
|
|
|
import akka.actor._
|
|
import chess.variant.Variant
|
|
import play.api.libs.json.Json
|
|
import scala.concurrent.duration._
|
|
|
|
import lila.common.{ Bus, Debouncer }
|
|
import lila.db.dsl._
|
|
import lila.game.{ Game, GameRepo, PerfPicker }
|
|
import lila.hub.actorApi.timeline.{ Propagate, SimulCreate, SimulJoin }
|
|
import lila.hub.LightTeam.TeamID
|
|
import lila.memo.CacheApi._
|
|
import lila.socket.Socket.SendToFlag
|
|
import lila.user.{ User, UserRepo }
|
|
|
|
final class SimulApi(
|
|
userRepo: UserRepo,
|
|
gameRepo: GameRepo,
|
|
onGameStart: lila.round.OnStart,
|
|
socket: SimulSocket,
|
|
renderer: lila.hub.actors.Renderer,
|
|
timeline: lila.hub.actors.Timeline,
|
|
repo: SimulRepo,
|
|
cacheApi: lila.memo.CacheApi
|
|
)(implicit
|
|
ec: scala.concurrent.ExecutionContext,
|
|
system: akka.actor.ActorSystem,
|
|
mode: play.api.Mode
|
|
) {
|
|
|
|
private val workQueue =
|
|
new lila.hub.AsyncActorSequencers(
|
|
maxSize = 128,
|
|
expiration = 10 minutes,
|
|
timeout = 10 seconds,
|
|
name = "simulApi"
|
|
)
|
|
|
|
def currentHostIds: Fu[Set[String]] = currentHostIdsCache.get {}
|
|
|
|
def find = repo.find _
|
|
def byIds = repo.byIds _
|
|
|
|
private val currentHostIdsCache = cacheApi.unit[Set[User.ID]] {
|
|
_.refreshAfterWrite(5 minutes)
|
|
.buildAsyncFuture { _ =>
|
|
repo.allStarted dmap (_.view.map(_.hostId).toSet)
|
|
}
|
|
}
|
|
|
|
def create(setup: SimulForm.Setup, me: User): Fu[Simul] = {
|
|
val simul = Simul.make(
|
|
name = setup.name,
|
|
clock = setup.clock,
|
|
variants = setup.actualVariants,
|
|
position = setup.realPosition,
|
|
host = me,
|
|
color = setup.color,
|
|
text = setup.text,
|
|
estimatedStartAt = setup.estimatedStartAt,
|
|
team = setup.team,
|
|
featurable = some(~setup.featured && me.canBeFeatured)
|
|
)
|
|
repo.create(simul) >>- publish() >>- {
|
|
timeline ! (Propagate(SimulCreate(me.id, simul.id, simul.fullName)) toFollowersOf me.id)
|
|
} inject simul
|
|
}
|
|
|
|
def update(prev: Simul, setup: SimulForm.Setup, me: User): Fu[Simul] = {
|
|
val simul = prev.copy(
|
|
name = setup.name,
|
|
clock = setup.clock,
|
|
variants = setup.actualVariants,
|
|
position = setup.realPosition,
|
|
color = setup.color.some,
|
|
text = setup.text,
|
|
estimatedStartAt = setup.estimatedStartAt,
|
|
team = setup.team,
|
|
featurable = some(~setup.featured && me.canBeFeatured)
|
|
)
|
|
repo.update(simul) >>- publish() inject simul
|
|
}
|
|
|
|
def addApplicant(simulId: Simul.ID, user: User, variantKey: String): Funit =
|
|
WithSimul(repo.findCreated, simulId) { simul =>
|
|
if (simul.nbAccepted >= Game.maxPlayingRealtime) simul
|
|
else {
|
|
timeline ! (Propagate(SimulJoin(user.id, simul.id, simul.fullName)) toFollowersOf user.id)
|
|
Variant(variantKey).filter(simul.variants.contains).fold(simul) { variant =>
|
|
simul addApplicant SimulApplicant.make(
|
|
SimulPlayer.make(
|
|
user,
|
|
variant,
|
|
PerfPicker.mainOrDefault(
|
|
speed = chess.Speed(simul.clock.config.some),
|
|
variant = variant,
|
|
daysPerTurn = none
|
|
)(user.perfs)
|
|
)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
def removeApplicant(simulId: Simul.ID, user: User): Funit =
|
|
WithSimul(repo.findCreated, simulId) { _ removeApplicant user.id }
|
|
|
|
def accept(simulId: Simul.ID, userId: String, v: Boolean): Funit =
|
|
userRepo byId userId flatMap {
|
|
_ ?? { user =>
|
|
WithSimul(repo.findCreated, simulId) { _.accept(user.id, v) }
|
|
}
|
|
}
|
|
|
|
def start(simulId: Simul.ID): Funit =
|
|
workQueue(simulId) {
|
|
repo.findCreated(simulId) flatMap {
|
|
_ ?? { simul =>
|
|
simul.start ?? { started =>
|
|
userRepo byId started.hostId orFail s"No such host: ${simul.hostId}" flatMap { host =>
|
|
started.pairings.zipWithIndex.map(makeGame(started, host)).sequenceFu map { games =>
|
|
games.headOption foreach { case (game, _) =>
|
|
socket.startSimul(simul, game)
|
|
}
|
|
games.foldLeft(started) { case (s, (g, hostColor)) =>
|
|
s.setPairingHostColor(g.id, hostColor)
|
|
}
|
|
}
|
|
} flatMap { s =>
|
|
Bus.publish(Simul.OnStart(s), "startSimul")
|
|
update(s) >>- currentHostIdsCache.invalidateUnit()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
def onPlayerConnection(game: Game, user: Option[User])(simul: Simul): Unit =
|
|
if (user.exists(simul.isHost) && simul.isRunning) {
|
|
repo.setHostGameId(simul, game.id)
|
|
socket.hostIsOn(simul.id, game.id)
|
|
}
|
|
|
|
def abort(simulId: Simul.ID): Funit =
|
|
workQueue(simulId) {
|
|
repo.findCreated(simulId) flatMap {
|
|
_ ?? { simul =>
|
|
(repo remove simul) >>- socket.aborted(simul.id) >>- publish()
|
|
}
|
|
}
|
|
}
|
|
|
|
def setText(simulId: Simul.ID, text: String): Funit =
|
|
workQueue(simulId) {
|
|
repo.find(simulId) flatMap {
|
|
_ ?? { simul =>
|
|
repo.setText(simul, text) >>- socket.reload(simulId)
|
|
}
|
|
}
|
|
}
|
|
|
|
def finishGame(game: Game): Funit =
|
|
game.simulId ?? { simulId =>
|
|
workQueue(simulId) {
|
|
repo.findStarted(simulId) flatMap {
|
|
_ ?? { simul =>
|
|
val simul2 = simul.updatePairing(
|
|
game.id,
|
|
_.finish(game.status, game.winnerUserId)
|
|
)
|
|
update(simul2) >>- {
|
|
if (simul2.isFinished) onComplete(simul2)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private def onComplete(simul: Simul): Unit = {
|
|
currentHostIdsCache.invalidateUnit()
|
|
Bus.publish(
|
|
lila.hub.actorApi.socket.SendTo(
|
|
simul.hostId,
|
|
lila.socket.Socket.makeMessage(
|
|
"simulEnd",
|
|
Json.obj(
|
|
"id" -> simul.id,
|
|
"name" -> simul.name
|
|
)
|
|
)
|
|
),
|
|
"socketUsers"
|
|
)
|
|
}
|
|
|
|
def ejectCheater(userId: String): Unit =
|
|
repo.allNotFinished foreach {
|
|
_ foreach { oldSimul =>
|
|
workQueue(oldSimul.id) {
|
|
repo.findCreated(oldSimul.id) flatMap {
|
|
_ ?? { simul =>
|
|
(simul ejectCheater userId) ?? { simul2 =>
|
|
update(simul2).void
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
def hostPing(simul: Simul): Funit =
|
|
simul.isCreated ?? {
|
|
repo.setHostSeenNow(simul) >> {
|
|
val applicantIds = simul.applicants.view.map(_.player.user).toSet
|
|
socket.filterPresent(simul, applicantIds) flatMap { online =>
|
|
val leaving = applicantIds diff online.toSet
|
|
leaving.nonEmpty ??
|
|
WithSimul(repo.findCreated, simul.id) {
|
|
_.copy(applicants = simul.applicants.filterNot(a => leaving(a.player.user)))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
def byTeamLeaders = repo.byTeamLeaders _
|
|
|
|
def idToName(id: Simul.ID): Fu[Option[String]] =
|
|
repo find id dmap2 { _.fullName }
|
|
|
|
def teamOf(id: Simul.ID): Fu[Option[TeamID]] =
|
|
repo.coll.primitiveOne[TeamID]($id(id), "team")
|
|
|
|
private def makeGame(simul: Simul, host: User)(
|
|
pairingAndNumber: (SimulPairing, Int)
|
|
): Fu[(Game, chess.Color)] =
|
|
pairingAndNumber match {
|
|
case (pairing, number) =>
|
|
for {
|
|
user <- userRepo byId pairing.player.user orFail s"No user with id ${pairing.player.user}"
|
|
hostColor = simul.hostColor | chess.Color.fromWhite(number % 2 == 0)
|
|
whiteUser = hostColor.fold(host, user)
|
|
blackUser = hostColor.fold(user, host)
|
|
clock = simul.clock.chessClockOf(hostColor)
|
|
perfPicker =
|
|
lila.game.PerfPicker.mainOrDefault(chess.Speed(clock.config), pairing.player.variant, none)
|
|
game1 = Game.make(
|
|
chess = chess
|
|
.Game(
|
|
variantOption = Some {
|
|
if (simul.position.isEmpty) pairing.player.variant
|
|
else chess.variant.FromPosition
|
|
},
|
|
fen = simul.position
|
|
)
|
|
.copy(clock = clock.start.some),
|
|
whitePlayer = lila.game.Player.make(chess.White, whiteUser.some, perfPicker),
|
|
blackPlayer = lila.game.Player.make(chess.Black, blackUser.some, perfPicker),
|
|
mode = chess.Mode.Casual,
|
|
source = lila.game.Source.Simul,
|
|
pgnImport = None
|
|
)
|
|
game2 =
|
|
game1
|
|
.withId(pairing.gameId)
|
|
.withSimulId(simul.id)
|
|
.start
|
|
_ <-
|
|
(gameRepo insertDenormalized game2) >>-
|
|
onGameStart(game2.id) >>-
|
|
socket.startGame(simul, game2)
|
|
} yield game2 -> hostColor
|
|
}
|
|
|
|
private def update(simul: Simul): Funit =
|
|
repo.update(simul) >>- socket.reload(simul.id) >>- publish()
|
|
|
|
private def WithSimul(
|
|
finding: Simul.ID => Fu[Option[Simul]],
|
|
simulId: Simul.ID
|
|
)(updating: Simul => Simul): Funit = {
|
|
workQueue(simulId) {
|
|
finding(simulId) flatMap {
|
|
_ ?? { simul =>
|
|
update(updating(simul))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private object publish {
|
|
private val siteMessage = SendToFlag("simul", Json.obj("t" -> "reload"))
|
|
private val debouncer = system.actorOf(
|
|
Props(
|
|
new Debouncer(
|
|
5 seconds,
|
|
(_: Debouncer.Nothing) => Bus.publish(siteMessage, "sendToFlag")
|
|
)
|
|
)
|
|
)
|
|
def apply(): Unit = { debouncer ! Debouncer.Nothing }
|
|
}
|
|
}
|