package core import ( "database/sql" "fmt" "log" "sync" "time" _ "github.com/mattn/go-sqlite3" "eve_haul_go/internal/esi" ) var RegionIDs = []int{ 10000002, // The Forge 10000016, // Lonetrek 10000030, // Heimatar 10000033, // The Citadel 10000032, // Sinq Laison 10000042, // Metropolis 10000064, // Essence 10000037, // Everyshore 10000043, // Domain 10000036, // Devoid 10000052, // Kador 10000067, // Genesis 10000068, // Verge Vendor 10000001, // Derelik 10000020, // Tash-Murkon } type Order struct { OrderID int `json:"order_id"` RegionID int `json:"region_id"` LocationID int `json:"location_id"` TypeID int `json:"type_id"` IsBuyOrder bool `json:"is_buy_order"` Price float64 `json:"price"` Issued time.Time `json:"issued"` VolumeRemain int `json:"volume_remain"` ItemName string `json:"item_name"` ItemCargoVolume float64 `json:"item_cargo_volume"` } type Trade struct { FromStation int `json:"from_station"` ToStation int `json:"to_station"` FromOrderID int `json:"from_order_id"` ToOrderID int `json:"to_order_id"` ItemName string `json:"item_name"` TypeID int `json:"type_id"` ItemCargoVolume float64 `json:"item_cargo_volume"` FromPrice float64 `json:"from_price"` ToPrice float64 `json:"to_price"` Quantity int `json:"quantity"` Cargo float64 `json:"cargo"` GrossProfit float64 `json:"gross_profit"` NetProfit *float64 `json:"net_profit,omitempty"` } type Market struct { DB *sql.DB ExpiresTimes map[int]time.Time Items map[int]*Item EsiClient *esi.EsiClient Mutex sync.RWMutex } func NewMarket(items map[int]*Item, esiClient *esi.EsiClient) (*Market, error) { db, err := sql.Open("sqlite3", ":memory:") if err != nil { return nil, err } _, err = db.Exec(` CREATE TABLE IF NOT EXISTS orders ( order_id INTEGER PRIMARY KEY, region_id INTEGER, location_id INTEGER, type_id INTEGER, is_buy_order BOOLEAN, price REAL, issued TEXT, volume_remain INTEGER, item_name TEXT, item_cargo_volume REAL ) `) if err != nil { return nil, err } _, err = db.Exec(`CREATE INDEX IF NOT EXISTS idx_arbitrage ON orders(type_id, is_buy_order, region_id)`) if err != nil { return nil, err } return &Market{ DB: db, ExpiresTimes: make(map[int]time.Time), Items: items, EsiClient: esiClient, }, nil } func (m *Market) UpdateOrders() error { m.Mutex.Lock() defer m.Mutex.Unlock() var wg sync.WaitGroup errChan := make(chan error, len(RegionIDs)) ordersChan := make(chan []Order, len(RegionIDs)) for _, regionID := range RegionIDs { expiryTime, exists := m.ExpiresTimes[regionID] if !exists || time.Now().UTC().After(expiryTime) { wg.Add(1) go func(rID int) { defer wg.Done() orders, err := m.downloadOrders(rID) if err != nil { errChan <- err return } ordersChan <- orders }(regionID) } } wg.Wait() close(errChan) close(ordersChan) var allErrors []error for err := range errChan { allErrors = append(allErrors, err) } if len(allErrors) > 0 { return fmt.Errorf("errors downloading orders: %v", allErrors) } for regionOrders := range ordersChan { if len(regionOrders) == 0 { continue } rID := regionOrders[0].RegionID tx, err := m.DB.Begin() if err != nil { return err } _, err = tx.Exec("DELETE FROM orders WHERE region_id = ?", rID) if err != nil { tx.Rollback() return err } stmt, err := tx.Prepare(`INSERT OR REPLACE INTO orders (order_id, region_id, location_id, type_id, is_buy_order, price, issued, volume_remain, item_name, item_cargo_volume) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`) if err != nil { tx.Rollback() return err } for _, o := range regionOrders { _, err = stmt.Exec(o.OrderID, o.RegionID, o.LocationID, o.TypeID, o.IsBuyOrder, o.Price, o.Issued.Format(time.RFC3339), o.VolumeRemain, o.ItemName, o.ItemCargoVolume) if err != nil { stmt.Close() tx.Rollback() return err } } stmt.Close() tx.Commit() } return nil } func (m *Market) downloadOrders(regionID int) ([]Order, error) { page := 1 var rawOrders []map[string]interface{} var lastExpiry string for { pageOrders, expiry, totalPages, err := m.EsiClient.GetRegionOrders(regionID, page) if err != nil { log.Printf("Failed to get region orders %d page %d: %v", regionID, page, err) break } if len(pageOrders) == 0 { break } rawOrders = append(rawOrders, pageOrders...) lastExpiry = expiry if page >= totalPages { break } page++ } if lastExpiry != "" { if parsed, err := time.Parse(time.RFC1123, lastExpiry); err == nil { m.ExpiresTimes[regionID] = parsed } else { m.ExpiresTimes[regionID] = time.Now().UTC() } } else { m.ExpiresTimes[regionID] = time.Now().UTC() } var formatted []Order for _, o := range rawOrders { typeID := int(o["type_id"].(float64)) item, exists := m.Items[typeID] if !exists { continue } cargoVol := item.Volume if cargoVol == nil || *cargoVol <= 0 { continue } issuedStr, _ := o["issued"].(string) issuedTime, _ := time.Parse("2006-01-02T15:04:05Z", issuedStr) isBuyOrder, _ := o["is_buy_order"].(bool) orderIDFloat, _ := o["order_id"].(float64) locIDFloat, _ := o["location_id"].(float64) price, _ := o["price"].(float64) volRemainFloat, _ := o["volume_remain"].(float64) formatted = append(formatted, Order{ OrderID: int(orderIDFloat), RegionID: regionID, LocationID: int(locIDFloat), TypeID: typeID, IsBuyOrder: isBuyOrder, Price: price, Issued: issuedTime, VolumeRemain: int(volRemainFloat), ItemName: item.TypeName, ItemCargoVolume: *cargoVol, }) } return formatted, nil } func (m *Market) ApplyOrderVolumeAdjustments(deductions map[int]int) error { m.Mutex.Lock() defer m.Mutex.Unlock() for orderID, volumeDeducted := range deductions { log.Printf("Deducting %d volume from order %d", volumeDeducted, orderID) _, err := m.DB.Exec("UPDATE orders SET volume_remain = volume_remain - ? WHERE order_id = ?", volumeDeducted, orderID) if err != nil { return err } } return nil }