287 lines
8.5 KiB
Go
287 lines
8.5 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/csv"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"time"
|
|
|
|
"code.mashffxiv.deadbeef.codes/MashPotato/gilgetter/inventorytools"
|
|
"github.com/fsnotify/fsnotify"
|
|
)
|
|
|
|
var (
|
|
lastUpdated time.Time
|
|
inventories inventorytools.Inventories
|
|
retainers []inventorytools.Retainer
|
|
inventoriesFilePath string
|
|
)
|
|
|
|
func init() {
|
|
appDataDir, err := os.UserConfigDir()
|
|
if err != nil {
|
|
log.Fatal("Getting appdata directory failed:", err)
|
|
}
|
|
inventoriesFilePath = fmt.Sprintf("%s/XIVLauncher/pluginConfigs/GilGetterPlugin/inventories.csv", appDataDir)
|
|
|
|
setRetainers() // Create your own definitions that satisfy the retainer struct - see README.md
|
|
}
|
|
|
|
func main() {
|
|
|
|
// initial refresh and upload to gilgetter
|
|
err := refreshFile()
|
|
if err != nil {
|
|
log.Fatalf("failed to refresh file: %v", err)
|
|
}
|
|
|
|
// watch file for changes and make subsequent uploads
|
|
|
|
go inventoryFileWatcher()
|
|
go downloadsFileWatcher()
|
|
|
|
select {} // Block indefinitely
|
|
}
|
|
|
|
// Watches the inventory JSON file for InventoryTools for any changes
|
|
// When a change is detected, it will call refreshFile to read the file
|
|
// and post the data to gilgetter. There is a 3 second rate limiter and
|
|
// refreshing fails it will make one more attempt, one second later
|
|
func inventoryFileWatcher() {
|
|
inventoryWatcher, err := fsnotify.NewWatcher()
|
|
if err != nil {
|
|
log.Fatal("NewWatcher failed: ", err)
|
|
}
|
|
defer inventoryWatcher.Close()
|
|
|
|
done := make(chan bool)
|
|
go func() {
|
|
defer close(done)
|
|
defer log.Printf("file watcher go routine shutting down!") // could be from returns below?
|
|
|
|
for {
|
|
select {
|
|
|
|
case event, ok := <-inventoryWatcher.Events:
|
|
if !ok {
|
|
log.Printf("inventory watcher _event_ NOT OK TERMINATING ARRRGGHGHH: %v", err)
|
|
return
|
|
}
|
|
if event.Op == fsnotify.Write {
|
|
time.Sleep(time.Millisecond * 250) // Prevent opening at same time that inventorytools is still writing. Technically a race condition, but meh.
|
|
err := refreshFile()
|
|
if err != nil {
|
|
log.Printf("trying again in 1 second: failed to refresh file: %v", err)
|
|
time.Sleep(time.Second)
|
|
err := refreshFile()
|
|
if err != nil {
|
|
log.Printf("retry failed: failed to refresh file: %v", err)
|
|
continue
|
|
}
|
|
log.Printf("retry successful!")
|
|
}
|
|
}
|
|
case err, ok := <-inventoryWatcher.Errors:
|
|
if !ok {
|
|
log.Printf("inventory watcher _error_ NOT OK TERMINATING ARRRGGHGHH: %v", err)
|
|
return
|
|
}
|
|
log.Printf("inventory watcher error: %v", err)
|
|
}
|
|
}
|
|
|
|
}()
|
|
err = inventoryWatcher.Add(inventoriesFilePath)
|
|
if err != nil {
|
|
log.Fatal("inventory watcher add inventories failed:", err)
|
|
}
|
|
|
|
<-done
|
|
}
|
|
|
|
// Reads the JSON file provided by InventoryTools/AllaganTools
|
|
// Parses information of interest, then posts to gilgetter
|
|
func refreshFile() error {
|
|
if time.Now().Before(lastUpdated.Add(time.Second * 3)) {
|
|
return nil // Do not do anything if posted within 3 seconds
|
|
}
|
|
lastUpdated = time.Now()
|
|
|
|
f, err := os.Open(inventoriesFilePath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read file '%s': %v", inventoriesFilePath, err)
|
|
}
|
|
|
|
parseInventoryCSV(f)
|
|
|
|
/*err = unmarshalJSON(fileBytes) // update global variables directly, no assignment here
|
|
if err != nil {
|
|
return fmt.Errorf("failed to unmarshal JSON in file '%s': %v", inventoriesFilePath, err)
|
|
}*/
|
|
|
|
// marshall the transformed data and upload to gilgetter
|
|
jsonBytes, err := json.Marshal(inventories)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal JSON: %v", err)
|
|
}
|
|
|
|
reader := bytes.NewReader(jsonBytes)
|
|
|
|
resp, err := http.Post(gilgetterURL, "application/json", reader)
|
|
if err != nil {
|
|
return fmt.Errorf("error posting json to gilgetter: %v", err)
|
|
}
|
|
if resp.StatusCode != 200 {
|
|
return fmt.Errorf("error posting json to gilgetter: expected http status of '200' but got '%d'", resp.StatusCode)
|
|
}
|
|
|
|
log.Printf("refreshed inventory with gilgetter")
|
|
|
|
return nil
|
|
}
|
|
|
|
func parseInventoryCSV(f *os.File) error {
|
|
setRetainers() // reinitialize retainers from configuration
|
|
inventories = inventorytools.Inventories{CharacterBags: make([]inventorytools.Item, 0), Crystals: make([]inventorytools.Item, 0), Retainers: retainers}
|
|
|
|
r := csv.NewReader(f)
|
|
data, err := r.ReadAll()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read file with CSV reader: %v", err)
|
|
}
|
|
|
|
for i, row := range data {
|
|
/*
|
|
0 = container
|
|
1 = slot
|
|
2 = item id
|
|
3 = quantity
|
|
20 = sorted container
|
|
21 = sorted slot index
|
|
22 = retainer id
|
|
*/
|
|
item := inventorytools.Item{}
|
|
item.ID, err = strconv.Atoi(row[2])
|
|
if err != nil {
|
|
return fmt.Errorf("failed to convert item ID '%s' to int on csv row '%d': %v", row[2], i, err)
|
|
}
|
|
item.SortedContainer, err = strconv.Atoi(row[0])
|
|
if err != nil {
|
|
return fmt.Errorf("failed to convert sorted container '%s' to int on csv row '%d': %v", row[20], i, err)
|
|
}
|
|
item.SortedSlotIndex, err = strconv.Atoi(row[21])
|
|
if err != nil {
|
|
return fmt.Errorf("failed to convert sorted slot index '%s' to int on csv row '%d': %v", row[21], i, err)
|
|
}
|
|
item.Quantity, err = strconv.Atoi(row[3])
|
|
if err != nil {
|
|
return fmt.Errorf("failed to convert item quantity '%s' to int on csv row '%d': %v", row[3], i, err)
|
|
}
|
|
retID, err := strconv.Atoi(row[22])
|
|
if err != nil {
|
|
return fmt.Errorf("failed to convert retainerID '%s' to int on csv row '%d': %v", row[22], i, err)
|
|
}
|
|
|
|
// 0 - 4 = player bags (pages)
|
|
// 1000 = player equipped
|
|
// 2001 = player crystal bag
|
|
// 3200 - 3209 = armoury chest
|
|
// 10000 = retainer inventory page 1
|
|
// 10001 = retainer inventory page 2
|
|
// 10002 = retainer inventory page 3, etc
|
|
// 11000 = retainer equipped
|
|
// 12001 = retainer crystal bag
|
|
// 12002 = retainer market item
|
|
containerType, err := strconv.Atoi(row[0])
|
|
if err != nil {
|
|
return fmt.Errorf("failed to convert containerType '%s' to int on csv row '%d': %v", row[0], i, err)
|
|
}
|
|
|
|
if retID == characterID { // Handle Character items
|
|
if containerType < 4 { // player bags
|
|
inventories.CharacterBags = append(inventories.CharacterBags, item)
|
|
continue
|
|
}
|
|
if containerType == 2001 { // player crystals
|
|
inventories.Crystals = append(inventories.Crystals, item)
|
|
continue
|
|
}
|
|
|
|
}
|
|
|
|
retMatched := false
|
|
for i, retainer := range retainers {
|
|
if retID == retainer.ID {
|
|
retMatched = true
|
|
if containerType >= 10000 && containerType <= 10006 { // retainer inventory
|
|
inventories.Retainers[i].RetainerBags = append(inventories.Retainers[i].RetainerBags, item)
|
|
break
|
|
}
|
|
if containerType == 12002 { // market item
|
|
inventories.Retainers[i].RetainerMarket = append(inventories.Retainers[i].RetainerMarket, item)
|
|
break
|
|
}
|
|
fmt.Printf("Retainer matched but unhandled container: Container: %s | Slot: %s | Item ID: %s | Quantity: %s | SC: %s | SSI: %s | RET ID: %s\n", row[0], row[1], row[2], row[3], row[20], row[21], row[22])
|
|
}
|
|
}
|
|
if retMatched {
|
|
continue
|
|
}
|
|
|
|
fmt.Printf("UNHANDLED ITEM: Container: %s | Slot: %s | Item ID: %s | Quantity: %s | SC: %s | SSI: %s | RET ID: %s\n", row[0], row[1], row[2], row[3], row[20], row[21], row[22])
|
|
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
/*
|
|
// custom unmarshaller to make sense of the inventories.json file
|
|
// why the fuck do people use dynamic keys in json instead of structuring their data with static definitions
|
|
func unmarshalJSON(d []byte) error {
|
|
inventories = inventorytools.Inventories{}
|
|
|
|
// This is a map as the json keys are dynamic
|
|
tmp := map[string]json.RawMessage{}
|
|
err := json.Unmarshal(d, &tmp)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to unmarshal json to raw message: %v", err)
|
|
}
|
|
|
|
// Unmarshal the character bags and crystals for the set character
|
|
if _, ok := tmp[strconv.Itoa(characterID)]; !ok {x
|
|
return fmt.Errorf("unmarshaled json map has no key matching character ID '%d'", characterID)
|
|
}
|
|
|
|
err = json.Unmarshal(tmp[strconv.Itoa(characterID)], &inventories)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to unmarshal character inventory: %v", err)
|
|
}
|
|
|
|
// Unmarshal and transform the retainer bags and market postings
|
|
for _, retainer := range retainers {
|
|
|
|
if _, ok := tmp[strconv.Itoa(retainer.ID)]; !ok {
|
|
return fmt.Errorf("unmarshaled json map has no key matching retainer ID '%d'", retainer.ID)
|
|
}
|
|
|
|
tmpRetainer := inventorytools.Retainer{}
|
|
err = json.Unmarshal(tmp[strconv.Itoa(retainer.ID)], &tmpRetainer)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to unmarshal retainer '%s' with ID '%d': %v", retainer.Name, retainer.ID, err)
|
|
}
|
|
tmpRetainer.ID = retainer.ID
|
|
tmpRetainer.Name = retainer.Name
|
|
tmpRetainer.MarketItemStorage = retainer.MarketItemStorage
|
|
inventories.Retainers = append(inventories.Retainers, tmpRetainer)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
*/
|