add dalamud inventorytools integration (no features yet)
continuous-integration/drone/push Build is failing Details

This commit is contained in:
MashPotato 2022-07-31 12:45:48 -06:00
parent c4ce99ec7b
commit 10876ba1c6
9 changed files with 292 additions and 1 deletions

5
.gitignore vendored
View File

@ -2,4 +2,7 @@ garlandtools-dump/item/*
garlandtools-dump/output-data.json
gilgetter.exe
gilgetter
build-dev.sh
build-dev.sh
inventorytools/client/configuration.go
inventorytools/client/client.exe
inventorytools/client/client

4
go.mod
View File

@ -1,3 +1,7 @@
module code.mashffxiv.com/MashPotato/gilgetter
go 1.17
require github.com/fsnotify/fsnotify v1.5.4
require golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect

4
go.sum Normal file
View File

@ -0,0 +1,4 @@
github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

View File

@ -2,12 +2,15 @@ package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"sort"
"strconv"
"time"
inventorytools "code.mashffxiv.com/MashPotato/gilgetter/inventoryTools"
)
// http - GET /
@ -286,3 +289,33 @@ func staleItemsListHandler(w http.ResponseWriter, _ *http.Request) {
}
w.Write(jsonBytes)
}
// http - GET /inventory
// http - POST /inventory
// When POST, accepts a JSON object containing inventory data from the inventorytools-link 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" {
// TBD make actual page
w.Write([]byte(fmt.Sprintf("%v", inventories)))
} 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
}
}

5
inventorytools/README.md Normal file
View File

@ -0,0 +1,5 @@
# inventorytools
This is a library containing data model for integrating with the Dalamud plugin "InventoryTools". It comes bundled with the client-side uploader utility to upload your inventory data to gilgetter.
TBD: long term would make sense to make this whole functionality a standalone Dalamud plugin rather than piggy backing off another plugin which is uneccesary dependancy (subject to changing data model, become unsupported, etc). When this is done it probably makes sense to reverse the data flow - have gilgetter sync to the new dalamud plugin and move the UI to the plugin rather than UI being external web page served by gilgetter.

View File

@ -0,0 +1,48 @@
# inventorytools-client
This is the client
Monitors file for changes: %appdata%\XIVLauncher\pluginConfigs\InventoryTools\inventories.json
If any changes it will parse the file and then post the resulting json to an endpoint on gilgetter.
### Usage
To build, it requires creating a configuration.go (configuration in code because I'm super lazy) file in the client directory with the following:
```go
package main
import (
inventorytools "code.mashffxiv.com/MashPotato/gilgetter/inventorytools"
)
const (
characterID = 18014498565524067 // set your own
gilgetterURL = "https://gilgetter.mashffxiv.com/inventory" // set your own
)
func setRetainers() {
retainers = make([]inventorytools.Retainer, 0)
retainer := inventorytools.Retainer{
ID: 33777097240525369, // set your own
Name: "Miamoore", // set your own
MarketItemStorage: true, // set your own
}
retainers = append(retainers, retainer)
retainer = inventorytools.Retainer{
ID: 33777097241150467, // set your own
Name: "Superbe", // set your own
MarketItemStorage: false, // set your own
}
retainers = append(retainers, retainer)
}
```
Then you simply run the utility and leave it running in the background. It will synchronize your character inventory, retainer bag inventories and retainer market inventories to gilgetter

View File

@ -0,0 +1,167 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"strconv"
"time"
"code.mashffxiv.com/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/InventoryTools/inventories.json", 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
fileWatcher()
}
func fileWatcher() {
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Fatal("NewWatcher failed: ", err)
}
defer watcher.Close()
done := make(chan bool)
go func() {
defer close(done)
for {
log.Printf("watching file for changes")
select {
case event, ok := <-watcher.Events:
if !ok {
return
}
log.Printf("%s %s\n", event.Name, event.Op)
if event.Op == fsnotify.Write {
err := refreshFile()
if err != nil {
log.Fatalf("failed to refresh file: %v", err)
}
}
case err, ok := <-watcher.Errors:
if !ok {
return
}
log.Println("error:", err)
}
}
}()
err = watcher.Add(inventoriesFilePath)
if err != nil {
log.Fatal("Add failed:", err)
}
<-done
}
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()
// read and parse the InventoryTools state file
fileBytes, err := os.ReadFile(inventoriesFilePath)
if err != nil {
return fmt.Errorf("failed to read file inventories.json file: %v", err)
}
err = unmarshalJSON(fileBytes)
if err != nil {
return fmt.Errorf("failed to unmarshal JSON: %v", 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
}
// 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 for the set character
if _, ok := tmp[strconv.Itoa(characterID)]; !ok {
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
}

View File

@ -0,0 +1,23 @@
package inventorytools
type Inventories struct {
CharacterBags []Item `json:"CharacterBags,omitempty"`
Retainers []Retainer `json:"Retainers,omitempty"`
}
type Item struct {
ID int `json:"iid"`
SortedContainer int `json:"sc"` // container/page 1-5 offset by 9999 for bags and (12000 for market?)
SortedSlotIndex int `json:"ssi"` // sorted slot index 0-34
Quantity int `json:"qty"`
}
type Retainer struct {
ID int `json:"id"`
Name string `json:"name"`
MarketItemStorage bool `json:"marketitemstorage,omitempty"` // true if retainer is used to store items ready for listing on market
RetainerBags []Item `json:"RetainerBags,omitempty"`
RetainerMarket []Item `json:"RetainerMarket,omitempty"`
}

View File

@ -14,6 +14,8 @@ import (
"sync"
"text/template"
"time"
inventorytools "code.mashffxiv.com/MashPotato/gilgetter/inventoryTools"
)
var (
@ -31,6 +33,7 @@ var (
world string // filter Universalis to this world name
retainers []string // contains name of retainers
craftingCheckoutItemIDs []int // The item IDs which were selected on the crafting checkout page, used for shopping list
inventories inventorytools.Inventories
)
// Object to represent a crafting material / ingredient in a recipe
@ -195,6 +198,7 @@ func main() {
http.HandleFunc("/", homePageHandler)
http.HandleFunc("/stale-items", staleItemsPageHandler)
http.HandleFunc("/crafting-checkout", craftingCheckoutPageHandler)
http.HandleFunc("/inventory", inventoryPageHandler)
// API Handlers
// Anything that is called by the front end but is not a web page