gilgetter/inventorytools/client/main.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
}
*/