941 lines
31 KiB
Go
941 lines
31 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"log"
|
|
"math"
|
|
"net/http"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
inventorytools "code.mashffxiv.deadbeef.codes/MashPotato/gilgetter/inventorytools"
|
|
)
|
|
|
|
// http - GET /
|
|
// Serves the home page
|
|
func homePageHandler(w http.ResponseWriter, _ *http.Request) {
|
|
type PageData struct {
|
|
Title string
|
|
MarketItems []MarketItem
|
|
LastPriceUpdate int
|
|
PriceUpdateProgress int
|
|
}
|
|
|
|
pageData := PageData{
|
|
Title: "Home",
|
|
LastPriceUpdate: int(time.Since(lastFullPriceUpdateTime).Minutes()),
|
|
PriceUpdateProgress: priceUpdateProgress,
|
|
}
|
|
|
|
if len(itemSlice) > 200 {
|
|
pageData.MarketItems = itemSlice[:200] // truncate to top 200
|
|
} else {
|
|
pageData.MarketItems = itemSlice
|
|
}
|
|
|
|
err := t.ExecuteTemplate(w, "home.html", pageData)
|
|
if err != nil {
|
|
log.Fatalf("failed to execute template: %v", err)
|
|
}
|
|
}
|
|
|
|
// http - GET /stale-items
|
|
// Displays a list of items on Universalis that have the most out of date information
|
|
// Page is is used to ask users to help contribute data for the items that need it most
|
|
func staleItemsPageHandler(w http.ResponseWriter, _ *http.Request) {
|
|
type PageData struct {
|
|
Title string
|
|
MarketItems []MarketItem
|
|
}
|
|
|
|
pageData := PageData{Title: "Stale Items", MarketItems: make([]MarketItem, 0)}
|
|
|
|
for _, item := range items {
|
|
if item.MarketBoardListing == nil { // If there are no market board listings, we don't care about it
|
|
continue
|
|
}
|
|
pageData.MarketItems = append(pageData.MarketItems, *(items[item.ID]))
|
|
}
|
|
|
|
// Sort the item slice by time last updated
|
|
sort.Slice(pageData.MarketItems, func(i, j int) bool {
|
|
return pageData.MarketItems[i].MarketBoardListing.LastUploadTime < pageData.MarketItems[j].MarketBoardListing.LastUploadTime
|
|
})
|
|
|
|
// Truncate the item slice to 200 items
|
|
if len(pageData.MarketItems) > 200 {
|
|
pageData.MarketItems = pageData.MarketItems[:200]
|
|
}
|
|
|
|
// TBD: could potentially sort by last updated here, however the current data type is a string and that's bad to sort with. Will see if it's needed in practice
|
|
err := t.ExecuteTemplate(w, "stale-items.html", pageData)
|
|
if err != nil {
|
|
log.Printf("failed to execute template: %v", err)
|
|
}
|
|
|
|
}
|
|
|
|
// http - GET /crafting-checkout
|
|
// http - POST /crafting-checkout
|
|
// When POST, accepts a JSON object detailing what item IDs are selected for crafting, saves to global variable
|
|
// When GET, produces a list of the items to crafting, suggested quantities to craft, and materials required
|
|
func craftingCheckoutPageHandler(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method == "GET" {
|
|
|
|
type AutoXivItem struct {
|
|
Row int
|
|
Col int
|
|
}
|
|
|
|
type Retainer struct {
|
|
RetainerNumber int
|
|
Name string
|
|
ItemsToGet []inventorytools.Item
|
|
ContainerOne []AutoXivItem
|
|
ContainerTwo []AutoXivItem
|
|
ContainerThree []AutoXivItem
|
|
ContainerFour []AutoXivItem
|
|
ContainerFive []AutoXivItem
|
|
}
|
|
|
|
type CraftingItem struct {
|
|
ID int
|
|
Name string
|
|
Job string
|
|
Quantity int
|
|
}
|
|
|
|
type PageData struct {
|
|
Title string
|
|
CraftingItems []CraftingItem // The items that will be crafted
|
|
Retainers []Retainer // items to get from retainers
|
|
MarketItems []inventorytools.Item // items that need to be bought from market
|
|
}
|
|
|
|
pageData := PageData{Title: "Crafting Checkout", CraftingItems: make([]CraftingItem, 0), Retainers: make([]Retainer, 0), MarketItems: make([]inventorytools.Item, 0)}
|
|
|
|
// Build list of crafting items, and how much materials are required
|
|
craftingMaterialMap := make(map[int]int) // key item ID, value quantity required
|
|
for _, craftingItemID := range craftingCheckoutItemIDs {
|
|
craftingItem := CraftingItem{ID: craftingItemID, Name: items[craftingItemID].Name, Job: items[craftingItemID].Job, Quantity: (items[craftingItemID].Velocity / 2) + 1}
|
|
for _, materialItem := range items[craftingItemID].CraftingMaterials {
|
|
if _, ok := craftingMaterialMap[materialItem.ID]; ok {
|
|
// we've seen it before, so we add to it
|
|
craftingMaterialMap[materialItem.ID] = craftingMaterialMap[materialItem.ID] + materialItem.Quantity*craftingItem.Quantity
|
|
} else {
|
|
// first occurrence
|
|
craftingMaterialMap[materialItem.ID] = materialItem.Quantity * craftingItem.Quantity
|
|
}
|
|
}
|
|
pageData.CraftingItems = append(pageData.CraftingItems, craftingItem)
|
|
}
|
|
// Sort by job to group items to craft together by their job
|
|
sort.Slice(pageData.CraftingItems, func(i, j int) bool {
|
|
return pageData.CraftingItems[i].Job < pageData.CraftingItems[j].Job
|
|
})
|
|
|
|
// Subtract materials in character inventory
|
|
for _, item := range inventories.CharacterBags {
|
|
if _, ok := craftingMaterialMap[item.ID]; ok {
|
|
craftingMaterialMap[item.ID] = craftingMaterialMap[item.ID] - item.Quantity
|
|
if craftingMaterialMap[item.ID] <= 0 {
|
|
delete(craftingMaterialMap, item.ID)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Materials in retainer inventory and subtract
|
|
retainerNumber := 0
|
|
for _, retainer := range inventories.Retainers {
|
|
retainerNumber++
|
|
itemsToGet := make([]inventorytools.Item, 0)
|
|
for _, item := range retainer.RetainerBags {
|
|
if _, ok := craftingMaterialMap[item.ID]; ok {
|
|
// This is an item we need that exists on the retainer
|
|
if item.Quantity > craftingMaterialMap[item.ID] { // the retainer has more than we need, so adjust quantity down to what we need
|
|
item.Quantity = craftingMaterialMap[item.ID]
|
|
}
|
|
|
|
// offset quantity and map name from ID
|
|
itemToGet := item
|
|
itemToGet.Name = items[itemToGet.ID].Name
|
|
itemToGet.SortedContainer -= 9999
|
|
itemToGet.SortedSlotIndex += 1
|
|
|
|
itemsToGet = append(itemsToGet, itemToGet)
|
|
craftingMaterialMap[item.ID] = craftingMaterialMap[item.ID] - item.Quantity
|
|
if craftingMaterialMap[item.ID] <= 0 {
|
|
delete(craftingMaterialMap, item.ID)
|
|
}
|
|
}
|
|
}
|
|
if len(itemsToGet) > 0 {
|
|
pageData.Retainers = append(pageData.Retainers, Retainer{Name: retainer.Name, RetainerNumber: retainerNumber, ItemsToGet: itemsToGet})
|
|
}
|
|
}
|
|
|
|
// Subtract crystal bag inventory
|
|
for _, item := range inventories.Crystals {
|
|
if _, ok := craftingMaterialMap[item.ID]; ok {
|
|
craftingMaterialMap[item.ID] = craftingMaterialMap[item.ID] - item.Quantity
|
|
if craftingMaterialMap[item.ID] <= 0 {
|
|
delete(craftingMaterialMap, item.ID)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Materials to be bought from market is what's left over
|
|
for itemID, itemQuantity := range craftingMaterialMap {
|
|
itemToGet := inventorytools.Item{ID: itemID, Name: items[itemID].Name, Quantity: itemQuantity}
|
|
pageData.MarketItems = append(pageData.MarketItems, itemToGet)
|
|
}
|
|
|
|
/*
|
|
for id, quantity := range craftingMaterialMap {
|
|
materialItem := TableItem{ID: id, Name: items[id].Name, Quantity: quantity}
|
|
pageData.CraftingMaterials = append(pageData.CraftingMaterials, materialItem)
|
|
}
|
|
*/
|
|
|
|
err := t.ExecuteTemplate(w, "crafting-checkout.html", pageData)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
log.Printf("failed to execute crafting-checkout.html template: %v", err)
|
|
return
|
|
}
|
|
|
|
// solve auto-xiv data for template
|
|
|
|
for i, retainer := range pageData.Retainers {
|
|
for _, item := range retainer.ItemsToGet {
|
|
|
|
autoXivItem := AutoXivItem{}
|
|
|
|
autoXivItem.Row = int(math.Ceil(float64(item.SortedSlotIndex) / 5))
|
|
autoXivItem.Col = item.SortedSlotIndex % 5
|
|
if autoXivItem.Col == 0 {
|
|
autoXivItem.Col = 5
|
|
}
|
|
|
|
if item.SortedContainer == 1 {
|
|
pageData.Retainers[i].ContainerOne = append(pageData.Retainers[i].ContainerOne, autoXivItem)
|
|
} else if item.SortedContainer == 2 {
|
|
pageData.Retainers[i].ContainerTwo = append(pageData.Retainers[i].ContainerTwo, autoXivItem)
|
|
} else if item.SortedContainer == 3 {
|
|
pageData.Retainers[i].ContainerThree = append(pageData.Retainers[i].ContainerThree, autoXivItem)
|
|
} else if item.SortedContainer == 4 {
|
|
pageData.Retainers[i].ContainerFour = append(pageData.Retainers[i].ContainerFour, autoXivItem)
|
|
} else if item.SortedContainer == 5 {
|
|
pageData.Retainers[i].ContainerFive = append(pageData.Retainers[i].ContainerFive, autoXivItem)
|
|
}
|
|
}
|
|
}
|
|
|
|
buf := bytes.NewBuffer(nil)
|
|
err = t.ExecuteTemplate(buf, "withdrawitems.ahk", pageData)
|
|
if err != nil {
|
|
log.Printf("failed to execute withdrawitems.ahk template: %v", err)
|
|
return
|
|
}
|
|
|
|
withdrawItemsAhk, err = ioutil.ReadAll(buf)
|
|
if err != nil {
|
|
log.Printf("failed to read withdrawitems.ahk processed template buffer to byte slice: %v", err)
|
|
}
|
|
|
|
} else if r.Method == "POST" {
|
|
body, err := ioutil.ReadAll(r.Body)
|
|
if err != nil {
|
|
log.Printf("failed to read request body on craftingCheckoutPageHandler: %v", err)
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
type CraftingCheckoutPageData struct {
|
|
ItemIDs []int `json:"itemids"`
|
|
}
|
|
|
|
craftingCheckoutPageData := CraftingCheckoutPageData{}
|
|
|
|
err = json.Unmarshal(body, &craftingCheckoutPageData)
|
|
if err != nil {
|
|
log.Printf("failed to unmarshal craftingCheckoutPageData into struct: %v", err)
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
craftingCheckoutItemIDs = make([]int, 0)
|
|
craftingCheckoutItemIDs = craftingCheckoutPageData.ItemIDs
|
|
|
|
/* // This causes velocity to be scaled up and messes up the crafting material quantity calculation
|
|
go func() { // queue up datacenter price updates
|
|
universalisPriceEndpoint = datacenter
|
|
defer func() { universalisPriceEndpoint = world }() // set back to world when done
|
|
|
|
// build list of crafting supply item IDs to be refreshed for datacenter pricing
|
|
itemsToRefresh := make(map[int]bool)
|
|
for _, craftingItemID := range craftingCheckoutItemIDs {
|
|
for _, craftingMaterial := range items[craftingItemID].CraftingMaterials {
|
|
itemsToRefresh[craftingMaterial.ID] = true
|
|
}
|
|
}
|
|
|
|
// queue up datacenter price updates
|
|
for itemID := range itemsToRefresh {
|
|
priceUpdateQueue <- itemID
|
|
updateRequestWg.Add(1)
|
|
}
|
|
|
|
// wait for channel to empty
|
|
updateRequestWg.Wait()
|
|
}()
|
|
*/
|
|
}
|
|
}
|
|
|
|
// http - GET /auto-xiv
|
|
//
|
|
// Downloads the .ahk file for withdrawing items
|
|
func autoXivHandler(w http.ResponseWriter, _ *http.Request) {
|
|
|
|
w.Header().Set("Content-Disposition", "attachment; filename=withdraw-items.ahk")
|
|
|
|
w.Write(withdrawItemsAhk)
|
|
}
|
|
|
|
// http - GET /full-price-update
|
|
// Requests a full price update if criteria is met
|
|
func fullPriceUpdateHandler(w http.ResponseWriter, _ *http.Request) {
|
|
err := fullPriceRefresh()
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusTooEarly)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
|
|
// http - GET /price-update
|
|
// Queues a price update for an item after 30 seconds
|
|
func singlePriceUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
|
err := r.ParseForm()
|
|
if err != nil {
|
|
log.Printf("failed to parse form in singlePriceUpdateHandler: %v", err)
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
itemID, err := strconv.Atoi(r.FormValue("itemid"))
|
|
if err != nil {
|
|
log.Printf("failed to convert itemid variable to integer in singlePriceUpdateHandler: %v", err)
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusAccepted)
|
|
|
|
// Don't keep the client waiting
|
|
go func() {
|
|
time.Sleep(time.Second * 30)
|
|
log.Printf("dynamic price update for %d triggered", itemID)
|
|
priceUpdateQueue <- itemID
|
|
updateRequestWg.Add(1)
|
|
}()
|
|
|
|
}
|
|
|
|
// http - GET /status
|
|
// 5 second polling each client sends to server for updating front end data
|
|
func statusHandler(w http.ResponseWriter, _ *http.Request) {
|
|
type PageData struct {
|
|
SecondsRemaining int `json:"secondsremaining"` // ETA for how long an ongoing update will take
|
|
PriceUpdateProgress int `json:"priceupdateprogress"` // if progress is 100 that means there is no price update ongoing
|
|
UpdatedMinutesAgo int `json:"updatedminutesago"` // How many minutes since the last full price refresh
|
|
NewSortAvailable bool `json:"newsortavailable"` // True if an updated metric sort is available
|
|
}
|
|
|
|
timePerRequest := time.Since(startFullPriceUpdateTime).Seconds() / float64(len(items)-len(priceUpdateQueue))
|
|
secondsRemaining := float64(len(priceUpdateQueue)) * timePerRequest
|
|
pageData := PageData{
|
|
SecondsRemaining: int(secondsRemaining),
|
|
PriceUpdateProgress: priceUpdateProgress,
|
|
UpdatedMinutesAgo: int(time.Since(lastFullPriceUpdateTime).Minutes()),
|
|
}
|
|
if pageData.UpdatedMinutesAgo < 1 {
|
|
pageData.NewSortAvailable = false
|
|
} else {
|
|
pageData.NewSortAvailable = time.Now().Before(lastSortTime.Add(time.Second * 6)) // true if last sort was within 6 seconds
|
|
}
|
|
|
|
jsonBytes, err := json.Marshal(pageData)
|
|
if err != nil {
|
|
log.Printf("statusHandler failed to marshal json to bytes: %v", err)
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.Write(jsonBytes)
|
|
}
|
|
|
|
// http - GET /items-list
|
|
// Returns a list of
|
|
func itemsListHandler(w http.ResponseWriter, _ *http.Request) {
|
|
// Remap our data into a smaller object, we don't need full data for the front end
|
|
var responseDataItemSlice []MarketItem
|
|
if len(itemSlice) > 200 {
|
|
responseDataItemSlice = itemSlice[:200] // truncate to top 200
|
|
} else {
|
|
responseDataItemSlice = itemSlice
|
|
}
|
|
|
|
// use inventory tools to update checkmarks - but universalis code still present as fallback
|
|
// expensive?
|
|
for i, itemSliceItem := range responseDataItemSlice {
|
|
|
|
/*
|
|
// Universalis only implementation - requires searching each item on market board to
|
|
// properly refresh universalis with up to date data first from the price uploader
|
|
// Depracated when inventory tools integration was added (below)
|
|
for _, listing := range itemSliceItem.MarketBoardListing.Listings {
|
|
for _, retainer := range retainers {
|
|
if listing.RetainerName == retainer {
|
|
itemSliceItem.InStock = true
|
|
}
|
|
}
|
|
}
|
|
*/
|
|
|
|
itemSliceItem.InStock = false // reset it to initial value of false
|
|
for _, retainer := range inventories.Retainers {
|
|
found := false
|
|
// check market listings
|
|
for _, marketItem := range retainer.RetainerMarket {
|
|
if itemSliceItem.ID == marketItem.ID {
|
|
found = true
|
|
itemSliceItem.InStock = true
|
|
break
|
|
}
|
|
}
|
|
if found {
|
|
break
|
|
}
|
|
|
|
// check retainer bags
|
|
for _, marketItem := range retainer.RetainerBags {
|
|
if itemSliceItem.ID == marketItem.ID {
|
|
found = true
|
|
itemSliceItem.InStock = true
|
|
break
|
|
}
|
|
}
|
|
if found {
|
|
break
|
|
}
|
|
}
|
|
// check player bags
|
|
for _, marketItem := range inventories.CharacterBags {
|
|
if itemSliceItem.ID == marketItem.ID {
|
|
itemSliceItem.InStock = true
|
|
break
|
|
}
|
|
}
|
|
responseDataItemSlice[i] = itemSliceItem
|
|
}
|
|
|
|
jsonBytes, err := json.Marshal(responseDataItemSlice)
|
|
if err != nil {
|
|
log.Printf("itemsListHandler failed to marshal json to bytes: %v", err)
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.Write(jsonBytes)
|
|
}
|
|
|
|
// http - GET /stale-items-list
|
|
// Returns a list of
|
|
func staleItemsListHandler(w http.ResponseWriter, _ *http.Request) {
|
|
// Remap our data into a smaller object, we don't need full data for the front end
|
|
type ResponseDataItem struct {
|
|
ID int `json:"id"`
|
|
Name string `json:"name"`
|
|
Data string `json:"data"`
|
|
}
|
|
var responseDataItemSlice []MarketItem
|
|
for _, item := range items {
|
|
if item.MarketBoardListing == nil { // If there are no market board listings, we don't care about it
|
|
continue
|
|
}
|
|
if item.MarketBoardListing.LastUploadTime == 0 {
|
|
continue
|
|
}
|
|
responseDataItemSlice = append(responseDataItemSlice, *(items[item.ID]))
|
|
}
|
|
|
|
// Sort the item slice by time last updated
|
|
sort.Slice(responseDataItemSlice, func(i, j int) bool {
|
|
return responseDataItemSlice[i].MarketBoardListing.LastUploadTime < responseDataItemSlice[j].MarketBoardListing.LastUploadTime
|
|
})
|
|
|
|
if len(responseDataItemSlice) > 600 {
|
|
responseDataItemSlice = responseDataItemSlice[:600] // truncate to top 600
|
|
}
|
|
|
|
responseData := make([]ResponseDataItem, 0)
|
|
|
|
for _, item := range responseDataItemSlice {
|
|
responseDataItem := ResponseDataItem{
|
|
ID: item.ID,
|
|
Name: item.Name,
|
|
Data: time.Unix(item.MarketBoardListing.LastUploadTime/1000, 0).Format("2006-01-02"),
|
|
}
|
|
responseData = append(responseData, responseDataItem)
|
|
}
|
|
|
|
jsonBytes, err := json.Marshal(responseData)
|
|
if err != nil {
|
|
log.Printf("itemsListHandler failed to marshal json to bytes: %v", err)
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.Write(jsonBytes)
|
|
}
|
|
|
|
// http - GET /bugged-metric-items-list
|
|
// Returns a list of
|
|
func buggedMetricItemsListHandler(w http.ResponseWriter, _ *http.Request) {
|
|
// Remap our data into a smaller object, we don't need full data for the front end
|
|
type ResponseDataItem struct {
|
|
ID int `json:"id"`
|
|
Name string `json:"name"`
|
|
Data string `json:"data"`
|
|
}
|
|
var responseDataItemSlice []MarketItem
|
|
for _, item := range items {
|
|
if item.ListingMetric != 0 {
|
|
continue
|
|
}
|
|
|
|
responseDataItemSlice = append(responseDataItemSlice, *(items[item.ID]))
|
|
}
|
|
|
|
// Sort the item slice by time last updated
|
|
sort.Slice(responseDataItemSlice, func(i, j int) bool {
|
|
return responseDataItemSlice[i].MarketBoardListing.LastUploadTime < responseDataItemSlice[j].MarketBoardListing.LastUploadTime
|
|
})
|
|
|
|
if len(responseDataItemSlice) > 600 {
|
|
responseDataItemSlice = responseDataItemSlice[:600] // truncate to top 600
|
|
}
|
|
|
|
responseData := make([]ResponseDataItem, 0)
|
|
|
|
for _, item := range responseDataItemSlice {
|
|
responseDataItem := ResponseDataItem{
|
|
ID: item.ID,
|
|
Name: item.Name,
|
|
Data: time.Unix(item.MarketBoardListing.LastUploadTime/1000, 0).Format("2006-01-02"),
|
|
}
|
|
responseData = append(responseData, responseDataItem)
|
|
}
|
|
|
|
jsonBytes, err := json.Marshal(responseData)
|
|
if err != nil {
|
|
log.Printf("itemsListHandler failed to marshal json to bytes: %v", err)
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.Write(jsonBytes)
|
|
}
|
|
|
|
// http - GET /search-listing-retainer
|
|
// Returns a list of listings on Universalis posted by a particular retainer name
|
|
func searchListingRetainerHandler(w http.ResponseWriter, r *http.Request) {
|
|
err := r.ParseForm()
|
|
if err != nil {
|
|
log.Printf("failed to parse form in searchListingRetainerHandler: %v", err)
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
retainerName := strings.ToUpper(r.FormValue("name"))
|
|
|
|
for _, item := range items {
|
|
for _, listing := range item.MarketBoardListing.Listings {
|
|
if strings.ToUpper(listing.RetainerName) == retainerName {
|
|
w.Write([]byte(item.Name + "\n"))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// http - GET /search-listing-craftedby
|
|
// Returns a list of listings on Universalis posted by a particular retainer name
|
|
func searchListingCraftedbyHandler(w http.ResponseWriter, r *http.Request) {
|
|
err := r.ParseForm()
|
|
if err != nil {
|
|
log.Printf("failed to parse form in searchListingCraftedbyHandler: %v", err)
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
craftedbyName := strings.ToUpper(r.FormValue("name"))
|
|
|
|
for _, item := range items {
|
|
for _, listing := range item.MarketBoardListing.Listings {
|
|
if strings.ToUpper(listing.CreatorName) == craftedbyName {
|
|
w.Write([]byte(item.Name + "\n"))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// http - GET /search-listing-price
|
|
// Returns a list of listings on Universalis posted by a particular listing price
|
|
func searchListingPriceHandler(w http.ResponseWriter, r *http.Request) {
|
|
err := r.ParseForm()
|
|
if err != nil {
|
|
log.Printf("failed to parse form in searchListingPriceHandler: %v", err)
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
price, err := strconv.Atoi(r.FormValue("price"))
|
|
if err != nil {
|
|
log.Printf("failed to convert '%s' to an integer in searchListingPriceHandler", r.FormValue("price"))
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
for _, item := range items {
|
|
for _, listing := range item.MarketBoardListing.Listings {
|
|
if listing.PricePerUnit == price {
|
|
w.Write([]byte(item.Name + "\n"))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// http - GET /inventory
|
|
// http - POST /inventory
|
|
// When POST, accepts a JSON object containing inventory data from the inventorytools client utility
|
|
// When GET, produces a list of the items in retainer inventory to take for listing on market
|
|
func inventoryPageHandler(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method == "GET" {
|
|
|
|
type ItemToPost struct {
|
|
ListingMetric int
|
|
InventoryData inventorytools.Item
|
|
}
|
|
|
|
type AutoXivItem struct {
|
|
Row int
|
|
Col int
|
|
}
|
|
|
|
type Retainer struct {
|
|
Name string
|
|
RetainerNumber int
|
|
ItemsToPost []ItemToPost
|
|
ContainerOne []AutoXivItem
|
|
ContainerTwo []AutoXivItem
|
|
ContainerThree []AutoXivItem
|
|
ContainerFour []AutoXivItem
|
|
ContainerFive []AutoXivItem
|
|
}
|
|
|
|
type PageData struct {
|
|
Title string
|
|
Retainers []Retainer
|
|
}
|
|
|
|
pageData := PageData{Title: "Inventory"}
|
|
pageData.Retainers = make([]Retainer, 0)
|
|
|
|
// # The plan
|
|
// 1. Create map named filteredIDs
|
|
// 2. Add all itemIDs in character inventory
|
|
// 3. Add all itemIDs in retainerMarket inventories
|
|
// 4. Loop through retainers that have the MarketItemStorage set to true
|
|
// 5. Make new slice per retainer itemsToPost
|
|
// 6. Add everything in retainer bag that doesn't match filteredIDs to the itemsToPost slice
|
|
// 7. Display a segment per retainer containing retainer name, their items to post, sorted container and sorted slot index
|
|
|
|
// Filter can be several modes
|
|
// * filterListed - filters out items which are in the character inventory and that are already listed on market for retainer. Useful for finding crafted items stored on retainers that can be posted.
|
|
// * none - no filter, lists all items stored on retainers
|
|
filter := r.URL.Query().Get("filter")
|
|
if filter == "" { // by default (empty string) it "filterListed" mode.
|
|
filter = "filterListed"
|
|
}
|
|
|
|
action := r.URL.Query().Get("action")
|
|
|
|
// Build a list of IDs that are ineligible to pull from retainer bags for posting
|
|
filteredIDs := make(map[int]bool)
|
|
for _, item := range inventories.CharacterBags { // ignore everything in character inventory
|
|
filteredIDs[item.ID] = true
|
|
}
|
|
|
|
if filter == "filterListed" {
|
|
for _, retainer := range inventories.Retainers { // go through each retainer
|
|
for _, item := range retainer.RetainerMarket { // ignore everything retainer already have posted to market
|
|
filteredIDs[item.ID] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// Materials in retainer inventory and subtract
|
|
retainerNumber := 0
|
|
|
|
// Build list of retainers and their items to post to be displayed on the web page
|
|
for _, retainer := range inventories.Retainers {
|
|
retainerNumber++
|
|
if !retainer.MarketItemStorage { // ignore retainers that aren't configured to be market items storage
|
|
continue
|
|
}
|
|
|
|
tmpRetainer := Retainer{Name: retainer.Name, RetainerNumber: retainerNumber}
|
|
tmpRetainer.ItemsToPost = make([]ItemToPost, 0)
|
|
|
|
for _, item := range retainer.RetainerBags {
|
|
if _, ok := items[item.ID]; !ok {
|
|
log.Printf("skipping item '%d' in retainer inventory - not a craftable item?", item.ID)
|
|
continue
|
|
}
|
|
|
|
if _, ok := filteredIDs[item.ID]; !ok { // if it's not a filtered item then we can display it on the page
|
|
if _, ok := items[item.ID]; ok {
|
|
item.Name = items[item.ID].Name // we have to set the name by cross referencing
|
|
} else {
|
|
item.Name = fmt.Sprintf("Unknown name - ID: %d", item.ID) // this might happen if a market item storage container is holding an item that isn't loaded from the crafting-recipes directory
|
|
}
|
|
item.SortedContainer = item.SortedContainer - 9999 // human friendly offsets
|
|
item.SortedSlotIndex = item.SortedSlotIndex + 1 // human friendly offsets
|
|
|
|
itemToPost := ItemToPost{ListingMetric: items[item.ID].ListingMetric / 1000, InventoryData: item} // divide by 1000 to decrease number of digits for human simplicity
|
|
tmpRetainer.ItemsToPost = append(tmpRetainer.ItemsToPost, itemToPost)
|
|
filteredIDs[item.ID] = true // make it a filtered item to avoid duplicates in future
|
|
|
|
if action == "refresh" { // If it's a refresh action, we queue it for an update
|
|
log.Printf("dynamic price update for %d triggered", item.ID)
|
|
priceUpdateQueue <- item.ID
|
|
updateRequestWg.Add(1)
|
|
}
|
|
}
|
|
}
|
|
|
|
sort.Slice(tmpRetainer.ItemsToPost, func(i, j int) bool {
|
|
return tmpRetainer.ItemsToPost[i].ListingMetric > tmpRetainer.ItemsToPost[j].ListingMetric
|
|
})
|
|
|
|
pageData.Retainers = append(pageData.Retainers, tmpRetainer)
|
|
}
|
|
|
|
if action == "refresh" {
|
|
updateRequestWg.Wait()
|
|
sortItemSlice() // required to update metrics
|
|
}
|
|
|
|
err := t.ExecuteTemplate(w, "inventory.html", pageData)
|
|
if err != nil {
|
|
log.Printf("failed to execute template: %v", err)
|
|
}
|
|
|
|
itemsRequired := 0 // number of items required to withdraw to have exactly 20/20 items posted on each retainer
|
|
|
|
for _, retainer := range inventories.Retainers {
|
|
if retainer.RetainerMarket != nil {
|
|
|
|
itemsRequired += 20 - len(retainer.RetainerMarket)
|
|
// Update since conversion to CSV - no longer do we have nil entries
|
|
/*
|
|
for _, item := range retainer.RetainerMarket {
|
|
fmt.Printf("Item ID: %d\n", item.ID)
|
|
if item.ID == 0 {
|
|
itemsRequired++
|
|
}
|
|
}*/
|
|
|
|
} else {
|
|
log.Printf("retainer '%s' market inventory is nil, assuming no items posted?", retainer.Name)
|
|
itemsRequired += 20
|
|
}
|
|
}
|
|
|
|
log.Printf("itemsRequired: %d", itemsRequired)
|
|
|
|
// second pass
|
|
for i, _ := range pageData.Retainers {
|
|
if itemsRequired <= 0 {
|
|
break
|
|
}
|
|
for _, item := range pageData.Retainers[i].ItemsToPost {
|
|
if itemsRequired <= 0 {
|
|
break
|
|
}
|
|
itemsRequired--
|
|
|
|
autoXivItem := AutoXivItem{}
|
|
|
|
autoXivItem.Row = int(math.Ceil(float64(item.InventoryData.SortedSlotIndex) / 5))
|
|
autoXivItem.Col = item.InventoryData.SortedSlotIndex % 5
|
|
if autoXivItem.Col == 0 {
|
|
autoXivItem.Col = 5
|
|
}
|
|
|
|
if item.InventoryData.SortedContainer == 1 {
|
|
pageData.Retainers[i].ContainerOne = append(pageData.Retainers[i].ContainerOne, autoXivItem)
|
|
} else if item.InventoryData.SortedContainer == 2 {
|
|
pageData.Retainers[i].ContainerTwo = append(pageData.Retainers[i].ContainerTwo, autoXivItem)
|
|
} else if item.InventoryData.SortedContainer == 3 {
|
|
pageData.Retainers[i].ContainerThree = append(pageData.Retainers[i].ContainerThree, autoXivItem)
|
|
} else if item.InventoryData.SortedContainer == 4 {
|
|
pageData.Retainers[i].ContainerFour = append(pageData.Retainers[i].ContainerFour, autoXivItem)
|
|
} else if item.InventoryData.SortedContainer == 5 {
|
|
pageData.Retainers[i].ContainerFive = append(pageData.Retainers[i].ContainerFive, autoXivItem)
|
|
}
|
|
}
|
|
}
|
|
|
|
buf := bytes.NewBuffer(nil)
|
|
err = t.ExecuteTemplate(buf, "withdrawitems.ahk", pageData)
|
|
if err != nil {
|
|
log.Printf("failed to execute withdrawitems.ahk template: %v", err)
|
|
return
|
|
}
|
|
|
|
withdrawItemsAhk, err = ioutil.ReadAll(buf)
|
|
if err != nil {
|
|
log.Printf("failed to read withdrawitems.ahk processed template buffer to byte slice: %v", err)
|
|
}
|
|
|
|
} else if r.Method == "POST" {
|
|
body, err := ioutil.ReadAll(r.Body)
|
|
if err != nil {
|
|
log.Printf("failed to read request body on inventoryPageHandler: %v", err)
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
inventories = inventorytools.Inventories{}
|
|
err = json.Unmarshal(body, &inventories)
|
|
if err != nil {
|
|
log.Printf("failed to unmarshal request body json to inventorytools.Inventories struct: %v", err)
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
log.Printf("processed inventory refresh")
|
|
return
|
|
}
|
|
}
|
|
|
|
// http - GET /listings
|
|
// When GET, produces a list of the items the retainers have listed on the market, sorted by their metric
|
|
func listingsPageHandler(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method == "GET" {
|
|
type ListedItem struct {
|
|
ID int
|
|
Name string
|
|
ListingMetric int
|
|
QuantityStored int
|
|
}
|
|
|
|
type Retainer struct {
|
|
Name string
|
|
ItemsListed []ListedItem
|
|
}
|
|
|
|
type PageData struct {
|
|
Title string
|
|
Retainers []Retainer
|
|
}
|
|
|
|
pageData := PageData{Title: "Listing"}
|
|
pageData.Retainers = make([]Retainer, 0)
|
|
|
|
action := r.URL.Query().Get("action")
|
|
|
|
// # The plan
|
|
// 1. Loop through retainers and build a list of listed items, their metric and number stored
|
|
// i. Loop through market listings
|
|
// ii. Get the metric from the item map
|
|
// iii. Check character and retainer bags for item and increment counter each time it's found
|
|
// 2. Sort the list of items by metric
|
|
|
|
quantityStored := make(map[int]int, 0) // key is item ID, value is number stored in retainer and character bag
|
|
|
|
// 1. Loop through retainers and build a list of listed items, their metric and number stored
|
|
for _, retainer := range inventories.Retainers {
|
|
tmpRetainer := Retainer{Name: retainer.Name}
|
|
tmpRetainer.ItemsListed = make([]ListedItem, 0)
|
|
|
|
for _, retainerMarketItem := range retainer.RetainerMarket {
|
|
if _, ok := items[retainerMarketItem.ID]; !ok { // if we skip this check, it results in invalid memory address error
|
|
log.Printf("skipping item with ID '%d' on listing page, as it doesn't exist in the items hash map. Is this a crafted item?", retainerMarketItem.ID) // can happen if retainers list items for sale that aren't crafted items
|
|
continue
|
|
}
|
|
|
|
tmpItem := ListedItem{ID: items[retainerMarketItem.ID].ID, Name: items[retainerMarketItem.ID].Name, ListingMetric: items[retainerMarketItem.ID].ListingMetric / 1000} // divide by 1000 to decrease number of digits for human simplicity
|
|
|
|
// Count how many of the items we have in character inventory
|
|
for _, item := range inventories.CharacterBags {
|
|
if item.ID == retainerMarketItem.ID {
|
|
if _, ok := items[retainerMarketItem.ID]; ok {
|
|
quantityStored[item.ID]++
|
|
} else {
|
|
quantityStored[item.ID] = 1
|
|
}
|
|
}
|
|
}
|
|
|
|
// Count how many of the items we have in THIS retainer inventories/storage
|
|
// A second pass later is required to set the quantity stored
|
|
for _, item := range retainer.RetainerBags {
|
|
if item.ID == retainerMarketItem.ID {
|
|
if _, ok := items[retainerMarketItem.ID]; ok {
|
|
quantityStored[item.ID]++
|
|
} else {
|
|
quantityStored[item.ID] = 1
|
|
}
|
|
}
|
|
}
|
|
|
|
if action == "refresh" { // If it's a refresh action, we queue it for an update
|
|
log.Printf("dynamic price update for %d triggered", retainerMarketItem.ID)
|
|
priceUpdateQueue <- retainerMarketItem.ID
|
|
updateRequestWg.Add(1)
|
|
}
|
|
tmpRetainer.ItemsListed = append(tmpRetainer.ItemsListed, tmpItem)
|
|
}
|
|
|
|
if action == "refresh" { // if refreshing, block until refresh is done the sort
|
|
updateRequestWg.Wait()
|
|
sortItemSlice() // required to update metrics
|
|
}
|
|
|
|
// 2. Sort the list of items
|
|
sort.Slice(tmpRetainer.ItemsListed, func(i, j int) bool {
|
|
return tmpRetainer.ItemsListed[i].ListingMetric > tmpRetainer.ItemsListed[j].ListingMetric
|
|
})
|
|
|
|
pageData.Retainers = append(pageData.Retainers, tmpRetainer)
|
|
}
|
|
|
|
// Update quantity stored
|
|
for _, retainer := range pageData.Retainers {
|
|
for _, itemListed := range retainer.ItemsListed {
|
|
itemListed.QuantityStored = quantityStored[itemListed.ID] // shouldn't need to check for existence of key in map as all these items should be in the map already...?
|
|
}
|
|
}
|
|
|
|
err := t.ExecuteTemplate(w, "listings.html", pageData)
|
|
if err != nil {
|
|
log.Printf("failed to execute template: %v", err)
|
|
}
|
|
|
|
}
|
|
}
|