crafted items support (#5)

This commit is contained in:
Evgenii Alekseev 2020-03-13 03:36:25 +03:00
parent 16ce0bf61c
commit 10c107d2c2
23 changed files with 307 additions and 172 deletions

View File

@ -0,0 +1,17 @@
-- loot
alter table loot add column piece_type text;
update loot set piece_type = 'Tome' where is_tome = 1;
update loot set piece_type = 'Savage' where is_tome = 0;
alter table loot alter column piece_type set not null;
alter table loot drop column is_tome;
-- bis
alter table bis add column piece_type text;
update bis set piece_type = 'Tome' where is_tome = 1;
update bis set piece_type = 'Savage' where is_tome = 0;
alter table bis alter column piece_type set not null;
alter table bis drop column is_tome;

View File

@ -0,0 +1,42 @@
-- loot
alter table loot add column piece_type text;
update loot set piece_type = 'Tome' where is_tome = 1;
update loot set piece_type = 'Savage' where is_tome = 0;
create table loot_new (
loot_id integer primary key autoincrement,
player_id integer not null,
created integer not null,
piece text not null,
piece_type text not null,
job text not null,
foreign key (player_id) references players(player_id) on delete cascade);
insert into loot_new select loot_id, player_id, created, piece, piece_type, job from loot;
drop index loot_owner_idx;
drop table loot;
alter table loot_new rename to loot;
create index loot_owner_idx on loot(player_id);
-- bis
alter table bis add column piece_type text;
update bis set piece_type = 'Tome' where is_tome = 1;
update bis set piece_type = 'Savage' where is_tome = 0;
create table bis_new (
player_id integer not null,
created integer not null,
piece text not null,
piece_type text not null,
job text not null,
foreign key (player_id) references players(player_id) on delete cascade);
insert into bis_new select player_id, created, piece, piece_type, job from bis;
drop index bis_piece_player_id_idx;
drop table bis;
alter table bis_new rename to bis;
create unique index bis_piece_player_id_idx on bis(player_id, piece);

View File

@ -16,12 +16,12 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.Operation
import javax.ws.rs._
import me.arcanis.ffxivbis.http.api.v1.json._
import me.arcanis.ffxivbis.models.{Job, Party, Permission, Piece}
import me.arcanis.ffxivbis.models.{Job, Party, Permission, Piece, PieceType}
@Path("api/v1")
class TypesEndpoint(config: Config) extends JsonSupport {
def route: Route = getJobs ~ getPermissions ~ getPieces ~ getPriority
def route: Route = getJobs ~ getPermissions ~ getPieces ~ getPieceTypes ~ getPriority
@GET
@Path("types/jobs")
@ -86,6 +86,27 @@ class TypesEndpoint(config: Config) extends JsonSupport {
}
}
@GET
@Path("types/pieces/types")
@Produces(value = Array("application/json"))
@Operation(summary = "piece types list", description = "Returns the available piece types",
responses = Array(
new ApiResponse(responseCode = "200", description = "List of available piece types",
content = Array(new Content(
array = new ArraySchema(schema = new Schema(implementation = classOf[String]))
))),
new ApiResponse(responseCode = "500", description = "Internal server error",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
),
tags = Array("types"),
)
def getPieceTypes: Route =
path("types" / "pieces" / "types") {
get {
complete(PieceType.available.map(_.toString))
}
}
@GET
@Path("types/priority")
@Produces(value = Array("application/json"))

View File

@ -9,16 +9,16 @@
package me.arcanis.ffxivbis.http.api.v1.json
import io.swagger.v3.oas.annotations.media.Schema
import me.arcanis.ffxivbis.models.{Job, Piece}
import me.arcanis.ffxivbis.models.{Job, Piece, PieceType}
case class PieceResponse(
@Schema(description = "is piece tome gear", required = true) isTome: Boolean,
@Schema(description = "piece type", required = true) pieceType: String,
@Schema(description = "job name to which piece belong or AnyJob", required = true, example = "DNC") job: String,
@Schema(description = "piece name", required = true, example = "body") piece: String) {
def toPiece: Piece = Piece(piece, isTome, Job.withName(job))
def toPiece: Piece = Piece(piece, PieceType.withName(pieceType), Job.withName(job))
}
object PieceResponse {
def fromPiece(piece: Piece): PieceResponse =
PieceResponse(piece.isTome, piece.job.toString, piece.piece)
PieceResponse(piece.pieceType.toString, piece.job.toString, piece.piece)
}

View File

@ -14,7 +14,7 @@ import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._
import akka.util.Timeout
import me.arcanis.ffxivbis.http.{Authorization, BiSHelper}
import me.arcanis.ffxivbis.models.{Piece, Player, PlayerId}
import me.arcanis.ffxivbis.models.{Piece, PieceType, Player, PlayerId}
import scala.concurrent.{ExecutionContext, Future}
import scala.util.Try
@ -46,10 +46,10 @@ class BiSView(override val storage: ActorRef, override val ariyala: ActorRef)(im
extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
post {
formFields("player".as[String], "piece".as[String].?, "is_tome".as[String].?, "link".as[String].?, "action".as[String]) {
(player, maybePiece, maybeIsTome, maybeLink, action) =>
onComplete(modifyBiSCall(partyId, player, maybePiece, maybeIsTome, maybeLink, action)) {
case _ => redirect(s"/party/$partyId/bis", StatusCodes.Found)
formFields("player".as[String], "piece".as[String].?, "piece_type".as[String].?, "link".as[String].?, "action".as[String]) {
(player, maybePiece, maybePieceType, maybeLink, action) =>
onComplete(modifyBiSCall(partyId, player, maybePiece, maybePieceType, maybeLink, action)) { _ =>
redirect(s"/party/$partyId/bis", StatusCodes.Found)
}
}
}
@ -58,25 +58,25 @@ class BiSView(override val storage: ActorRef, override val ariyala: ActorRef)(im
}
private def modifyBiSCall(partyId: String, player: String,
maybePiece: Option[String], maybeIsTome: Option[String],
maybePiece: Option[String], maybePieceType: Option[String],
maybeLink: Option[String], action: String)
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = {
import me.arcanis.ffxivbis.utils.Implicits._
def getPiece(playerId: PlayerId, piece: String, pieceType: String) =
Try(Piece(piece, PieceType.withName(pieceType), playerId.job)).toOption
def getPiece(playerId: PlayerId, piece: String) =
Try(Piece(piece, maybeIsTome, playerId.job)).toOption
def bisAction(playerId: PlayerId, piece: String, pieceType: String)(fn: Piece => Future[Int]) =
getPiece(playerId, piece, pieceType) match {
case Some(item) => fn(item).map(_ => ())
case _ => Future.failed(new Error(s"Could not construct piece from `$piece ($pieceType)`"))
}
PlayerId(partyId, player) match {
case Some(playerId) => (maybePiece, action, maybeLink) match {
case (Some(piece), "add", _) => getPiece(playerId, piece) match {
case Some(item) => addPieceBiS(playerId, item).map(_ => ())
case _ => Future.failed(new Error(s"Could not construct piece from `$piece`"))
}
case (Some(piece), "remove", _) => getPiece(playerId, piece) match {
case Some(item) => removePieceBiS(playerId, item).map(_ => ())
case _ => Future.failed(new Error(s"Could not construct piece from `$piece`"))
}
case (_, "create", Some(link)) => putBiS(playerId, link).map(_ => ())
case Some(playerId) => (maybePiece, maybePieceType, action, maybeLink) match {
case (Some(piece), Some(pieceType), "add", _) =>
bisAction(playerId, piece, pieceType) { item => addPieceBiS(playerId, item) }
case (Some(piece), Some(pieceType), "remove", _) =>
bisAction(playerId, piece, pieceType) { item => removePieceBiS(playerId, item) }
case (_, _, "create", Some(link)) => putBiS(playerId, link).map(_ => ())
case _ => Future.failed(new Error(s"Could not perform $action"))
}
case _ => Future.failed(new Error(s"Could not construct player id from `$player`"))
@ -107,8 +107,8 @@ object BiSView {
(for (player <- party) yield option(player.playerId.toString)),
select(name:="piece", id:="piece", title:="piece")
(for (piece <- Piece.available) yield option(piece)),
input(name:="is_tome", id:="is_tome", title:="is tome", `type`:="checkbox"),
label(`for`:="is_tome")("is tome gear"),
select(name:="piece_type", id:="piece_type", title:="piece type")
(for (pieceType <- PieceType.available) yield option(pieceType.toString)),
input(name:="action", id:="action", `type`:="hidden", value:="add"),
input(name:="add", id:="add", `type`:="submit", value:="add")
),
@ -125,18 +125,18 @@ object BiSView {
tr(
th("player"),
th("piece"),
th("is tome"),
th("piece type"),
th("")
),
for (player <- party; piece <- player.bis.pieces) yield tr(
td(`class`:="include_search")(player.playerId.toString),
td(`class`:="include_search")(piece.piece),
td(piece.isTomeToString),
td(piece.pieceType.toString),
td(
form(action:=s"/party/$partyId/bis", method:="post")(
input(name:="player", id:="player", `type`:="hidden", value:=player.playerId.toString),
input(name:="piece", id:="piece", `type`:="hidden", value:=piece.piece),
input(name:="is_tome", id:="is_tome", `type`:="hidden", value:=piece.isTomeToString),
input(name:="piece_type", id:="piece_type", `type`:="hidden", value:=piece.pieceType.toString),
input(name:="action", id:="action", `type`:="hidden", value:="remove"),
input(name:="remove", id:="remove", `type`:="submit", value:="x")
)

View File

@ -14,7 +14,7 @@ import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route
import akka.util.Timeout
import me.arcanis.ffxivbis.http.{Authorization, LootHelper}
import me.arcanis.ffxivbis.models.{Job, Piece, PlayerIdWithCounters}
import me.arcanis.ffxivbis.models.{Job, Piece, PieceType, PlayerIdWithCounters}
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success, Try}
@ -43,9 +43,8 @@ class LootSuggestView(override val storage: ActorRef)(implicit timeout: Timeout)
extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
post {
formFields("piece".as[String], "job".as[String], "is_tome".as[String].?) { (piece, job, maybeTome) =>
import me.arcanis.ffxivbis.utils.Implicits._
val maybePiece = Try(Piece(piece, maybeTome, Job.withName(job))).toOption
formFields("piece".as[String], "job".as[String], "piece_type".as[String]) { (piece, job, pieceType) =>
val maybePiece = Try(Piece(piece, PieceType.withName(pieceType), Job.withName(job))).toOption
onComplete(suggestLootCall(partyId, maybePiece)) {
case Success(players) =>
@ -92,8 +91,8 @@ object LootSuggestView {
(for (piece <- Piece.available) yield option(piece)),
select(name:="job", id:="job", title:="job")
(for (job <- Job.availableWithAnyJob) yield option(job.toString)),
input(name:="is_tome", id:="is_tome", title:="is tome", `type`:="checkbox"),
label(`for`:="is_tome")("is tome gear"),
select(name:="piece_type", id:="piece_type", title:="piece type")
(for (pieceType <- PieceType.available) yield option(pieceType.toString)),
input(name:="suggest", id:="suggest", `type`:="submit", value:="suggest")
),
@ -116,7 +115,7 @@ object LootSuggestView {
form(action:=s"/party/$partyId/loot", method:="post")(
input(name:="player", id:="player", `type`:="hidden", value:=player.playerId.toString),
input(name:="piece", id:="piece", `type`:="hidden", value:=piece.map(_.piece).getOrElse("")),
input(name:="is_tome", id:="is_tome", `type`:="hidden", value:=piece.map(_.isTomeToString).getOrElse("")),
input(name:="piece_type", id:="piece_type", `type`:="hidden", value:=piece.map(_.pieceType.toString).getOrElse("")),
input(name:="action", id:="action", `type`:="hidden", value:="add"),
input(name:="add", id:="add", `type`:="submit", value:="add")
)

View File

@ -14,7 +14,7 @@ import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route
import akka.util.Timeout
import me.arcanis.ffxivbis.http.{Authorization, LootHelper}
import me.arcanis.ffxivbis.models.{Piece, Player, PlayerId}
import me.arcanis.ffxivbis.models.{Piece, PieceType, Player, PlayerId}
import scala.concurrent.{ExecutionContext, Future}
import scala.util.Try
@ -46,10 +46,10 @@ class LootView (override val storage: ActorRef)(implicit timeout: Timeout)
extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
post {
formFields("player".as[String], "piece".as[String], "is_tome".as[String].?, "action".as[String]) {
(player, maybePiece, maybeIsTome, action) =>
onComplete(modifyLootCall(partyId, player, maybePiece, maybeIsTome, action)) {
case _ => redirect(s"/party/$partyId/loot", StatusCodes.Found)
formFields("player".as[String], "piece".as[String], "piece_type".as[String], "action".as[String]) {
(player, piece, pieceType, action) =>
onComplete(modifyLootCall(partyId, player, piece, pieceType, action)) { _ =>
redirect(s"/party/$partyId/loot", StatusCodes.Found)
}
}
}
@ -57,20 +57,17 @@ class LootView (override val storage: ActorRef)(implicit timeout: Timeout)
}
}
private def modifyLootCall(partyId: String, player: String,
maybePiece: String, maybeIsTome: Option[String],
action: String)
private def modifyLootCall(partyId: String, player: String, maybePiece: String,
maybePieceType: String, action: String)
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = {
import me.arcanis.ffxivbis.utils.Implicits._
def getPiece(playerId: PlayerId) =
Try(Piece(maybePiece, maybeIsTome, playerId.job)).toOption
Try(Piece(maybePiece, PieceType.withName(maybePieceType), playerId.job)).toOption
PlayerId(partyId, player) match {
case Some(playerId) => (getPiece(playerId), action) match {
case (Some(piece), "add") => addPieceLoot(playerId, piece).map(_ => ())
case (Some(piece), "remove") => removePieceLoot(playerId, piece).map(_ => ())
case _ => Future.failed(new Error(s"Could not construct piece from `$maybePiece`"))
case _ => Future.failed(new Error(s"Could not construct piece from `$maybePiece ($maybePieceType)`"))
}
case _ => Future.failed(new Error(s"Could not construct player id from `$player`"))
}
@ -100,8 +97,8 @@ object LootView {
(for (player <- party) yield option(player.playerId.toString)),
select(name:="piece", id:="piece", title:="piece")
(for (piece <- Piece.available) yield option(piece)),
input(name:="is_tome", id:="is_tome", title:="is tome", `type`:="checkbox"),
label(`for`:="is_tome")("is tome gear"),
select(name:="piece_type", id:="piece_type", title:="piece type")
(for (pieceType <- PieceType.available) yield option(pieceType.toString)),
input(name:="action", id:="action", `type`:="hidden", value:="add"),
input(name:="add", id:="add", `type`:="submit", value:="add")
),
@ -110,20 +107,20 @@ object LootView {
tr(
th("player"),
th("piece"),
th("is tome"),
th("piece type"),
th("timestamp"),
th("")
),
for (player <- party; loot <- player.loot) yield tr(
td(`class`:="include_search")(player.playerId.toString),
td(`class`:="include_search")(loot.piece.piece),
td(loot.piece.isTomeToString),
td(loot.piece.pieceType.toString),
td(loot.timestamp.toString),
td(
form(action:=s"/party/$partyId/loot", method:="post")(
input(name:="player", id:="player", `type`:="hidden", value:=player.playerId.toString),
input(name:="piece", id:="piece", `type`:="hidden", value:=loot.piece.piece),
input(name:="is_tome", id:="is_tome", `type`:="hidden", value:=loot.piece.isTomeToString),
input(name:="piece_type", id:="piece_type", `type`:="hidden", value:=loot.piece.pieceType.toString),
input(name:="action", id:="action", `type`:="hidden", value:="remove"),
input(name:="remove", id:="remove", `type`:="submit", value:="x")
)

View File

@ -101,9 +101,9 @@ object Job {
lazy val availableWithAnyJob: Seq[Job] = available.prepended(AnyJob)
def withName(job: String): Job.Job =
availableWithAnyJob.find(_.toString.equalsIgnoreCase(job.toUpperCase)) match {
availableWithAnyJob.find(_.toString.equalsIgnoreCase(job)) match {
case Some(value) => value
case None if job.isEmpty => AnyJob
case _ => throw new IllegalArgumentException("Invalid or unknown job")
case _ => throw new IllegalArgumentException(s"Invalid or unknown job $job")
}
}

View File

@ -9,15 +9,14 @@
package me.arcanis.ffxivbis.models
sealed trait Piece {
def isTome: Boolean
def pieceType: PieceType.PieceType
def job: Job.Job
def piece: String
def withJob(other: Job.Job): Piece
def isTomeToString: String = if (isTome) "yes" else "no"
def upgrade: Option[PieceUpgrade] = this match {
case _ if !isTome => None
case _ if pieceType != PieceType.Tome => None
case _: Waist => Some(AccessoryUpgrade)
case _: PieceAccessory => Some(AccessoryUpgrade)
case _: PieceBody => Some(BodyUpgrade)
@ -28,59 +27,59 @@ sealed trait Piece {
trait PieceAccessory extends Piece
trait PieceBody extends Piece
trait PieceUpgrade extends Piece {
val isTome: Boolean = true
val pieceType: PieceType.PieceType = PieceType.Tome
val job: Job.Job = Job.AnyJob
def withJob(other: Job.Job): Piece = this
}
trait PieceWeapon extends Piece
case class Weapon(override val isTome: Boolean, override val job: Job.Job) extends PieceWeapon {
case class Weapon(override val pieceType: PieceType.PieceType, override val job: Job.Job) extends PieceWeapon {
val piece: String = "weapon"
def withJob(other: Job.Job): Piece = copy(job = other)
}
case class Head(override val isTome: Boolean, override val job: Job.Job) extends PieceBody {
case class Head(override val pieceType: PieceType.PieceType, override val job: Job.Job) extends PieceBody {
val piece: String = "head"
def withJob(other: Job.Job): Piece = copy(job = other)
}
case class Body(override val isTome: Boolean, override val job: Job.Job) extends PieceBody {
case class Body(override val pieceType: PieceType.PieceType, override val job: Job.Job) extends PieceBody {
val piece: String = "body"
def withJob(other: Job.Job): Piece = copy(job = other)
}
case class Hands(override val isTome: Boolean, override val job: Job.Job) extends PieceBody {
case class Hands(override val pieceType: PieceType.PieceType, override val job: Job.Job) extends PieceBody {
val piece: String = "hands"
def withJob(other: Job.Job): Piece = copy(job = other)
}
case class Waist(override val isTome: Boolean, override val job: Job.Job) extends PieceBody {
case class Waist(override val pieceType: PieceType.PieceType, override val job: Job.Job) extends PieceBody {
val piece: String = "waist"
def withJob(other: Job.Job): Piece = copy(job = other)
}
case class Legs(override val isTome: Boolean, override val job: Job.Job) extends PieceBody {
case class Legs(override val pieceType: PieceType.PieceType, override val job: Job.Job) extends PieceBody {
val piece: String = "legs"
def withJob(other: Job.Job): Piece = copy(job = other)
}
case class Feet(override val isTome: Boolean, override val job: Job.Job) extends PieceBody {
case class Feet(override val pieceType: PieceType.PieceType, override val job: Job.Job) extends PieceBody {
val piece: String = "feet"
def withJob(other: Job.Job): Piece = copy(job = other)
}
case class Ears(override val isTome: Boolean, override val job: Job.Job) extends PieceAccessory {
case class Ears(override val pieceType: PieceType.PieceType, override val job: Job.Job) extends PieceAccessory {
val piece: String = "ears"
def withJob(other: Job.Job): Piece = copy(job = other)
}
case class Neck(override val isTome: Boolean, override val job: Job.Job) extends PieceAccessory {
case class Neck(override val pieceType: PieceType.PieceType, override val job: Job.Job) extends PieceAccessory {
val piece: String = "neck"
def withJob(other: Job.Job): Piece = copy(job = other)
}
case class Wrist(override val isTome: Boolean, override val job: Job.Job) extends PieceAccessory {
case class Wrist(override val pieceType: PieceType.PieceType, override val job: Job.Job) extends PieceAccessory {
val piece: String = "wrist"
def withJob(other: Job.Job): Piece = copy(job = other)
}
case class Ring(override val isTome: Boolean, override val job: Job.Job, override val piece: String = "ring")
case class Ring(override val pieceType: PieceType.PieceType, override val job: Job.Job, override val piece: String = "ring")
extends PieceAccessory {
def withJob(other: Job.Job): Piece = copy(job = other)
override def equals(obj: Any): Boolean = obj match {
case Ring(thatIsTome, thatJob, _) => (thatIsTome == isTome) && (thatJob == job)
case Ring(thatPieceType, thatJob, _) => (thatPieceType == pieceType) && (thatJob == job)
case _ => false
}
}
@ -96,19 +95,19 @@ case object WeaponUpgrade extends PieceUpgrade {
}
object Piece {
def apply(piece: String, isTome: Boolean, job: Job.Job = Job.AnyJob): Piece =
def apply(piece: String, pieceType: PieceType.PieceType, job: Job.Job = Job.AnyJob): Piece =
piece.toLowerCase match {
case "weapon" => Weapon(isTome, job)
case "head" => Head(isTome, job)
case "body" => Body(isTome, job)
case "hands" => Hands(isTome, job)
case "waist" => Waist(isTome, job)
case "legs" => Legs(isTome, job)
case "feet" => Feet(isTome, job)
case "ears" => Ears(isTome, job)
case "neck" => Neck(isTome, job)
case "wrist" => Wrist(isTome, job)
case ring @ ("ring" | "left ring" | "right ring") => Ring(isTome, job, ring)
case "weapon" => Weapon(pieceType, job)
case "head" => Head(pieceType, job)
case "body" => Body(pieceType, job)
case "hands" => Hands(pieceType, job)
case "waist" => Waist(pieceType, job)
case "legs" => Legs(pieceType, job)
case "feet" => Feet(pieceType, job)
case "ears" => Ears(pieceType, job)
case "neck" => Neck(pieceType, job)
case "wrist" => Wrist(pieceType, job)
case ring @ ("ring" | "left ring" | "right ring") => Ring(pieceType, job, ring)
case "accessory upgrade" => AccessoryUpgrade
case "body upgrade" => BodyUpgrade
case "weapon upgrade" => WeaponUpgrade

View File

@ -0,0 +1,18 @@
package me.arcanis.ffxivbis.models
object PieceType {
sealed trait PieceType
case object Crafted extends PieceType
case object Tome extends PieceType
case object Savage extends PieceType
lazy val available: Seq[PieceType] =
Seq(Crafted, Tome, Savage)
def withName(pieceType: String): PieceType =
available.find(_.toString.equalsIgnoreCase(pieceType)) match {
case Some(value) => value
case _ => throw new IllegalArgumentException(s"Invalid or unknown piece type $pieceType")
}
}

View File

@ -46,7 +46,7 @@ case class Player(id: Long,
}
}
def bisCountTotal(piece: Option[Piece]): Int = bis.pieces.count(!_.isTome)
def bisCountTotal(piece: Option[Piece]): Int = bis.pieces.count(_.pieceType == PieceType.Savage)
def lootCount(piece: Option[Piece]): Int = piece match {
case Some(p) => loot.count(_.piece == p)
case None => lootCountTotal(piece)

View File

@ -18,7 +18,7 @@ import akka.stream.ActorMaterializer
import akka.stream.scaladsl.{Keep, Sink}
import akka.util.ByteString
import com.typesafe.scalalogging.StrictLogging
import me.arcanis.ffxivbis.models.{BiS, Job, Piece}
import me.arcanis.ffxivbis.models.{BiS, Job, Piece, PieceType}
import spray.json._
import scala.concurrent.{ExecutionContext, Future}
@ -54,10 +54,10 @@ class Ariyala extends Actor with StrictLogging {
.withPath(Uri.Path / "store.app")
.withQuery(Uri.Query(Map("identifier" -> id)))
sendRequest(uri, Ariyala.parseAriyalaJsonToPieces(job, getIsTome))
sendRequest(uri, Ariyala.parseAriyalaJsonToPieces(job, getPieceType))
}
private def getIsTome(itemIds: Seq[Long]): Future[Map[Long, Boolean]] = {
private def getPieceType(itemIds: Seq[Long]): Future[Map[Long, PieceType.PieceType]] = {
val uriForItems = Uri(xivapiUrl)
.withPath(Uri.Path / "item")
.withQuery(Uri.Query(Map(
@ -77,7 +77,7 @@ class Ariyala extends Actor with StrictLogging {
"private_key" -> xivapiKey.getOrElse("")
)))
sendRequest(uriForShops, Ariyala.parseXivapiJsonToTome(shops))
sendRequest(uriForShops, Ariyala.parseXivapiJsonToType(shops))
}
}
@ -120,28 +120,34 @@ object Ariyala {
}
}
private def parseAriyalaJsonToPieces(job: Job.Job, isTome: Seq[Long] => Future[Map[Long, Boolean]])(js: JsObject)
private def parseAriyalaJsonToPieces(job: Job.Job, pieceTypes: Seq[Long] => Future[Map[Long, PieceType.PieceType]])
(js: JsObject)
(implicit executionContext: ExecutionContext): Future[Seq[Piece]] =
parseAriyalaJson(job)(js).flatMap { pieces =>
isTome(pieces.values.toSeq).map { tomePieces =>
pieces.view.mapValues(tomePieces).map {
case (piece, isTomePiece) => Piece(piece, isTomePiece, job)
pieceTypes(pieces.values.toSeq).map { types =>
pieces.view.mapValues(types).map {
case (piece, pieceType) => Piece(piece, pieceType, job)
}.toSeq
}
}
private def parseXivapiJsonToShop(js: JsObject)
(implicit executionContext: ExecutionContext): Future[Map[Long, (String, Long)]] = {
def extractTraderId(js: JsObject) =
js.fields("SpecialShop").asJsObject
.fields.collectFirst {
case (shopName, JsArray(array)) if shopName.startsWith("ItemReceive") =>
val shopId = array.head match {
case JsNumber(id) => id.toLong
case other => throw deserializationError(s"Could not parse $other")
def extractTraderId(js: JsObject) = {
js.fields
.get("Recipe").map(_ => "crafted" -> -1L) // you can craft this item
.orElse { // lets try shop items
js.fields("SpecialShop").asJsObject
.fields.collectFirst {
case (shopName, JsArray(array)) if shopName.startsWith("ItemReceive") =>
val shopId = array.head match {
case JsNumber(id) => id.toLong
case other => throw deserializationError(s"Could not parse $other")
}
shopName.replace("ItemReceive", "") -> shopId
}
(shopName.replace("ItemReceive", ""), shopId)
}.getOrElse(throw deserializationError(s"Could not parse $js"))
}.getOrElse(throw deserializationError(s"Could not parse $js"))
}
Future {
js.fields("Results") match {
@ -155,8 +161,8 @@ object Ariyala {
}
}
private def parseXivapiJsonToTome(shops: Map[Long, (String, Long)])(js: JsObject)
(implicit executionContext: ExecutionContext): Future[Map[Long, Boolean]] =
private def parseXivapiJsonToType(shops: Map[Long, (String, Long)])(js: JsObject)
(implicit executionContext: ExecutionContext): Future[Map[Long, PieceType.PieceType]] =
Future {
val shopMap = js.fields("Results") match {
case array: JsArray =>
@ -170,14 +176,20 @@ object Ariyala {
}
shops.map { case (itemId, (index, shopId)) =>
val isTome = Try(shopMap(shopId).fields(s"ItemCost$index").asJsObject).toOption.getOrElse(throw new Exception(s"${shopMap(shopId).fields(s"ItemCost$index")}, $index"))
.getFields("IsUnique", "StackSize") match {
case Seq(JsNumber(isUnique), JsNumber(stackSize)) =>
// either upgraded gear or tomes found
isUnique == 1 || stackSize.toLong == 2000
case other => throw deserializationError(s"Could not parse $other")
}
itemId -> isTome
val pieceType =
if (index == "crafted" && shopId == -1) PieceType.Crafted
else {
Try(shopMap(shopId).fields(s"ItemCost$index").asJsObject)
.toOption
.getOrElse(throw new Exception(s"${shopMap(shopId).fields(s"ItemCost$index")}, $index"))
.getFields("IsUnique", "StackSize") match {
case Seq(JsNumber(isUnique), JsNumber(stackSize)) =>
if (isUnique == 1 || stackSize.toLong == 2000) PieceType.Tome // either upgraded gear or tomes found
else PieceType.Savage
case other => throw deserializationError(s"Could not parse $other")
}
}
itemId -> pieceType
}
}

View File

@ -10,7 +10,7 @@ package me.arcanis.ffxivbis.storage
import java.time.Instant
import me.arcanis.ffxivbis.models.{Job, Loot, Piece}
import me.arcanis.ffxivbis.models.{Job, Loot, Piece, PieceType}
import slick.lifted.ForeignKeyQuery
import scala.concurrent.Future
@ -18,24 +18,27 @@ import scala.concurrent.Future
trait BiSProfile { this: DatabaseProfile =>
import dbConfig.profile.api._
case class BiSRep(playerId: Long, created: Long, piece: String, isTome: Int, job: String) {
def toLoot: Loot = Loot(playerId, Piece(piece, isTome == 1, Job.withName(job)), Instant.ofEpochMilli(created))
case class BiSRep(playerId: Long, created: Long, piece: String,
pieceType: String, job: String) {
def toLoot: Loot = Loot(
playerId, Piece(piece, PieceType.withName(pieceType), Job.withName(job)),
Instant.ofEpochMilli(created))
}
object BiSRep {
def fromPiece(playerId: Long, piece: Piece) =
BiSRep(playerId, DatabaseProfile.now, piece.piece, if (piece.isTome) 1 else 0,
piece.job.toString)
def fromPiece(playerId: Long, piece: Piece): BiSRep =
BiSRep(playerId, DatabaseProfile.now, piece.piece,
piece.pieceType.toString, piece.job.toString)
}
class BiSPieces(tag: Tag) extends Table[BiSRep](tag, "bis") {
def playerId: Rep[Long] = column[Long]("player_id", O.PrimaryKey)
def created: Rep[Long] = column[Long]("created")
def piece: Rep[String] = column[String]("piece", O.PrimaryKey)
def isTome: Rep[Int] = column[Int]("is_tome")
def pieceType: Rep[String] = column[String]("piece_type")
def job: Rep[String] = column[String]("job")
def * =
(playerId, created, piece, isTome, job) <> ((BiSRep.apply _).tupled, BiSRep.unapply)
(playerId, created, piece, pieceType, job) <> ((BiSRep.apply _).tupled, BiSRep.unapply)
def fkPlayerId: ForeignKeyQuery[Players, PlayerRep] =
foreignKey("player_id", playerId, playersTable)(_.playerId, onDelete = ForeignKeyAction.Cascade)

View File

@ -10,7 +10,7 @@ package me.arcanis.ffxivbis.storage
import java.time.Instant
import me.arcanis.ffxivbis.models.{Job, Loot, Piece}
import me.arcanis.ffxivbis.models.{Job, Loot, Piece, PieceType}
import slick.lifted.{ForeignKeyQuery, Index}
import scala.concurrent.Future
@ -18,14 +18,17 @@ import scala.concurrent.Future
trait LootProfile { this: DatabaseProfile =>
import dbConfig.profile.api._
case class LootRep(lootId: Option[Long], playerId: Long, created: Long, piece: String,
isTome: Int, job: String) {
def toLoot: Loot = Loot(playerId, Piece(piece, isTome == 1, Job.withName(job)), Instant.ofEpochMilli(created))
case class LootRep(lootId: Option[Long], playerId: Long, created: Long,
piece: String, pieceType: String, job: String) {
def toLoot: Loot = Loot(
playerId,
Piece(piece, PieceType.withName(pieceType), Job.withName(job)),
Instant.ofEpochMilli(created))
}
object LootRep {
def fromPiece(playerId: Long, piece: Piece) =
LootRep(None, playerId, DatabaseProfile.now, piece.piece, if (piece.isTome) 1 else 0,
piece.job.toString)
def fromPiece(playerId: Long, piece: Piece): LootRep =
LootRep(None, playerId, DatabaseProfile.now, piece.piece,
piece.pieceType.toString, piece.job.toString)
}
class LootPieces(tag: Tag) extends Table[LootRep](tag, "loot") {
@ -33,11 +36,11 @@ trait LootProfile { this: DatabaseProfile =>
def playerId: Rep[Long] = column[Long]("player_id")
def created: Rep[Long] = column[Long]("created")
def piece: Rep[String] = column[String]("piece")
def isTome: Rep[Int] = column[Int]("is_tome")
def pieceType: Rep[String] = column[String]("piece_type")
def job: Rep[String] = column[String]("job")
def * =
(lootId.?, playerId, created, piece, isTome, job) <> ((LootRep.apply _).tupled, LootRep.unapply)
(lootId.?, playerId, created, piece, pieceType, job) <> ((LootRep.apply _).tupled, LootRep.unapply)
def fkPlayerId: ForeignKeyQuery[Players, PlayerRep] =
foreignKey("player_id", playerId, playersTable)(_.playerId, onDelete = ForeignKeyAction.Cascade)

View File

@ -15,7 +15,8 @@ import scala.concurrent.Future
trait PartyProfile { this: DatabaseProfile =>
import dbConfig.profile.api._
case class PartyRep(partyId: Option[Long], partyName: String, partyAlias: Option[String]) {
case class PartyRep(partyId: Option[Long], partyName: String,
partyAlias: Option[String]) {
def toDescription: PartyDescription = PartyDescription(partyName, partyAlias)
}
object PartyRep {

View File

@ -15,10 +15,11 @@ import scala.concurrent.Future
trait PlayersProfile { this: DatabaseProfile =>
import dbConfig.profile.api._
case class PlayerRep(partyId: String, playerId: Option[Long], created: Long, nick: String,
job: String, link: Option[String], priority: Int) {
case class PlayerRep(partyId: String, playerId: Option[Long], created: Long,
nick: String, job: String, link: Option[String], priority: Int) {
def toPlayer: Player =
Player(playerId.getOrElse(-1), partyId, Job.withName(job), nick, BiS(Seq.empty), List.empty, link, priority)
Player(playerId.getOrElse(-1), partyId, Job.withName(job), nick,
BiS(Seq.empty), List.empty, link, priority)
}
object PlayerRep {
def fromPlayer(player: Player, id: Option[Long]): PlayerRep =

View File

@ -16,8 +16,8 @@ import scala.concurrent.Future
trait UsersProfile { this: DatabaseProfile =>
import dbConfig.profile.api._
case class UserRep(partyId: String, userId: Option[Long], username: String, password: String,
permission: String) {
case class UserRep(partyId: String, userId: Option[Long], username: String,
password: String, permission: String) {
def toUser: User = User(partyId, username, password, Permission.withName(permission))
}
object UserRep {

View File

@ -5,33 +5,33 @@ import me.arcanis.ffxivbis.models._
object Fixtures {
lazy val bis: BiS = BiS(
Seq(
Weapon(isTome = false ,Job.DNC),
Head(isTome = false, Job.DNC),
Body(isTome = false, Job.DNC),
Hands(isTome = true, Job.DNC),
Waist(isTome = true, Job.DNC),
Legs(isTome = true, Job.DNC),
Feet(isTome = false, Job.DNC),
Ears(isTome = false, Job.DNC),
Neck(isTome = true, Job.DNC),
Wrist(isTome = false, Job.DNC),
Ring(isTome = true, Job.DNC, "left ring"),
Ring(isTome = true, Job.DNC, "right ring")
Weapon(pieceType = PieceType.Savage ,Job.DNC),
Head(pieceType = PieceType.Savage, Job.DNC),
Body(pieceType = PieceType.Savage, Job.DNC),
Hands(pieceType = PieceType.Tome, Job.DNC),
Waist(pieceType = PieceType.Tome, Job.DNC),
Legs(pieceType = PieceType.Tome, Job.DNC),
Feet(pieceType = PieceType.Savage, Job.DNC),
Ears(pieceType = PieceType.Savage, Job.DNC),
Neck(pieceType = PieceType.Tome, Job.DNC),
Wrist(pieceType = PieceType.Savage, Job.DNC),
Ring(pieceType = PieceType.Tome, Job.DNC, "left ring"),
Ring(pieceType = PieceType.Tome, Job.DNC, "right ring")
)
)
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)
lazy val lootHands: Piece = Hands(isTome = true, Job.AnyJob)
lazy val lootWaist: Piece = Waist(isTome = true, Job.AnyJob)
lazy val lootLegs: Piece = Legs(isTome = false, Job.AnyJob)
lazy val lootEars: Piece = Ears(isTome = false, Job.AnyJob)
lazy val lootRing: Piece = Ring(isTome = true, Job.AnyJob)
lazy val lootLeftRing: Piece = Ring(isTome = true, Job.AnyJob, "left ring")
lazy val lootRightRing: Piece = Ring(isTome = true, Job.AnyJob, "right ring")
lazy val lootWeapon: Piece = Weapon(pieceType = PieceType.Tome, Job.AnyJob)
lazy val lootBody: Piece = Body(pieceType = PieceType.Savage, Job.AnyJob)
lazy val lootHands: Piece = Hands(pieceType = PieceType.Tome, Job.AnyJob)
lazy val lootWaist: Piece = Waist(pieceType = PieceType.Tome, Job.AnyJob)
lazy val lootLegs: Piece = Legs(pieceType = PieceType.Savage, Job.AnyJob)
lazy val lootEars: Piece = Ears(pieceType = PieceType.Savage, Job.AnyJob)
lazy val lootRing: Piece = Ring(pieceType = PieceType.Tome, Job.AnyJob)
lazy val lootLeftRing: Piece = Ring(pieceType = PieceType.Tome, Job.AnyJob, "left ring")
lazy val lootRightRing: Piece = Ring(pieceType = PieceType.Tome, Job.AnyJob, "right ring")
lazy val lootUpgrade: Piece = BodyUpgrade
lazy val loot: Seq[Piece] = Seq(lootBody, lootHands, lootLegs, lootUpgrade)

View File

@ -6,7 +6,7 @@ import akka.http.scaladsl.server._
import com.typesafe.config.Config
import me.arcanis.ffxivbis.Settings
import me.arcanis.ffxivbis.http.api.v1.json._
import me.arcanis.ffxivbis.models.{Job, Party, Permission, Piece}
import me.arcanis.ffxivbis.models.{Job, Party, Permission, Piece, PieceType}
import org.scalatest.{Matchers, WordSpec}
import scala.language.postfixOps
@ -41,6 +41,13 @@ class TypesEndpointTest extends WordSpec
}
}
"return all available piece types" in {
Get("/types/pieces/types") ~> route ~> check {
status shouldEqual StatusCodes.OK
responseAs[Seq[String]] shouldEqual PieceType.available.map(_.toString)
}
}
"return current priority" in {
Get("/types/priority") ~> route ~> check {
status shouldEqual StatusCodes.OK

View File

@ -7,11 +7,6 @@ class PieceTest extends WordSpecLike with Matchers with BeforeAndAfterAll {
"piece model" must {
"convert `isTome` property to string" in {
Fixtures.lootBody.isTomeToString shouldEqual "no"
Fixtures.lootHands.isTomeToString shouldEqual "yes"
}
"return upgrade" in {
Fixtures.lootWeapon.upgrade shouldEqual Some(WeaponUpgrade)
Fixtures.lootBody.upgrade shouldEqual None
@ -25,7 +20,7 @@ class PieceTest extends WordSpecLike with Matchers with BeforeAndAfterAll {
"build piece from string" in {
Fixtures.bis.pieces.foreach { piece =>
Piece(piece.piece, piece.isTome, piece.job) shouldEqual piece
Piece(piece.piece, piece.pieceType, piece.job) shouldEqual piece
}
}

View File

@ -0,0 +1,20 @@
package me.arcanis.ffxivbis.models
import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpecLike}
class PieceTypeTest extends WordSpecLike with Matchers with BeforeAndAfterAll {
"piece type model" must {
"create piece type from string" in {
PieceType.available.foreach { pieceType =>
PieceType.withName(pieceType.toString) shouldEqual pieceType
}
}
"fail on unknown piece type" in {
an [IllegalArgumentException] should be thrownBy PieceType.withName("random string")
}
}
}

View File

@ -65,7 +65,7 @@ class DatabaseBiSHandlerTest
}
"update piece in bis set" in {
val newPiece = Hands(isTome = false, Job.DNC)
val newPiece = Hands(pieceType = PieceType.Savage, Job.DNC)
database ! impl.DatabaseBiSHandler.AddPieceToBis(Fixtures.playerEmpty.playerId, newPiece)
expectMsg(timeout, 1)

View File

@ -37,11 +37,11 @@ class LootSelectorTest extends TestKit(ActorSystem("lootselector"))
"loot selector" must {
"suggest loot by isRequired" in {
toPlayerId(default.suggestLoot(Head(isTome = false, Job.AnyJob))) shouldEqual Seq(dnc.playerId, drg.playerId)
toPlayerId(default.suggestLoot(Head(pieceType = PieceType.Savage, 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 piece = Body(pieceType = PieceType.Savage, Job.AnyJob)
val party = default.withPlayer(dnc.withLoot(piece))
toPlayerId(party.suggestLoot(piece)) shouldEqual Seq(drg.playerId, dnc.playerId)
@ -50,7 +50,7 @@ class LootSelectorTest extends TestKit(ActorSystem("lootselector"))
"suggest upgrade" in {
val party = default.withPlayer(
dnc.withBiS(
Some(dnc.bis.copy(weapon = Some(Weapon(isTome = true, Job.DNC))))
Some(dnc.bis.copy(weapon = Some(Weapon(pieceType = PieceType.Tome, Job.DNC))))
)
)
@ -60,24 +60,24 @@ class LootSelectorTest extends TestKit(ActorSystem("lootselector"))
"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)
toPlayerId(party.suggestLoot(Body(pieceType = PieceType.Savage, 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)))
val party = default.withPlayer(dnc.withLoot(Head(pieceType = PieceType.Savage, Job.AnyJob)))
toPlayerId(party.suggestLoot(Body(isTome = false, Job.AnyJob))) shouldEqual Seq(drg.playerId, dnc.playerId)
toPlayerId(party.suggestLoot(Body(pieceType = PieceType.Savage, Job.AnyJob))) shouldEqual Seq(drg.playerId, dnc.playerId)
}
"suggest loot by this piece got" in {
val piece = Body(isTome = true, Job.AnyJob)
val piece = Body(pieceType = PieceType.Tome, 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 piece = Body(pieceType = PieceType.Tome, Job.AnyJob)
val party = default
.withPlayer(dnc.withLoot(Seq(piece, piece).map(pieceToLoot)))
.withPlayer(drg.withLoot(piece))