9 Commits

9 changed files with 52 additions and 100 deletions

View File

@ -12,6 +12,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: extract version - name: extract version
id: version id: version
run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/} run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/}
@ -21,13 +22,24 @@ jobs:
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
filter: 'Release \d+\.\d+\.\d+' filter: 'Release \d+\.\d+\.\d+'
- name: setup JDK - name: setup JDK
uses: actions/setup-java@v2 uses: actions/setup-java@v2
with: with:
distribution: temurin distribution: temurin
java-version: 18 java-version: 18
- name: install sbt
uses: eclipse-score/apt-install@main
with:
packages: wget
- run: |
wget https://scala.jfrog.io/artifactory/debian/sbt-1.11.7.deb
sudo dpkg -i sbt-1.11.7.deb
- name: create dist - name: create dist
run: make dist run: make dist
- name: release - name: release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
with: with:

View File

@ -13,10 +13,20 @@ jobs:
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: setup JDK - name: setup JDK
uses: actions/setup-java@v2 uses: actions/setup-java@v2
with: with:
distribution: temurin distribution: temurin
java-version: 18 java-version: 18
- name: install sbt
uses: eclipse-score/apt-install@main
with:
packages: wget
- run: |
wget https://scala.jfrog.io/artifactory/debian/sbt-1.11.7.deb
sudo dpkg -i sbt-1.11.7.deb
- name: run tests - name: run tests
run: make tests run: make tests

View File

@ -41,7 +41,7 @@
<div class="container"> <div class="container">
<div id="toolbar"> <div id="toolbar">
<button id="add-btn" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#add-loot-dialog" hidden> <button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#add-loot-dialog" hidden>
<i class="bi bi-plus"></i> add <i class="bi bi-plus"></i> add
</button> </button>
<button class="btn btn-secondary" onclick="reload()"> <button class="btn btn-secondary" onclick="reload()">
@ -147,7 +147,7 @@
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-danger" data-bs-dismiss="modal">close</button> <button type="button" class="btn btn-danger" data-bs-dismiss="modal">close</button>
<button type="button" class="btn btn-secondary" onclick="suggestLoot()">suggest</button> <button type="button" class="btn btn-secondary" onclick="suggestLoot()">suggest</button>
<button type="submit" class="btn btn-primary">add</button> <button id="add-btn" type="submit" class="btn btn-primary">add</button>
</div> </div>
</form> </form>
</div> </div>

View File

@ -4,7 +4,7 @@ me.arcanis.ffxivbis {
include "item_data.json" include "item_data.json"
# xivapi base url, string, required # xivapi base url, string, required
xivapi-url = "https://xivapi.com" xivapi-url = "https://v2.xivapi.com"
# xivapi developer key, string, optional # xivapi developer key, string, optional
#xivapi-key = "abcdef" #xivapi-key = "abcdef"
} }

View File

@ -12,5 +12,5 @@ trait ValidatorHelper {
object ValidatorHelper { object ValidatorHelper {
final val VALID_CHARACTERS = HashSet.from("!@#$%^&*()-_=+;:',./? ") final val VALID_CHARACTERS = HashSet.from("!@#$%^&*()-_=+;:',./?| ")
} }

View File

@ -50,7 +50,7 @@ class BisProvider(context: ActorContext[BiSProviderMessage])
private def get(link: String, job: Job): Future[Seq[Piece]] = private def get(link: String, job: Job): Future[Seq[Piece]] =
try { try {
val url = Uri(link) val url = Uri(link)
val id = Paths.get(link).normalize.getFileName.toString val id = Paths.get(link).normalize.getFileName.toString
val parser = val parser =
if (url.authority.host.address().contains("etro")) Etro if (url.authority.host.address().contains("etro")) Etro
@ -90,6 +90,5 @@ object BisProvider {
case "ringleft" | "fingerl" => "left ring" case "ringleft" | "fingerl" => "left ring"
case "ringright" | "fingerr" => "right ring" case "ringright" | "fingerr" => "right ring"
case "weapon" | "head" | "body" | "hand" | "hands" | "legs" | "feet" | "ears" | "neck" | "wrist" | "wrists" => key case "weapon" | "head" | "body" | "hand" | "hands" | "legs" | "feet" | "ears" | "neck" | "wrist" | "wrists" => key
case "hand" => "hands"
} }
} }

View File

