| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208 |
- from __future__ import annotations
- import collections
- import dataclasses
- import json
- import math
- import typing
- import tap
- import cache
- from config import config
- import market
- class Args(tap.Tap):
- planets: tuple[str, ...]
- weight: float
- volume: float
- include_ship: tuple[str, ...] = ()
- def configure(self) -> None:
- self.add_argument('planets', nargs='+', metavar='planet') # take planets as positional args instead of flag
- def main() -> None:
- args = Args().parse_args()
- planets = [Planet(fio_burn) for fio_burn in get_fio_burn(args.planets)]
- if args.include_ship:
- stores: typing.Sequence[market.Storage] = cache.get('https://rest.fnar.net/storage/' + config.username,
- headers={'Authorization': config.fio_api_key})
- for ship in args.include_ship:
- ship_name, planet_name = ship.casefold().split('=')
- for store in stores:
- if store['Type'] == 'SHIP_STORE' and store['Name'].casefold() == ship_name:
- break
- else:
- raise Exception(f'ship storage {ship_name} not found')
- (planet,) = (p for p in planets if p.name.casefold() == planet_name)
- for item in store['StorageItems']:
- planet.inventory[item['MaterialTicker']] = planet.inventory.get(item['MaterialTicker'], 0) + item['MaterialAmount']
- raw_materials: typing.Sequence[Material] = cache.get('https://rest.fnar.net/material/allmaterials')
- materials = {mat['Ticker']: mat for mat in raw_materials}
- target_days = float('inf')
- for planet in planets:
- vol_per_day = weight_per_day = 0
- for consumption in planet.net_consumption:
- ticker = consumption['MaterialTicker']
- vol_per_day += materials[ticker]['Volume'] * consumption['net_consumption']
- weight_per_day += materials[ticker]['Weight'] * consumption['net_consumption']
- days = planet.inventory.get(ticker, 0) / consumption['net_consumption']
- if days < target_days:
- target_days = days
- print(planet.name, f'consumes {vol_per_day:.1f}㎥, {weight_per_day:.1f}t per day')
- optimal: dict[str, dict[str, int]] = None # pyright: ignore[reportAssignmentType]
- total_weight_used: float = None # pyright: ignore[reportAssignmentType]
- total_volume_used: float = None # pyright: ignore[reportAssignmentType]
- target_days = round(target_days + 0.05, 1)
- load_more = True
- while load_more:
- buys: dict[str, dict[str, int]] = {}
- iteration_weight = iteration_volume = 0
- for planet in planets:
- buy = planet.buy_for_target(target_days)
- weight_used, volume_used = shipping_used(materials, buy)
- iteration_weight += weight_used
- iteration_volume += volume_used
- if iteration_weight > args.weight or iteration_volume > args.volume:
- load_more = False
- break
- buys[planet.name] = buy
- if load_more:
- optimal = buys
- total_weight_used = iteration_weight
- total_volume_used = iteration_volume
- target_days += 0.1
- print('supply for', round(target_days, 1), 'days,', end=' ')
- print(f'consuming {round(total_weight_used, 1)}t and {round(total_volume_used, 1)}㎥') # pyright: ignore[reportPossiblyUnboundVariable]
- raw_prices: typing.Mapping[str, market.RawPrice] = {p['MaterialTicker']: p
- for p in cache.get('https://refined-prun.github.io/refined-prices/all.json') if p['ExchangeCode'] == 'IC1'}
- total_cost = 0
- for planet in planets:
- print('\n' + cyan(planet.name))
- for consumption in planet.net_consumption:
- ticker = consumption['MaterialTicker']
- avail = planet.inventory.get(ticker, 0)
- daily_consumption = consumption['net_consumption']
- days = avail / daily_consumption
- print(f'{ticker:>3}: {avail:5d} ({daily_consumption:8.2f}/d) {days:4.1f} d', end='')
- if need := optimal[planet.name].get(ticker): # pyright: ignore[reportOptionalMemberAccess]
- cost = raw_prices[ticker]['Ask'] * need
- total_cost += cost
- print(f' | {need:6.1f} (${cost:6.0f})')
- else:
- print()
- print(f'\ntotal cost: {total_cost:,}')
- combined_buy: dict[str, int] = collections.defaultdict(int)
- for buy in optimal.values():
- for ticker, amount in buy.items():
- combined_buy[ticker] += amount
- print(cyan('\nbuy:\n') + json.dumps({
- 'actions': [
- {'name': 'BuyItems', 'type': 'CX Buy', 'group': 'A1', 'exchange': 'IC1',
- 'priceLimits': {}, 'buyPartial': False, 'useCXInv': True},
- {'type': 'MTRA', 'name': 'TransferAction', 'group': 'A1',
- 'origin': 'Hortus Station Warehouse', 'dest': 'Configure on Execution'},
- ],
- 'global': {'name': 'supply ' + ' '.join(args.planets)},
- 'groups': [{
- 'type': 'Manual', 'name': 'A1', 'materials': {mat: amount for mat, amount in combined_buy.items()}
- }],
- }))
- for planet in planets:
- buy = optimal[planet.name]
- print(cyan(f'unload {planet.name}:\n') + json.dumps({
- 'actions': [
- {'type': 'MTRA', 'name': 'TransferAction', 'group': 'A1',
- 'origin': 'Configure on Execution', 'dest': planet.name + ' Base'},
- ],
- 'global': {'name': 'unload ' + planet.name},
- 'groups': [{
- 'type': 'Manual', 'name': 'A1', 'materials': {mat: amount for mat, amount in buy.items()}
- }],
- }))
- def get_fio_burn(planet_names: typing.Sequence[str]) -> typing.Iterator[FIOBurn]:
- planets: list[FIOBurn] = cache.get('https://rest.fnar.net/fioweb/burn/user/' + config.username,
- headers={'Authorization': config.fio_api_key})
- for name in planet_names:
- name = name.casefold()
- for planet_data in planets:
- if name in (planet_data['PlanetName'].casefold(), planet_data['PlanetNaturalId'].casefold()):
- assert planet_data['Error'] is None
- yield planet_data
- break
- else:
- raise ValueError(name + ' not found')
- def shipping_used(materials: dict[str, Material], buy: dict[str, int]) -> tuple[float, float]:
- weight = volume = 0
- for ticker, amount in buy.items():
- weight += amount * materials[ticker]['Weight']
- volume += amount * materials[ticker]['Volume']
- return weight, volume
- def cyan(text: str) -> str:
- return '\033[36m' + text + '\033[0m'
- class FIOBurn(typing.TypedDict):
- PlanetName: str
- PlanetNaturalId: str
- Error: typing.Any
- OrderConsumption: list[Amount]
- WorkforceConsumption: list[Amount]
- Inventory: list[market.StorageItem]
- OrderProduction: list[Amount]
- @dataclasses.dataclass(init=False, eq=False, slots=True)
- class Planet:
- name: str
- inventory: dict[str, int]
- net_consumption: typing.Sequence[Amount]
- def __init__(self, fio_burn: FIOBurn) -> None:
- self.name = fio_burn['PlanetName'] or fio_burn['PlanetNaturalId']
- self.inventory = {item['MaterialTicker']: item['MaterialAmount'] for item in fio_burn['Inventory']}
- producing = {item['MaterialTicker']: item for item in fio_burn['OrderProduction']}
- self.net_consumption = []
- for c in fio_burn['OrderConsumption'] + fio_burn['WorkforceConsumption']:
- net = c['DailyAmount']
- if production := producing.get(c['MaterialTicker']):
- net -= production['DailyAmount']
- if net < 0:
- continue
- c['net_consumption'] = net
- if c['MaterialTicker'] in config.supply.ignore_materials:
- continue
- self.net_consumption.append(c)
- def buy_for_target(self, target_days: float) -> dict[str, int]:
- buy: dict[str, int] = {}
- for consumption in self.net_consumption:
- ticker = consumption['MaterialTicker']
- avail = self.inventory.get(ticker, 0)
- daily_consumption = consumption['net_consumption']
- days = avail / daily_consumption
- if days < target_days:
- buy[ticker] = math.ceil((target_days - days) * daily_consumption)
- return buy
- class Amount(typing.TypedDict):
- MaterialTicker: str
- DailyAmount: float
- net_consumption: float
- class Material(typing.TypedDict):
- Ticker: str
- Weight: float
- Volume: float
- if __name__ == '__main__':
- main()
|