diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..c92e8bc --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +sudo: required +language: generic + +services: + - docker + +script: + - docker run -it --rm mozilla/sbt sbt compile + - docker run -it --rm mozilla/sbt sbt test diff --git a/src/main/resources/reference.conf b/src/main/resources/reference.conf index 5e22871..378cf84 100644 --- a/src/main/resources/reference.conf +++ b/src/main/resources/reference.conf @@ -1,16 +1,16 @@ me.arcanis.ffxivbis { ariyala { - // ariyala base url, string, required + # ariyala base url, string, required ariyala-url = "https://ffxiv.ariyala.com" - // xivapi base url, string, required + # xivapi base url, string, required xivapi-url = "https://xivapi.com" - // xivapi developer key, string, optional - # xivapi-key = "abc-def" + # xivapi developer key, string, optional + # xivapi-key = "abcdef" } database { - // database section. Section must be declared inside - // for more detailed section descriptions refer to slick documentation + # database section. Section must be declared inside + # for more detailed section descriptions refer to slick documentation mode = "sqlite" sqlite { @@ -35,19 +35,19 @@ me.arcanis.ffxivbis { } settings { - // counters of Player class which will be called to sort players for loot priority - // list of strings, required + # counters of Player class which will be called to sort players for loot priority + # list of strings, required priority = [ "isRequired", "lootCountBiS", "priority", "lootCount", "lootCountTotal" ] - // general request timeout, duratin, required + # general request timeout, duratin, required request-timeout = 10s } web { - // address to bind, string, required + # address to bind, string, required host = "0.0.0.0" - // port to bind, int, required + # port to bind, int, required port = 8000 } } diff --git a/src/main/scala/me/arcanis/ffxivbis/models/Party.scala b/src/main/scala/me/arcanis/ffxivbis/models/Party.scala index 9090f76..b71a1e1 100644 --- a/src/main/scala/me/arcanis/ffxivbis/models/Party.scala +++ b/src/main/scala/me/arcanis/ffxivbis/models/Party.scala @@ -50,7 +50,7 @@ object Party { case (acc, (playerId, player)) => acc + (player.playerId -> player .withBiS(bisByPlayer.get(playerId)) - .withLoot(lootByPlayer.get(playerId))) + .withLoot(lootByPlayer.getOrElse(playerId, Seq.empty))) } Party(partyId, getRules(config), playersWithItems) } diff --git a/src/main/scala/me/arcanis/ffxivbis/models/Player.scala b/src/main/scala/me/arcanis/ffxivbis/models/Player.scala index acef4c1..3bbf3bd 100644 --- a/src/main/scala/me/arcanis/ffxivbis/models/Player.scala +++ b/src/main/scala/me/arcanis/ffxivbis/models/Player.scala @@ -27,9 +27,10 @@ case class Player(partyId: String, partyId, job, nick, isRequired(piece), priority, bisCountTotal(piece), lootCount(piece), lootCountBiS(piece), lootCountTotal(piece)) - def withLoot(list: Option[Seq[Piece]]): Player = list match { - case Some(value) => copy(loot = value) - case None => this + def withLoot(piece: Piece): Player = withLoot(Seq(piece)) + def withLoot(list: Seq[Piece]): Player = list match { + case Nil => this + case _ => copy(loot = list) } def isRequired(piece: Option[Piece]): Boolean = { diff --git a/src/main/scala/me/arcanis/ffxivbis/models/PlayerIdWithCounters.scala b/src/main/scala/me/arcanis/ffxivbis/models/PlayerIdWithCounters.scala index fbc616c..f28c6bc 100644 --- a/src/main/scala/me/arcanis/ffxivbis/models/PlayerIdWithCounters.scala +++ b/src/main/scala/me/arcanis/ffxivbis/models/PlayerIdWithCounters.scala @@ -26,12 +26,12 @@ case class PlayerIdWithCounters(partyId: String, def playerId: PlayerId = PlayerId(partyId, job, nick) private val counters: Map[String, Int] = Map( - "isRequired" -> (if (isRequired) 1 else 0), - "priority" -> priority, - "bisCountTotal" -> bisCountTotal, - "lootCount" -> lootCount, - "lootCountBiS" -> lootCountBiS, - "lootCountTotal" -> lootCountTotal) withDefaultValue 0 + "isRequired" -> (if (isRequired) 1 else 0), // true has more priority + "priority" -> -priority, // the less value the more priority + "bisCountTotal" -> bisCountTotal, // the more pieces in bis the more priority + "lootCount" -> -lootCount, // the less loot got the more priority + "lootCountBiS" -> -lootCountBiS, // the less bis pieces looted the more priority + "lootCountTotal" -> -lootCountTotal) withDefaultValue 0 // the less pieces looted the more priority private def withCounters(orderBy: Seq[String]): PlayerCountersComparator = PlayerCountersComparator(orderBy.map(counters): _*) diff --git a/src/main/scala/me/arcanis/ffxivbis/service/Ariyala.scala b/src/main/scala/me/arcanis/ffxivbis/service/Ariyala.scala index b13fd4e..7f9deb6 100644 --- a/src/main/scala/me/arcanis/ffxivbis/service/Ariyala.scala +++ b/src/main/scala/me/arcanis/ffxivbis/service/Ariyala.scala @@ -22,7 +22,7 @@ import me.arcanis.ffxivbis.models.{BiS, Job, Piece} import spray.json._ import scala.concurrent.{ExecutionContext, Future} -import scala.util.Try +import scala.util.{Failure, Success, Try} class Ariyala extends Actor with StrictLogging { import Ariyala._ @@ -51,12 +51,16 @@ class Ariyala extends Actor with StrictLogging { sendRequest(uri, Ariyala.parseAriyalaJsonToPieces(job, getIsTome)) } - private def getIsTome(itemId: Long): Future[Boolean] = { - val uri = Try(Uri(xivapiUrl) - .withPath(Uri.Path / "item" / itemId.toString) - .withQuery(Uri.Query(Map("columns" -> "IsEquippable", "private_key" -> xivapiKey.getOrElse(""))))) + private def getIsTome(itemIds: Seq[Long]): Future[Map[Long, Boolean]] = { + val uri = Uri(xivapiUrl) + .withPath(Uri.Path / "item") + .withQuery(Uri.Query(Map( + "columns" -> Seq("ID", "IsEquippable").mkString(","), + "ids" -> itemIds.mkString(","), + "private_key" -> xivapiKey.getOrElse("") + ))) - sendRequest(uri.toOption.get, Ariyala.parseXivapiJson) + sendRequest(uri, Ariyala.parseXivapiJson) } private def sendRequest[T](uri: Uri, parser: JsObject => Future[T]): Future[T] = @@ -68,7 +72,7 @@ class Ariyala extends Actor with StrictLogging { .map(result => parser(result.parseJson.asJsObject)) .toMat(Sink.head)(Keep.right) .run().flatten - case _ => Future.failed(deserializationError("Invalid response from server")) + case other => Future.failed(new Error(s"Invalid response from server $other")) }.flatten } @@ -77,13 +81,14 @@ object Ariyala { case class GetBiS(link: String, job: Job.Job) - private def parseAriyalaJson(job: Job.Job)(js: JsObject): Future[Map[String, Long]] = { - try { + private def parseAriyalaJson(job: Job.Job)(js: JsObject) + (implicit executionContext: ExecutionContext): Future[Map[String, Long]] = + Future { val apiJob = js.fields.get("content") match { case Some(JsString(value)) => value case other => throw deserializationError(s"Invalid job name $other") } - Future.successful(js.fields.get("datasets") match { + js.fields.get("datasets") match { case Some(datasets: JsObject) => val fields = datasets.fields fields.getOrElse(apiJob, fields(job.toString)).asJsObject @@ -94,24 +99,30 @@ object Ariyala { case (acc, _) => acc } case other => throw deserializationError(s"Invalid json $other") - }) - } catch { - case e: Exception => Future.failed(e) + } } - } - private def parseAriyalaJsonToPieces(job: Job.Job, isTome: Long => Future[Boolean])(js: JsObject) + private def parseAriyalaJsonToPieces(job: Job.Job, isTome: Seq[Long] => Future[Map[Long, Boolean]])(js: JsObject) (implicit executionContext: ExecutionContext): Future[Seq[Piece]] = - parseAriyalaJson(job)(js).map { pieces => - Future.sequence(pieces.toSeq.map { - case (itemName, itemId) => isTome(itemId).map(Piece(itemName, _, job)) - }) - }.flatten + parseAriyalaJson(job)(js).flatMap { pieces => + isTome(pieces.values.toSeq).map { tomePieces => + pieces.view.mapValues(tomePieces).map { + case (piece, isTomePiece) => Piece(piece, isTomePiece, job) + }.toSeq + } + } - private def parseXivapiJson(js: JsObject): Future[Boolean] = - js.fields.get("IsEquippable") match { - case Some(JsNumber(value)) => Future.successful(value == 0) // don't ask - case other => Future.failed(deserializationError(s"Could not parse $other")) + private def parseXivapiJson(js: JsObject) + (implicit executionContext: ExecutionContext): Future[Map[Long, Boolean]] = + Future { + js.fields("Results") match { + case array: JsArray => + array.elements.map(_.asJsObject.getFields("ID", "IsEquippable") match { + case Seq(JsNumber(id), JsNumber(isTome)) => id.toLong -> (isTome == 0) + case other => throw deserializationError(s"Could not parse $other") + }).toMap + case other => throw deserializationError(s"Could not parse $other") + } } private def remapKey(key: String): Option[String] = key match { diff --git a/src/test/scala/me/arcanis/ffxivbis/models/Fixtures.scala b/src/test/scala/me/arcanis/ffxivbis/Fixtures.scala similarity index 93% rename from src/test/scala/me/arcanis/ffxivbis/models/Fixtures.scala rename to src/test/scala/me/arcanis/ffxivbis/Fixtures.scala index 5f79c84..bfe9df6 100644 --- a/src/test/scala/me/arcanis/ffxivbis/models/Fixtures.scala +++ b/src/test/scala/me/arcanis/ffxivbis/Fixtures.scala @@ -1,4 +1,6 @@ -package me.arcanis.ffxivbis.models +package me.arcanis.ffxivbis + +import me.arcanis.ffxivbis.models._ object Fixtures { lazy val bis: BiS = BiS( @@ -19,6 +21,7 @@ object Fixtures { ) lazy val link: String = "https://ffxiv.ariyala.com/19V5R" + lazy val link2: String = "https://ffxiv.ariyala.com/1A0WM" lazy val lootWeapon: Piece = Weapon(isTome = true, Job.AnyJob) lazy val lootBody: Piece = Body(isTome = false, Job.AnyJob) diff --git a/src/test/scala/me/arcanis/ffxivbis/models/Settings.scala b/src/test/scala/me/arcanis/ffxivbis/Settings.scala similarity index 96% rename from src/test/scala/me/arcanis/ffxivbis/models/Settings.scala rename to src/test/scala/me/arcanis/ffxivbis/Settings.scala index c9ff3f1..835cf28 100644 --- a/src/test/scala/me/arcanis/ffxivbis/models/Settings.scala +++ b/src/test/scala/me/arcanis/ffxivbis/Settings.scala @@ -1,4 +1,4 @@ -package me.arcanis.ffxivbis.models +package me.arcanis.ffxivbis import java.io.File diff --git a/src/test/scala/me/arcanis/ffxivbis/models/BiSTest.scala b/src/test/scala/me/arcanis/ffxivbis/models/BiSTest.scala index cd789ce..647f7c4 100644 --- a/src/test/scala/me/arcanis/ffxivbis/models/BiSTest.scala +++ b/src/test/scala/me/arcanis/ffxivbis/models/BiSTest.scala @@ -1,5 +1,6 @@ package me.arcanis.ffxivbis.models +import me.arcanis.ffxivbis.Fixtures import me.arcanis.ffxivbis.utils.Compare import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpecLike} diff --git a/src/test/scala/me/arcanis/ffxivbis/models/PartyTest.scala b/src/test/scala/me/arcanis/ffxivbis/models/PartyTest.scala index 4ffba03..695f5c9 100644 --- a/src/test/scala/me/arcanis/ffxivbis/models/PartyTest.scala +++ b/src/test/scala/me/arcanis/ffxivbis/models/PartyTest.scala @@ -1,5 +1,6 @@ package me.arcanis.ffxivbis.models +import me.arcanis.ffxivbis.Fixtures import me.arcanis.ffxivbis.utils.Compare import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpecLike} diff --git a/src/test/scala/me/arcanis/ffxivbis/models/PieceTest.scala b/src/test/scala/me/arcanis/ffxivbis/models/PieceTest.scala index 522fc17..7df02a9 100644 --- a/src/test/scala/me/arcanis/ffxivbis/models/PieceTest.scala +++ b/src/test/scala/me/arcanis/ffxivbis/models/PieceTest.scala @@ -1,5 +1,6 @@ package me.arcanis.ffxivbis.models +import me.arcanis.ffxivbis.Fixtures import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpecLike} class PieceTest extends WordSpecLike with Matchers with BeforeAndAfterAll { diff --git a/src/test/scala/me/arcanis/ffxivbis/models/PlayerIdTest.scala b/src/test/scala/me/arcanis/ffxivbis/models/PlayerIdTest.scala index 8f3e07f..ba9eff7 100644 --- a/src/test/scala/me/arcanis/ffxivbis/models/PlayerIdTest.scala +++ b/src/test/scala/me/arcanis/ffxivbis/models/PlayerIdTest.scala @@ -1,5 +1,6 @@ package me.arcanis.ffxivbis.models +import me.arcanis.ffxivbis.Fixtures import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpecLike} class PlayerIdTest extends WordSpecLike with Matchers with BeforeAndAfterAll { diff --git a/src/test/scala/me/arcanis/ffxivbis/models/PlayerTest.scala b/src/test/scala/me/arcanis/ffxivbis/models/PlayerTest.scala index 6f6035a..871cbdd 100644 --- a/src/test/scala/me/arcanis/ffxivbis/models/PlayerTest.scala +++ b/src/test/scala/me/arcanis/ffxivbis/models/PlayerTest.scala @@ -1,5 +1,6 @@ package me.arcanis.ffxivbis.models +import me.arcanis.ffxivbis.Fixtures import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpecLike} class PlayerTest extends WordSpecLike with Matchers with BeforeAndAfterAll { @@ -11,7 +12,7 @@ class PlayerTest extends WordSpecLike with Matchers with BeforeAndAfterAll { } "add loot" in { - Fixtures.playerEmpty.withLoot(Some(Fixtures.loot)).loot shouldEqual Fixtures.loot + Fixtures.playerEmpty.withLoot(Fixtures.loot).loot shouldEqual Fixtures.loot } } diff --git a/src/test/scala/me/arcanis/ffxivbis/models/UserTest.scala b/src/test/scala/me/arcanis/ffxivbis/models/UserTest.scala index f0911c9..b188446 100644 --- a/src/test/scala/me/arcanis/ffxivbis/models/UserTest.scala +++ b/src/test/scala/me/arcanis/ffxivbis/models/UserTest.scala @@ -1,5 +1,6 @@ package me.arcanis.ffxivbis.models +import me.arcanis.ffxivbis.Fixtures import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpecLike} class UserTest extends WordSpecLike with Matchers with BeforeAndAfterAll { diff --git a/src/test/scala/me/arcanis/ffxivbis/service/AriyalaTest.scala b/src/test/scala/me/arcanis/ffxivbis/service/AriyalaTest.scala index 82115b6..7679727 100644 --- a/src/test/scala/me/arcanis/ffxivbis/service/AriyalaTest.scala +++ b/src/test/scala/me/arcanis/ffxivbis/service/AriyalaTest.scala @@ -2,7 +2,8 @@ package me.arcanis.ffxivbis.service import akka.actor.ActorSystem import akka.testkit.{ImplicitSender, TestKit} -import me.arcanis.ffxivbis.models.{Fixtures, Job} +import me.arcanis.ffxivbis.Fixtures +import me.arcanis.ffxivbis.models._ import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpecLike} import scala.concurrent.duration._ diff --git a/src/test/scala/me/arcanis/ffxivbis/service/DatabaseBiSHandlerTest.scala b/src/test/scala/me/arcanis/ffxivbis/service/DatabaseBiSHandlerTest.scala index b41cabf..4204125 100644 --- a/src/test/scala/me/arcanis/ffxivbis/service/DatabaseBiSHandlerTest.scala +++ b/src/test/scala/me/arcanis/ffxivbis/service/DatabaseBiSHandlerTest.scala @@ -3,7 +3,8 @@ package me.arcanis.ffxivbis.service import akka.actor.ActorSystem import akka.pattern.ask import akka.testkit.{ImplicitSender, TestKit} -import me.arcanis.ffxivbis.models.{Fixtures, Hands, Job, Piece, Player, Settings} +import me.arcanis.ffxivbis.{Fixtures, Settings} +import me.arcanis.ffxivbis.models._ import me.arcanis.ffxivbis.storage.Migration import me.arcanis.ffxivbis.utils.Compare import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpecLike} diff --git a/src/test/scala/me/arcanis/ffxivbis/service/DatabaseLootHandlerTest.scala b/src/test/scala/me/arcanis/ffxivbis/service/DatabaseLootHandlerTest.scala index 8b834ae..6ad3d8d 100644 --- a/src/test/scala/me/arcanis/ffxivbis/service/DatabaseLootHandlerTest.scala +++ b/src/test/scala/me/arcanis/ffxivbis/service/DatabaseLootHandlerTest.scala @@ -3,7 +3,8 @@ package me.arcanis.ffxivbis.service import akka.actor.ActorSystem import akka.pattern.ask import akka.testkit.{ImplicitSender, TestKit} -import me.arcanis.ffxivbis.models.{Fixtures, Piece, Player, Settings} +import me.arcanis.ffxivbis.{Fixtures, Settings} +import me.arcanis.ffxivbis.models._ import me.arcanis.ffxivbis.storage.Migration import me.arcanis.ffxivbis.utils.Compare import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpecLike} diff --git a/src/test/scala/me/arcanis/ffxivbis/service/DatabasePartyHandlerTest.scala b/src/test/scala/me/arcanis/ffxivbis/service/DatabasePartyHandlerTest.scala index 5727f55..59caee8 100644 --- a/src/test/scala/me/arcanis/ffxivbis/service/DatabasePartyHandlerTest.scala +++ b/src/test/scala/me/arcanis/ffxivbis/service/DatabasePartyHandlerTest.scala @@ -2,7 +2,8 @@ package me.arcanis.ffxivbis.service import akka.actor.ActorSystem import akka.testkit.{ImplicitSender, TestKit} -import me.arcanis.ffxivbis.models.{Fixtures, Party, Settings} +import me.arcanis.ffxivbis.{Fixtures, Settings} +import me.arcanis.ffxivbis.models._ import me.arcanis.ffxivbis.storage.Migration import me.arcanis.ffxivbis.utils.Compare import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpecLike} diff --git a/src/test/scala/me/arcanis/ffxivbis/service/DatabaseUserHandlerTest.scala b/src/test/scala/me/arcanis/ffxivbis/service/DatabaseUserHandlerTest.scala index 66a7469..3a871a0 100644 --- a/src/test/scala/me/arcanis/ffxivbis/service/DatabaseUserHandlerTest.scala +++ b/src/test/scala/me/arcanis/ffxivbis/service/DatabaseUserHandlerTest.scala @@ -2,7 +2,7 @@ package me.arcanis.ffxivbis.service import akka.actor.ActorSystem import akka.testkit.{ImplicitSender, TestKit} -import me.arcanis.ffxivbis.models.{Fixtures, Settings} +import me.arcanis.ffxivbis.{Fixtures, Settings} import me.arcanis.ffxivbis.storage.Migration import me.arcanis.ffxivbis.utils.Compare import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpecLike} diff --git a/src/test/scala/me/arcanis/ffxivbis/service/LootSelectorTest.scala b/src/test/scala/me/arcanis/ffxivbis/service/LootSelectorTest.scala new file mode 100644 index 0000000..2d973e3 --- /dev/null +++ b/src/test/scala/me/arcanis/ffxivbis/service/LootSelectorTest.scala @@ -0,0 +1,89 @@ +package me.arcanis.ffxivbis.service + +import akka.actor.ActorSystem +import akka.pattern.ask +import akka.testkit.{ImplicitSender, TestKit} +import me.arcanis.ffxivbis.{Fixtures, Settings} +import me.arcanis.ffxivbis.models._ +import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpecLike} + +import scala.concurrent.Await +import scala.concurrent.duration._ +import scala.language.postfixOps + +class LootSelectorTest extends TestKit(ActorSystem("lootselector")) + with ImplicitSender with WordSpecLike with Matchers with BeforeAndAfterAll { + + private var default: Party = Party(Some(Fixtures.partyId), Settings.config(Map.empty)) + private var dnc: Player = Player(Fixtures.partyId, Job.DNC, "a nick", BiS(), Seq.empty, Some(Fixtures.link)) + private var drg: Player = Player(Fixtures.partyId, Job.DRG, "another nick", BiS(), Seq.empty, Some(Fixtures.link2)) + private val timeout: FiniteDuration = 60 seconds + + override def beforeAll(): Unit = { + val ariyala = system.actorOf(Ariyala.props) + + val dncSet = Await.result((ariyala ? Ariyala.GetBiS(Fixtures.link, Job.DNC) )(timeout).mapTo[BiS], timeout) + dnc = dnc.withBiS(Some(dncSet)) + + val drgSet = Await.result((ariyala ? Ariyala.GetBiS(Fixtures.link2, Job.DRG) )(timeout).mapTo[BiS], timeout) + drg = drg.withBiS(Some(drgSet)) + + default = default.withPlayer(dnc).withPlayer(drg) + system.stop(ariyala) + } + + "loot selector" must { + + "suggest loot by isRequired" in { + toPlayerId(default.suggestLoot(Head(isTome = false, Job.AnyJob))) shouldEqual Seq(dnc.playerId, drg.playerId) + } + + "suggest loot if a player already have it" in { + val piece = Body(isTome = false, Job.AnyJob) + val party = default.withPlayer(dnc.withLoot(piece)) + + toPlayerId(party.suggestLoot(piece)) shouldEqual Seq(drg.playerId, dnc.playerId) + } + + "suggest upgrade" in { + val party = default.withPlayer( + dnc.withBiS( + Some(dnc.bis.copy(weapon = Some(Weapon(isTome = true, Job.DNC)))) + ) + ) + + toPlayerId(party.suggestLoot(WeaponUpgrade)) shouldEqual Seq(dnc.playerId, drg.playerId) + } + + "suggest loot by priority" in { + val party = default.withPlayer(dnc.copy(priority = 2)) + + toPlayerId(party.suggestLoot(Body(isTome = false, Job.AnyJob))) shouldEqual Seq(drg.playerId, dnc.playerId) + } + + "suggest loot by bis pieces got" in { + val party = default.withPlayer(dnc.withLoot(Head(isTome = false, Job.AnyJob))) + + toPlayerId(party.suggestLoot(Body(isTome = false, Job.AnyJob))) shouldEqual Seq(drg.playerId, dnc.playerId) + } + + "suggest loot by this piece got" in { + val piece = Body(isTome = true, Job.AnyJob) + val party = default.withPlayer(dnc.withLoot(piece)) + + toPlayerId(party.suggestLoot(piece)) shouldEqual Seq(drg.playerId, dnc.playerId) + } + + "suggest loot by total piece got" in { + val piece = Body(isTome = true, Job.AnyJob) + val party = default + .withPlayer(dnc.withLoot(Seq(piece, piece))) + .withPlayer(drg.withLoot(piece)) + + toPlayerId(party.suggestLoot(piece)) shouldEqual Seq(drg.playerId, dnc.playerId) + } + + } + + private def toPlayerId(result: LootSelector.LootSelectorResult): Seq[PlayerId] = result.result.map(_.playerId) +}