lila/modules/relay/src/main/RelaySync.scala

220 lines
7.4 KiB
Scala
Raw Normal View History

2017-09-19 20:24:59 -06:00
package lila.relay
2017-10-05 15:48:14 -06:00
import org.joda.time.DateTime
2017-10-06 17:33:33 -06:00
import chess.format.pgn.{ Tag, Tags }
2019-07-13 12:02:50 -06:00
import lila.socket.Socket.Sri
2017-09-20 13:35:28 -06:00
import lila.study._
2017-09-19 20:24:59 -06:00
2019-12-13 07:30:20 -07:00
final private class RelaySync(
2017-09-20 13:35:28 -06:00
studyApi: StudyApi,
chapterRepo: ChapterRepo
)(implicit ec: scala.concurrent.ExecutionContext) {
2017-09-19 20:24:59 -06:00
2017-10-04 23:59:35 -06:00
private type NbMoves = Int
2021-04-22 05:36:48 -06:00
def apply(relay: Relay, tour: RelayTour, games: RelayGames): Fu[SyncResult.Ok] =
2019-12-03 21:58:09 -07:00
studyApi byId relay.studyId orFail "Missing relay study!" flatMap { study =>
chapterRepo orderedByStudy study.id flatMap { chapters =>
2018-10-17 03:55:10 -06:00
RelayInputSanity(chapters, games) match {
2018-10-17 06:33:29 -06:00
case Some(fail) => fufail(fail.msg)
2019-12-13 07:30:20 -07:00
case None =>
2019-12-28 18:19:54 -07:00
lila.common.Future.linear(games) { game =>
2019-12-13 07:30:20 -07:00
findCorrespondingChapter(game, chapters, games.size) match {
2021-04-22 05:36:48 -06:00
case Some(chapter) => updateChapter(relay, tour, study, chapter, game)
2019-12-13 07:30:20 -07:00
case None =>
createChapter(study, game) flatMap { chapter =>
chapters.find(_.isEmptyInitial).ifTrue(chapter.order == 2).?? { initial =>
studyApi.deleteChapter(study.id, initial.id) {
actorApi.Who(study.ownerId, sri)
}
} inject chapter.root.mainline.size
2019-10-25 04:46:42 -06:00
}
2018-10-17 03:55:10 -06:00
}
2020-08-16 06:37:41 -06:00
} map { _.sum } dmap { SyncResult.Ok(_, games) }
2018-10-17 03:55:10 -06:00
}
}
2017-09-20 13:35:28 -06:00
}
2017-09-19 20:24:59 -06:00
/*
* If the source contains several games, use their index to match them with the study chapter.
* If the source contains only one game, use the player tags (and site) to match with the study chapter.
* So the TCEC style - one game per file, reusing the file for all games - is supported.
* lichess will create a new chapter when the game player tags differ.
*/
2019-12-13 07:30:20 -07:00
private def findCorrespondingChapter(
game: RelayGame,
chapters: List[Chapter],
nbGames: Int
): Option[Chapter] =
if (nbGames == 1 || game.looksLikeLichess) chapters find game.staticTagsMatch
else chapters.find(_.relay.exists(_.index == game.index))
2021-04-22 05:36:48 -06:00
private def updateChapter(
relay: Relay,
tour: RelayTour,
study: Study,
chapter: Chapter,
game: RelayGame
): Fu[NbMoves] =
updateChapterTags(relay, tour, study, chapter, game) >>
updateChapterTree(study, chapter, game)
2019-10-25 04:46:42 -06:00
private def updateChapterTree(study: Study, chapter: Chapter, game: RelayGame): Fu[NbMoves] = {
val who = actorApi.Who(chapter.ownerId, sri)
2017-09-20 13:35:28 -06:00
game.root.mainline.foldLeft(Path.root -> none[Node]) {
case ((parentPath, None), gameNode) =>
val path = parentPath + gameNode
chapter.root.nodeAt(path) match {
case None => parentPath -> gameNode.some
case Some(existing) =>
gameNode.clock.filter(c => !existing.clock.has(c)) ?? { c =>
2017-10-09 12:36:36 -06:00
studyApi.setClock(
studyId = study.id,
position = Position(chapter, path).ref,
2019-10-25 04:46:42 -06:00
clock = c.some
)(who)
}
path -> none
}
2017-09-20 13:35:28 -06:00
case (found, _) => found
} match {
case (path, newNode) =>
!Path.isMainline(chapter.root, path) ?? {
2018-10-17 04:23:47 -06:00
logger.info(s"Change mainline ${showSC(study, chapter)} $path")
studyApi.promote(
studyId = study.id,
position = Position(chapter, path).ref,
2019-10-25 04:46:42 -06:00
toMainline = true
)(who) >> chapterRepo.setRelayPath(chapter.id, path)
} >> newNode.?? { node =>
2021-02-02 05:17:48 -07:00
lila.common.Future.fold(node.mainline.toList)(Position(chapter, path).ref) { case (position, n) =>
2020-09-21 01:28:28 -06:00
studyApi.addNode(
studyId = study.id,
position = position,
node = n,
opts = moveOpts.copy(clock = n.clock),
relay = Chapter
.Relay(
index = game.index,
path = position.path + n,
lastMoveAt = DateTime.now
)
.some
)(who) inject position + n
} inject {
if (chapter.root.children.nodes.isEmpty && node.mainline.nonEmpty)
studyApi.reloadChapters(study)
node.mainline.size
}
}
2017-09-20 13:35:28 -06:00
}
2019-10-25 04:46:42 -06:00
}
2017-09-20 13:35:28 -06:00
2021-04-22 05:36:48 -06:00
private def updateChapterTags(
relay: Relay,
tour: RelayTour,
study: Study,
chapter: Chapter,
game: RelayGame
): Funit = {
2020-09-21 01:28:28 -06:00
val gameTags = game.tags.value.foldLeft(Tags(Nil)) { case (newTags, tag) =>
if (!chapter.tags.value.exists(tag ==)) newTags + tag
else newTags
2017-10-06 17:33:33 -06:00
}
val newEndTag = game.end
2017-10-06 17:33:33 -06:00
.ifFalse(gameTags(_.Result).isDefined)
.filterNot(end => chapter.tags(_.Result).??(end.resultText ==))
.map(end => Tag(_.Result, end.resultText))
val tags = newEndTag.fold(gameTags)(gameTags + _)
2020-09-21 01:28:28 -06:00
val chapterNewTags = tags.value.foldLeft(chapter.tags) { case (chapterTags, tag) =>
PgnTags(chapterTags + tag)
}
(chapterNewTags != chapter.tags) ?? {
2018-10-17 03:55:10 -06:00
if (vs(chapterNewTags) != vs(chapter.tags))
2018-10-17 04:23:47 -06:00
logger.info(s"Update ${showSC(study, chapter)} tags '${vs(chapter.tags)}' -> '${vs(chapterNewTags)}'")
studyApi.setTags(
studyId = study.id,
chapterId = chapter.id,
2019-10-25 04:46:42 -06:00
tags = chapterNewTags
)(actorApi.Who(chapter.ownerId, sri)) >> {
val newEnd = chapter.tags.resultColor.isEmpty && tags.resultColor.isDefined
2021-04-22 05:36:48 -06:00
newEnd ?? onChapterEnd(relay, tour, study, chapter)
}
}
2017-10-06 17:33:33 -06:00
}
2021-04-22 05:36:48 -06:00
private def onChapterEnd(relay: Relay, tour: RelayTour, study: Study, chapter: Chapter): Funit =
chapterRepo.setRelayPath(chapter.id, Path.root) >> {
2021-04-22 05:36:48 -06:00
(tour.official && chapter.root.mainline.sizeIs > 10) ?? studyApi.analysisRequest(
studyId = study.id,
chapterId = chapter.id,
userId = study.ownerId
)
} >>- studyApi.reloadChapters(study)
2018-03-13 09:08:48 -06:00
private def createChapter(study: Study, game: RelayGame): Fu[Chapter] =
2017-09-20 13:35:28 -06:00
chapterRepo.nextOrderByStudy(study.id) flatMap { order =>
val name = {
for {
w <- game.tags(_.White)
b <- game.tags(_.Black)
} yield s"$w - $b"
} orElse game.tags("board") getOrElse "?"
2017-09-20 13:35:28 -06:00
val chapter = Chapter.make(
studyId = study.id,
name = Chapter.Name(name),
2017-09-20 13:35:28 -06:00
setup = Chapter.Setup(
none,
game.variant,
2017-09-20 13:35:28 -06:00
chess.Color.White
),
root = game.root,
2017-10-05 11:28:07 -06:00
tags = game.tags,
2017-09-20 13:35:28 -06:00
order = order,
ownerId = study.ownerId,
practice = false,
gamebook = false,
2017-10-07 06:14:25 -06:00
conceal = none,
2019-12-13 07:30:20 -07:00
relay = Chapter
.Relay(
index = game.index,
path = game.root.mainlinePath,
lastMoveAt = DateTime.now
)
.some
2017-09-20 13:35:28 -06:00
)
2019-10-25 04:46:42 -06:00
studyApi.doAddChapter(study, chapter, sticky = false, actorApi.Who(study.ownerId, sri)) inject chapter
2017-09-20 13:35:28 -06:00
}
private val moveOpts = MoveOpts(
write = true,
sticky = false,
promoteToMainline = true,
clock = none
)
2019-10-25 04:46:42 -06:00
private val sri = Sri("")
2018-10-17 03:55:10 -06:00
2018-10-17 04:23:47 -06:00
private def vs(tags: Tags) = s"${tags(_.White) | "?"} - ${tags(_.Black) | "?"}"
2018-10-17 03:55:10 -06:00
private def showSC(study: Study, chapter: Chapter) =
s"#${study.id} chapter[${chapter.relay.fold("?")(_.index.toString)}]"
2017-09-19 20:24:59 -06:00
}
2017-10-18 11:54:50 -06:00
sealed trait SyncResult {
val reportKey: String
}
object SyncResult {
2017-10-18 11:54:50 -06:00
case class Ok(moves: Int, games: RelayGames) extends SyncResult {
val reportKey = "ok"
}
case object Timeout extends Exception with SyncResult {
2019-12-13 07:30:20 -07:00
val reportKey = "timeout"
override def getMessage = "In progress..."
}
2018-10-17 06:33:29 -06:00
case class Error(msg: String) extends SyncResult {
2017-10-18 11:54:50 -06:00
val reportKey = "error"
}
}