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 } */