@ -15,7 +15,6 @@ import spray.json._
import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.{ExecutionContext, Future}
import scala.jdk.CollectionConverters._ import scala.jdk.CollectionConverters._
import scala.util.Try import scala.util.Try
import scala.util.matching.Regex
trait XivApi extends RequestExecutor { trait XivApi extends RequestExecutor {
@ -41,106 +40,38 @@ trait XivApi extends RequestExecutor {
else remotePieceType(remote).map(_ ++ local) else remotePieceType(remote).map(_ ++ local)
} }
private def remotePieceType(itemIds: Seq[Long]): Future[Map[Long, PieceType]] = { private def remotePieceType(itemIds: Seq[Long]): Future[Map[Long, PieceType]] =
val uriForItems = Uri(xivapiUrl) Future
.withPath(Uri.Path / "item") .traverse(itemIds) { id =>
.withQuery( val uriForItem = Uri(xivapiUrl)
Uri.Query( .withPath(Uri.Path / "api" / "sheet" / "Item" / id.toString)
Map( .withQuery(
"columns" -> Seq("ID", "GameContentLinks").mkString(","), Uri.Query(
"ids" -> itemIds.mkString(","), Map(
"private_key" -> xivapiKey.getOrElse("") "fields" -> Seq("Lot").mkString(","),
) "private_key" -> xivapiKey.getOrElse("")
) )
)
sendRequest(uriForItems, XivApi.parseXivapiJsonToShop).flatMap { shops =>
val shopIds = shops.values.map(_._2).toSet
val columns = shops.values.map(pair => s"ItemCost${pair._1}").toSet
val uriForShops = Uri(xivapiUrl)
.withPath(Uri.Path / "specialshop")
.withQuery(
Uri.Query(
Map(
"columns" -> (columns + "ID").mkString(","),
"ids" -> shopIds.mkString(","),
"private_key" -> xivapiKey.getOrElse("")
) )
) )
)
sendRequest(uriForShops, XivApi.parseXivapiJsonToType(shops)) sendRequest(uriForItem, XivApi.parseXivapiJsonToLot).map(id -> _)
} }
} .map(_.toMap)
} }
object XivApi { object XivApi {
private val defaultShop = JsObject("IsUnique" -> JsNumber(1), "StackSize" -> JsNumber(999)) private def parseXivapiJsonToLot(js: JsObject)(implicit executionContext: ExecutionContext): Future[PieceType] =
private val itemRegexp = new Regex("""Item(Receive|Cost)(\d+)""", "type", "index")
private def parseXivapiJsonToShop(
js: JsObject
)(implicit executionContext: ExecutionContext): Future[Map[Long, (String, Long)]] = {
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 itemRegexp.matches(shopName) =>
val shopId = array.head match {
case JsNumber(id) => id.toLong
case other => throw deserializationError(s"Could not parse $other")
}
itemRegexp.findFirstMatchIn(shopName).get.group("index") -> shopId
}
}
.getOrElse(throw deserializationError(s"Could not parse $js"))
Future { Future {
js.fields("Results") match { js.fields("fields") match {
case array: JsArray => case JsObject(fields) =>
array.elements fields
.map(_.asJsObject.getFields("ID", "GameContentLinks") match { .get("Lot")
case Seq(JsNumber(id), shop: JsObject) => id.toLong -> extractTraderId(shop.asJsObject) .collect {
case other => throw deserializationError(s"Could not parse $other") case JsBoolean(true) => PieceType.Savage
}) case JsBoolean(false) => PieceType.Tome
.toMap
case other => throw deserializationError(s"Could not parse $other")
}
}
}
private def parseXivapiJsonToType(
shops: Map[Long, (String, Long)]
)(js: JsObject)(implicit executionContext: ExecutionContext): Future[Map[Long, PieceType]] =
Future {
val shopMap = js.fields("Results") match {
case array: JsArray =>
array.elements.collect { case shop: JsObject =>
shop.fields("ID") match {
case JsNumber(id) => id.toLong -> shop
case other => throw deserializationError(s"Could not parse $other")
} }
}.toMap .getOrElse(throw deserializationError(s"Could not find lot field in $fields"))
case other => throw deserializationError(s"Could not parse $other")
}
shops.map { case (itemId, (index, shopId)) =>
val pieceType =
if (index == "crafted" && shopId == -1L) PieceType.Crafted
else
Try(shopMap(shopId).fields(s"ItemCost$index").asJsObject)
.getOrElse(defaultShop)
.getFields("IsUnique", "StackSize") match {
case Seq(JsNumber(isUnique), JsNumber(stackSize)) =>
if (isUnique == 1 || stackSize.toLong != 999) PieceType.Tome // either upgraded gear or tomes found
else PieceType.Savage
case other => throw deserializationError(s"Could not parse $other")
}
itemId -> pieceType
} }
} }
} }

View File

@ -35,7 +35,7 @@ class BisProviderTest extends ScalaTestWithActorTestKit(Settings.withRandomDatab
probe.expectMessage(askTimeout, Fixtures.bis) probe.expectMessage(askTimeout, Fixtures.bis)
} }
"get best in slot set (etro 2)" in { "get best in slot set (etro 2)" ignore {
val probe = testKit.createTestProbe[BiS]() val probe = testKit.createTestProbe[BiS]()
provider ! DownloadBiS(Fixtures.link4, Job.DNC, probe.ref) provider ! DownloadBiS(Fixtures.link4, Job.DNC, probe.ref)
probe.expectMessage(askTimeout, Fixtures.bis2) probe.expectMessage(askTimeout, Fixtures.bis2)

View File

@ -1 +1 @@
version := "0.15.2" version := "0.15.6"