mirror of
https://github.com/arcan1s/ffxivbis.git
synced 2026-01-11 20:13:40 +00:00
feat: multi loot selector preview
This commit is contained in:
@ -15,6 +15,7 @@ import akka.util.Timeout
|
|||||||
import ch.megard.akka.http.cors.scaladsl.CorsDirectives.cors
|
import ch.megard.akka.http.cors.scaladsl.CorsDirectives.cors
|
||||||
import com.typesafe.scalalogging.StrictLogging
|
import com.typesafe.scalalogging.StrictLogging
|
||||||
import me.arcanis.ffxivbis.http.api.v1.RootApiV1Endpoint
|
import me.arcanis.ffxivbis.http.api.v1.RootApiV1Endpoint
|
||||||
|
import me.arcanis.ffxivbis.http.api.v2.RootApiV2Endpoint
|
||||||
import me.arcanis.ffxivbis.http.view.RootView
|
import me.arcanis.ffxivbis.http.view.RootView
|
||||||
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message}
|
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message}
|
||||||
|
|
||||||
@ -31,6 +32,7 @@ class RootEndpoint(system: ActorSystem[Nothing], storage: ActorRef[Message], pro
|
|||||||
private val auth = AuthorizationProvider(config, storage)
|
private val auth = AuthorizationProvider(config, storage)
|
||||||
|
|
||||||
private val rootApiV1Endpoint = new RootApiV1Endpoint(storage, auth, provider, config)
|
private val rootApiV1Endpoint = new RootApiV1Endpoint(storage, auth, provider, config)
|
||||||
|
private val rootApiV2Endpoint = new RootApiV2Endpoint(storage, auth)
|
||||||
private val rootView = new RootView(auth)
|
private val rootView = new RootView(auth)
|
||||||
private val swagger = new Swagger(config)
|
private val swagger = new Swagger(config)
|
||||||
|
|
||||||
@ -47,6 +49,7 @@ class RootEndpoint(system: ActorSystem[Nothing], storage: ActorRef[Message], pro
|
|||||||
pathPrefix("api") {
|
pathPrefix("api") {
|
||||||
pathPrefix(Segment) {
|
pathPrefix(Segment) {
|
||||||
case "v1" => rootApiV1Endpoint.routes
|
case "v1" => rootApiV1Endpoint.routes
|
||||||
|
case "v2" => rootApiV2Endpoint.routes
|
||||||
case _ => reject
|
case _ => reject
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,14 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||||
|
*
|
||||||
|
* This file is part of ffxivbis
|
||||||
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
*
|
||||||
|
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
*/
|
||||||
|
package me.arcanis.ffxivbis.http.api.v2
|
||||||
|
|
||||||
|
import me.arcanis.ffxivbis.http.api.v1.{HttpHandler => HttpHandlerV1}
|
||||||
|
import me.arcanis.ffxivbis.http.api.v2.json.JsonSupport
|
||||||
|
|
||||||
|
trait HttpHandler extends HttpHandlerV1 { this: JsonSupport => }
|
||||||
@ -0,0 +1,107 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||||
|
*
|
||||||
|
* This file is part of ffxivbis
|
||||||
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
*
|
||||||
|
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
*/
|
||||||
|
package me.arcanis.ffxivbis.http.api.v2
|
||||||
|
|
||||||
|
import akka.actor.typed.{ActorRef, Scheduler}
|
||||||
|
import akka.http.scaladsl.server.Directives._
|
||||||
|
import akka.http.scaladsl.server._
|
||||||
|
import akka.util.Timeout
|
||||||
|
import io.swagger.v3.oas.annotations.enums.ParameterIn
|
||||||
|
import io.swagger.v3.oas.annotations.media.{ArraySchema, Content, Schema}
|
||||||
|
import io.swagger.v3.oas.annotations.parameters.RequestBody
|
||||||
|
import io.swagger.v3.oas.annotations.responses.ApiResponse
|
||||||
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement
|
||||||
|
import io.swagger.v3.oas.annotations.{Operation, Parameter}
|
||||||
|
import jakarta.ws.rs._
|
||||||
|
import me.arcanis.ffxivbis.http.api.v1.json.{ErrorModel, PieceModel, PlayerIdWithCountersModel}
|
||||||
|
import me.arcanis.ffxivbis.http.api.v2.json._
|
||||||
|
import me.arcanis.ffxivbis.http.helpers.LootHelper
|
||||||
|
import me.arcanis.ffxivbis.http.{Authorization, AuthorizationProvider}
|
||||||
|
import me.arcanis.ffxivbis.messages.Message
|
||||||
|
|
||||||
|
@Path("/api/v2")
|
||||||
|
class LootEndpoint(override val storage: ActorRef[Message], override val auth: AuthorizationProvider)(implicit
|
||||||
|
timeout: Timeout,
|
||||||
|
scheduler: Scheduler
|
||||||
|
) extends LootHelper
|
||||||
|
with Authorization
|
||||||
|
with JsonSupport
|
||||||
|
with HttpHandler {
|
||||||
|
|
||||||
|
def routes: Route = suggestLoot
|
||||||
|
|
||||||
|
@PUT
|
||||||
|
@Path("party/{partyId}/loot")
|
||||||
|
@Consumes(value = Array("application/json"))
|
||||||
|
@Produces(value = Array("application/json"))
|
||||||
|
@Operation(
|
||||||
|
summary = "suggest loot",
|
||||||
|
description = "Suggest loot pieces to party",
|
||||||
|
parameters = Array(
|
||||||
|
new Parameter(
|
||||||
|
name = "partyId",
|
||||||
|
in = ParameterIn.PATH,
|
||||||
|
description = "unique party ID",
|
||||||
|
example = "o3KicHQPW5b0JcOm5yI3"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
requestBody = new RequestBody(
|
||||||
|
description = "piece description",
|
||||||
|
required = true,
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[PieceModel])))
|
||||||
|
),
|
||||||
|
responses = Array(
|
||||||
|
new ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "Players with counters ordered by priority to get this item",
|
||||||
|
content = Array(
|
||||||
|
new Content(
|
||||||
|
array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerIdWithCountersModel])),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new ApiResponse(
|
||||||
|
responseCode = "400",
|
||||||
|
description = "Invalid parameters were supplied",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
|
new ApiResponse(
|
||||||
|
responseCode = "401",
|
||||||
|
description = "Supplied authorization is invalid",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
|
new ApiResponse(
|
||||||
|
responseCode = "403",
|
||||||
|
description = "Access is forbidden",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
|
new ApiResponse(
|
||||||
|
responseCode = "500",
|
||||||
|
description = "Internal server error",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
|
),
|
||||||
|
security = Array(new SecurityRequirement(name = "basic", scopes = Array("get"))),
|
||||||
|
tags = Array("loot"),
|
||||||
|
)
|
||||||
|
def suggestLoot: Route =
|
||||||
|
path("party" / Segment / "loot") { partyId =>
|
||||||
|
extractExecutionContext { implicit executionContext =>
|
||||||
|
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
|
||||||
|
put {
|
||||||
|
entity(as[PiecesModel]) { piece =>
|
||||||
|
onSuccess(suggestPiece(partyId, piece.toPiece)) { response =>
|
||||||
|
complete(response.map(PlayerIdWithCountersModel.fromPlayerId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||||
|
*
|
||||||
|
* This file is part of ffxivbis
|
||||||
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
*
|
||||||
|
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
*/
|
||||||
|
package me.arcanis.ffxivbis.http.api.v2
|
||||||
|
|
||||||
|
import akka.actor.typed.{ActorRef, Scheduler}
|
||||||
|
import akka.http.scaladsl.server.Directives._
|
||||||
|
import akka.http.scaladsl.server.Route
|
||||||
|
import akka.util.Timeout
|
||||||
|
import me.arcanis.ffxivbis.http.AuthorizationProvider
|
||||||
|
import me.arcanis.ffxivbis.http.api.v2.json.JsonSupport
|
||||||
|
import me.arcanis.ffxivbis.messages.Message
|
||||||
|
|
||||||
|
class RootApiV2Endpoint(
|
||||||
|
storage: ActorRef[Message],
|
||||||
|
auth: AuthorizationProvider,
|
||||||
|
)(implicit
|
||||||
|
timeout: Timeout,
|
||||||
|
scheduler: Scheduler
|
||||||
|
) extends JsonSupport
|
||||||
|
with HttpHandler {
|
||||||
|
|
||||||
|
private val lootEndpoint = new LootEndpoint(storage, auth)
|
||||||
|
|
||||||
|
def routes: Route =
|
||||||
|
handleExceptions(exceptionHandler) {
|
||||||
|
handleRejections(rejectionHandler) {
|
||||||
|
lootEndpoint.routes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||||
|
*
|
||||||
|
* This file is part of ffxivbis
|
||||||
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
*
|
||||||
|
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
*/
|
||||||
|
package me.arcanis.ffxivbis.http.api.v2.json
|
||||||
|
|
||||||
|
import me.arcanis.ffxivbis.http.api.v1.json.{JsonSupport => JsonSupportV1}
|
||||||
|
import spray.json._
|
||||||
|
|
||||||
|
trait JsonSupport extends JsonSupportV1 {
|
||||||
|
|
||||||
|
implicit val piecesFormat: RootJsonFormat[PiecesModel] = jsonFormat1(PiecesModel.apply)
|
||||||
|
}
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||||
|
*
|
||||||
|
* This file is part of ffxivbis
|
||||||
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
*
|
||||||
|
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
*/
|
||||||
|
package me.arcanis.ffxivbis.http.api.v2.json
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema
|
||||||
|
import me.arcanis.ffxivbis.http.api.v1.json.PieceModel
|
||||||
|
import me.arcanis.ffxivbis.models.Piece
|
||||||
|
|
||||||
|
case class PiecesModel(
|
||||||
|
@Schema(description = "pieces list", required = true) pieces: Seq[PieceModel],
|
||||||
|
) {
|
||||||
|
|
||||||
|
def toPiece: Seq[Piece] = pieces.map(_.toPiece)
|
||||||
|
}
|
||||||
|
|
||||||
|
object PiecesModel {
|
||||||
|
|
||||||
|
def fromPiece(pieces: Seq[Piece]): PiecesModel = PiecesModel(pieces.map(PieceModel.fromPiece))
|
||||||
|
}
|
||||||
@ -56,4 +56,11 @@ trait LootHelper {
|
|||||||
scheduler: Scheduler
|
scheduler: Scheduler
|
||||||
): Future[Seq[PlayerIdWithCounters]] =
|
): Future[Seq[PlayerIdWithCounters]] =
|
||||||
storage.ask(SuggestLoot(partyId, piece, _)).map(_.result)
|
storage.ask(SuggestLoot(partyId, piece, _)).map(_.result)
|
||||||
|
|
||||||
|
def suggestPiece(partyId: String, pieces: Seq[Piece])(implicit
|
||||||
|
executionContext: ExecutionContext,
|
||||||
|
timeout: Timeout,
|
||||||
|
scheduler: Scheduler
|
||||||
|
): Future[Seq[PlayerIdWithCounters]] =
|
||||||
|
storage.ask(SuggestMultiLoot(partyId, pieces, _)).map(_.result)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -69,6 +69,11 @@ object DatabaseMessage {
|
|||||||
override val isReadOnly: Boolean = true
|
override val isReadOnly: Boolean = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case class SuggestMultiLoot(partyId: String, pieces: Seq[Piece], replyTo: ActorRef[LootSelector.LootSelectorResult])
|
||||||
|
extends LootDatabaseMessage {
|
||||||
|
override val isReadOnly: Boolean = true
|
||||||
|
}
|
||||||
|
|
||||||
// party handler
|
// party handler
|
||||||
trait PartyDatabaseMessage extends DatabaseMessage
|
trait PartyDatabaseMessage extends DatabaseMessage
|
||||||
|
|
||||||
|
|||||||
@ -35,8 +35,8 @@ case class Party(partyDescription: PartyDescription, rules: Seq[String], players
|
|||||||
this
|
this
|
||||||
}
|
}
|
||||||
|
|
||||||
def suggestLoot(piece: Piece): LootSelector.LootSelectorResult =
|
def suggestLoot(pieces: Seq[Piece]): LootSelector.LootSelectorResult =
|
||||||
LootSelector(getPlayers, piece, rules)
|
LootSelector(getPlayers, pieces, rules)
|
||||||
}
|
}
|
||||||
|
|
||||||
object Party {
|
object Party {
|
||||||
|
|||||||
@ -10,9 +10,9 @@ package me.arcanis.ffxivbis.service
|
|||||||
|
|
||||||
import me.arcanis.ffxivbis.models.{Piece, Player, PlayerIdWithCounters}
|
import me.arcanis.ffxivbis.models.{Piece, Player, PlayerIdWithCounters}
|
||||||
|
|
||||||
class LootSelector(players: Seq[Player], piece: Piece, orderBy: Seq[String]) {
|
class LootSelector(players: Seq[Player], pieces: Seq[Piece], orderBy: Seq[String]) {
|
||||||
|
|
||||||
val counters: Seq[PlayerIdWithCounters] = players.map(_.withCounters(Some(piece)))
|
val counters: Seq[PlayerIdWithCounters] = pieces.flatMap(piece => players.map(_.withCounters(Some(piece))))
|
||||||
|
|
||||||
def suggest: LootSelector.LootSelectorResult =
|
def suggest: LootSelector.LootSelectorResult =
|
||||||
LootSelector.LootSelectorResult {
|
LootSelector.LootSelectorResult {
|
||||||
@ -22,8 +22,8 @@ class LootSelector(players: Seq[Player], piece: Piece, orderBy: Seq[String]) {
|
|||||||
|
|
||||||
object LootSelector {
|
object LootSelector {
|
||||||
|
|
||||||
def apply(players: Seq[Player], piece: Piece, orderBy: Seq[String]): LootSelectorResult =
|
def apply(players: Seq[Player], pieces: Seq[Piece], orderBy: Seq[String]): LootSelectorResult =
|
||||||
new LootSelector(players, piece, orderBy).suggest
|
new LootSelector(players, pieces, orderBy).suggest
|
||||||
|
|
||||||
case class LootSelectorResult(result: Seq[PlayerIdWithCounters])
|
case class LootSelectorResult(result: Seq[PlayerIdWithCounters])
|
||||||
}
|
}
|
||||||
|
|||||||
@ -72,6 +72,7 @@ object XivApi {
|
|||||||
case JsBoolean(false) => PieceType.Tome
|
case JsBoolean(false) => PieceType.Tome
|
||||||
}
|
}
|
||||||
.getOrElse(throw deserializationError(s"Could not find lot field in $fields"))
|
.getOrElse(throw deserializationError(s"Could not find lot field in $fields"))
|
||||||
|
case other => throw deserializationError(s"Could not read fields as object from $other")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,7 +40,14 @@ trait DatabaseLootHandler { this: Database =>
|
|||||||
case SuggestLoot(partyId, piece, client) =>
|
case SuggestLoot(partyId, piece, client) =>
|
||||||
run {
|
run {
|
||||||
getParty(partyId, withBiS = true, withLoot = true)
|
getParty(partyId, withBiS = true, withLoot = true)
|
||||||
.map(_.suggestLoot(piece))
|
.map(_.suggestLoot(Seq(piece)))
|
||||||
|
}(client ! _)
|
||||||
|
Behaviors.same
|
||||||
|
|
||||||
|
case SuggestMultiLoot(partyId, pieces, client) =>
|
||||||
|
run {
|
||||||
|
getParty(partyId, withBiS = true, withLoot = true)
|
||||||
|
.map(_.suggestLoot(pieces))
|
||||||
}(client ! _)
|
}(client ! _)
|
||||||
Behaviors.same
|
Behaviors.same
|
||||||
}
|
}
|
||||||
|
|||||||
@ -43,14 +43,14 @@ class LootSelectorTest extends AnyWordSpecLike with Matchers with BeforeAndAfter
|
|||||||
"loot selector" must {
|
"loot selector" must {
|
||||||
|
|
||||||
"suggest loot by isRequired" in {
|
"suggest loot by isRequired" in {
|
||||||
toPlayerId(default.suggestLoot(Piece.Head(pieceType = PieceType.Savage, Job.AnyJob))) shouldEqual Seq(dnc.playerId, drg.playerId)
|
toPlayerId(default.suggestLoot(Seq(Piece.Head(pieceType = PieceType.Savage, Job.AnyJob)))) shouldEqual Seq(dnc.playerId, drg.playerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
"suggest loot if a player already have it" in {
|
"suggest loot if a player already have it" in {
|
||||||
val piece = Piece.Body(pieceType = PieceType.Savage, Job.AnyJob)
|
val piece = Piece.Body(pieceType = PieceType.Savage, Job.AnyJob)
|
||||||
val party = default.withPlayer(dnc.withLoot(piece))
|
val party = default.withPlayer(dnc.withLoot(piece))
|
||||||
|
|
||||||
toPlayerId(party.suggestLoot(piece)) shouldEqual Seq(drg.playerId, dnc.playerId)
|
toPlayerId(party.suggestLoot(Seq(piece))) shouldEqual Seq(drg.playerId, dnc.playerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
"suggest upgrade" in {
|
"suggest upgrade" in {
|
||||||
@ -60,26 +60,26 @@ class LootSelectorTest extends AnyWordSpecLike with Matchers with BeforeAndAfter
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
toPlayerId(party.suggestLoot(Piece.WeaponUpgrade)) shouldEqual Seq(dnc.playerId, drg.playerId)
|
toPlayerId(party.suggestLoot(Seq(Piece.WeaponUpgrade))) shouldEqual Seq(dnc.playerId, drg.playerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
"suggest loot by priority" in {
|
"suggest loot by priority" in {
|
||||||
val party = default.withPlayer(dnc.copy(priority = 2))
|
val party = default.withPlayer(dnc.copy(priority = 2))
|
||||||
|
|
||||||
toPlayerId(party.suggestLoot(Piece.Body(pieceType = PieceType.Savage, Job.AnyJob))) shouldEqual Seq(drg.playerId, dnc.playerId)
|
toPlayerId(party.suggestLoot(Seq(Piece.Body(pieceType = PieceType.Savage, Job.AnyJob)))) shouldEqual Seq(drg.playerId, dnc.playerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
"suggest loot by bis pieces got" in {
|
"suggest loot by bis pieces got" in {
|
||||||
val party = default.withPlayer(dnc.withLoot(Piece.Head(pieceType = PieceType.Savage, Job.AnyJob)))
|
val party = default.withPlayer(dnc.withLoot(Piece.Head(pieceType = PieceType.Savage, Job.AnyJob)))
|
||||||
|
|
||||||
toPlayerId(party.suggestLoot(Piece.Body(pieceType = PieceType.Savage, Job.AnyJob))) shouldEqual Seq(drg.playerId, dnc.playerId)
|
toPlayerId(party.suggestLoot(Seq(Piece.Body(pieceType = PieceType.Savage, Job.AnyJob)))) shouldEqual Seq(drg.playerId, dnc.playerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
"suggest loot by this piece got" in {
|
"suggest loot by this piece got" in {
|
||||||
val piece = Piece.Body(pieceType = PieceType.Tome, Job.AnyJob)
|
val piece = Piece.Body(pieceType = PieceType.Tome, Job.AnyJob)
|
||||||
val party = default.withPlayer(dnc.withLoot(piece))
|
val party = default.withPlayer(dnc.withLoot(piece))
|
||||||
|
|
||||||
toPlayerId(party.suggestLoot(piece)) shouldEqual Seq(drg.playerId, dnc.playerId)
|
toPlayerId(party.suggestLoot(Seq(piece))) shouldEqual Seq(drg.playerId, dnc.playerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
"suggest loot by total piece got" in {
|
"suggest loot by total piece got" in {
|
||||||
@ -88,7 +88,7 @@ class LootSelectorTest extends AnyWordSpecLike with Matchers with BeforeAndAfter
|
|||||||
.withPlayer(dnc.withLoot(Seq(piece, piece).map(pieceToLoot)))
|
.withPlayer(dnc.withLoot(Seq(piece, piece).map(pieceToLoot)))
|
||||||
.withPlayer(drg.withLoot(piece))
|
.withPlayer(drg.withLoot(piece))
|
||||||
|
|
||||||
toPlayerId(party.suggestLoot(piece)) shouldEqual Seq(drg.playerId, dnc.playerId)
|
toPlayerId(party.suggestLoot(Seq(piece))) shouldEqual Seq(drg.playerId, dnc.playerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user