180 lines
6.9 KiB
Scala
180 lines
6.9 KiB
Scala
package repository
|
|
|
|
import com.typesafe.config.{Config, ConfigFactory}
|
|
|
|
import java.io.{File, PrintWriter}
|
|
import scala.collection.JavaConverters._
|
|
import scala.util.Try
|
|
|
|
/** Описание одной карты в маппуле десайдера. */
|
|
case class DeciderMapConfig(map: String, description: Option[String] = None)
|
|
|
|
/** Полное описание десайдера (турнира). */
|
|
case class Decider(
|
|
key: String,
|
|
name: String,
|
|
rules: String,
|
|
raceCount: Int,
|
|
existAdditionalRaces: Boolean,
|
|
existTextFieldRaces: Boolean,
|
|
description: Boolean,
|
|
maps: List[DeciderMapConfig]
|
|
)
|
|
|
|
/**
|
|
* Чтение и запись десайдеров.
|
|
*
|
|
* Десайдеры хранятся во внешнем HOCON-файле (admin.decidersFile, по умолчанию
|
|
* "conf/deciders.conf"). Пока этот файл не существует, десайдеры читаются из
|
|
* application.conf, поэтому приложение работает "из коробки". Как только админ
|
|
* что-то сохраняет через админку, внешний файл становится единственным источником
|
|
* правды.
|
|
*/
|
|
object DeciderRepository {
|
|
|
|
private def appConfig: Config = ConfigFactory.load()
|
|
|
|
/** Файл, в который админка пишет десайдеров. */
|
|
def file: File = {
|
|
val path = Try(appConfig.getString("admin.decidersFile")).getOrElse("conf/deciders.conf")
|
|
new File(path)
|
|
}
|
|
|
|
/** Десайдеры как объект Config (содержимое блока `deciders { ... }`). */
|
|
def decidersConfig: Config = {
|
|
val f = file
|
|
if (f.isFile) {
|
|
Try(ConfigFactory.parseFile(f)).toOption
|
|
.filter(_.hasPath("deciders"))
|
|
.map(_.getConfig("deciders"))
|
|
.getOrElse(fallback)
|
|
} else {
|
|
fallback
|
|
}
|
|
}
|
|
|
|
/** Переинициализировать кеш конфигуратора (вызывать после сохранения). */
|
|
def reload(): Unit = {
|
|
ConfigFactory.invalidateCaches()
|
|
}
|
|
|
|
private def fallback: Config =
|
|
if (appConfig.hasPath("deciders")) appConfig.getConfig("deciders") else ConfigFactory.empty()
|
|
|
|
/** Ключи всех десайдеров (отсортированные). */
|
|
def keys: Seq[String] = decidersConfig.root().keySet().asScala.toSeq.sorted
|
|
|
|
/** Один десайдер по ключу. */
|
|
def get(key: String): Option[Decider] = {
|
|
val c = decidersConfig
|
|
if (!c.hasPath(key)) None
|
|
else {
|
|
val d = c.getConfig(key)
|
|
val maps = d.getList("maps").asScala.toList.map { value =>
|
|
value.unwrapped() match {
|
|
case list: java.util.List[_] =>
|
|
val parts = list.asScala.map(_.toString).toList
|
|
DeciderMapConfig(parts.head, parts.lift(1).filter(_.nonEmpty))
|
|
case other =>
|
|
// Некоторые десайдеры заданы просто списком строк без описаний.
|
|
DeciderMapConfig(other.toString, None)
|
|
}
|
|
}
|
|
Some(Decider(
|
|
key = key,
|
|
name = Try(d.getString("name")).getOrElse(key),
|
|
rules = Try(d.getString("rules")).getOrElse(""),
|
|
raceCount = Try(d.getInt("raceCount")).getOrElse(1),
|
|
existAdditionalRaces = Try(d.getBoolean("existAdditionalRaces")).getOrElse(false),
|
|
existTextFieldRaces = Try(d.getBoolean("existTextFieldRaces")).getOrElse(false),
|
|
description = Try(d.getBoolean("description")).getOrElse(false),
|
|
maps = maps
|
|
))
|
|
}
|
|
}
|
|
|
|
/** Все десайдеры. */
|
|
def all: Seq[Decider] = keys.flatMap(get)
|
|
|
|
/** Создаёт или обновляет десайдера и перезаписывает весь файл десайдеров. */
|
|
def save(decider: Decider): Unit = {
|
|
val updated = (all.filterNot(_.key == decider.key) :+ decider).sortBy(_.key)
|
|
write(updated)
|
|
}
|
|
|
|
/** Удаляет десайдера. Возвращает true, если десайдер существовал. */
|
|
def delete(key: String): Boolean = {
|
|
val current = all
|
|
if (current.exists(_.key == key)) {
|
|
write(current.filterNot(_.key == key))
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
private def write(deciders: Seq[Decider]): Unit = {
|
|
val sb = new StringBuilder
|
|
sb.append("# Конфигурация десайдеров.\n")
|
|
sb.append("# Файл управляется админкой (авторизация через Steam).\n")
|
|
sb.append("# Ручные правки могут быть перезаписаны при следующем сохранении.\n\n")
|
|
sb.append("deciders {\n")
|
|
deciders.foreach { d =>
|
|
sb.append(" ").append(quoteKey(d.key)).append(" {\n")
|
|
sb.append(" name = ").append(quote(d.name)).append('\n')
|
|
sb.append(" raceCount = ").append(d.raceCount).append('\n')
|
|
if (d.existAdditionalRaces) sb.append(" existAdditionalRaces = true\n")
|
|
if (d.existTextFieldRaces) sb.append(" existTextFieldRaces = true\n")
|
|
if (d.description) sb.append(" description = true\n")
|
|
if (d.maps.isEmpty) {
|
|
sb.append(" maps = []\n")
|
|
} else {
|
|
sb.append(" maps = [\n")
|
|
d.maps.zipWithIndex.foreach { case (m, i) =>
|
|
sb.append(" ").append(renderMap(m))
|
|
if (i < d.maps.size - 1) sb.append(',')
|
|
sb.append('\n')
|
|
}
|
|
sb.append(" ]\n")
|
|
}
|
|
sb.append(" rules = ").append(quote(d.rules)).append('\n')
|
|
sb.append(" }\n")
|
|
}
|
|
sb.append("}\n")
|
|
|
|
val f = file
|
|
Option(f.getParentFile).foreach(_.mkdirs())
|
|
val writer = new PrintWriter(f, "UTF-8")
|
|
try writer.write(sb.toString) finally writer.close()
|
|
}
|
|
|
|
private def renderMap(m: DeciderMapConfig): String =
|
|
m.description.filter(_.nonEmpty) match {
|
|
case Some(desc) => s"[${quote(m.map)}, ${quote(desc)}]"
|
|
case None => s"[${quote(m.map)}]"
|
|
}
|
|
|
|
private def quoteKey(key: String): String =
|
|
if (key.matches("[A-Za-z0-9_-]+") && !key.forall(_.isDigit)) key else quote(key)
|
|
|
|
/** Рендерит строку как строку в формате JSON/HOCON. */
|
|
private def quote(value: String): String = {
|
|
val sb = new StringBuilder("\"")
|
|
value.foreach {
|
|
case '"' => sb.append("\\\"")
|
|
case '\\' => sb.append("\\\\")
|
|
case '\n' => sb.append("\\n")
|
|
case '\r' => sb.append("\\r")
|
|
case '\t' => sb.append("\\t")
|
|
// Литерал из обратного слэша и буквы u ломает компилятор Scala:
|
|
// такая последовательность обрабатывается как Unicode-escape ещё до лексера.
|
|
// Поэтому символы добавляем по отдельности, без escape-последовательности в исходнике.
|
|
case c if c < ' ' =>
|
|
sb.append(0x5c.toChar).append('u').append("%04x".format(c.toInt))
|
|
case c => sb.append(c)
|
|
}
|
|
sb.append('"')
|
|
sb.toString
|
|
}
|
|
}
|