Commander cleanup
First impl of ship logs
Ship display improved
Store debugged
This commit is contained in:
Richard 2024-01-09 20:07:27 +01:00
parent 2181583843
commit 237dcc8c14
11 changed files with 562 additions and 374 deletions

13
main.py
View File

@ -2,18 +2,21 @@
import argparse import argparse
from nullptr.commander import Commander from nullptr.commander import Commander
import os import os
from nullptr.store_analyzer import StoreAnalyzer
from nullptr.models.base import Base from nullptr.models.base import Base
def main(args): def main(args):
if not os.path.isdir(args.data_dir): if not os.path.isdir(args.data_dir):
os.makedirs(args.data_dir ) os.makedirs(args.data_dir )
c = Commander(args.data_dir) if args.analyze:
c.run() a = StoreAnalyzer(verbose=True)
a.run(args.analyze)
else:
c = Commander(args.data_dir)
c.run()
# X1-AG74-41076A
# X1-KS52-51429E
if __name__ == '__main__': if __name__ == '__main__':
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('-d', '--data-dir', default='data') parser.add_argument('-d', '--data-dir', default='data')
parser.add_argument('-a', '--analyze', type=argparse.FileType('rb'))
args = parser.parse_args() args = parser.parse_args()
main(args) main(args)

View File

@ -7,6 +7,14 @@ from copy import copy
class AnalyzerException(Exception): class AnalyzerException(Exception):
pass pass
def path_dist(m):
t = 0
o = Point(0,0)
for w in m:
t +=w.distance(o)
o = w
return t
@dataclass @dataclass
class Point: class Point:
@ -70,7 +78,6 @@ class Analyzer:
possibles = sorted(candidates, key=lambda m: m[2]) possibles = sorted(candidates, key=lambda m: m[2])
possibles = possibles[:10] possibles = possibles[:10]
results = [] results = []
print(len(possibles))
for typ,m,d in possibles: for typ,m,d in possibles:
system = m.waypoint.system system = m.waypoint.system
p = self.find_path(origin, system) p = self.find_path(origin, system)
@ -101,7 +108,8 @@ class Analyzer:
path = [] path = []
mkts = [m.waypoint for m in self.store.all_members(orig.system, Marketplace)] mkts = [m.waypoint for m in self.store.all_members(orig.system, Marketplace)]
cur = orig cur = orig
if orig == to:
return []
while cur != to: while cur != to:
best = cur best = cur
bestdist = cur.distance(to) bestdist = cur.distance(to)
@ -156,30 +164,25 @@ class Analyzer:
def find_trade(self, system): def find_trade(self, system):
prices = self.prices(system) prices = self.prices(system)
occupied_resources = set()
for s in self.store.all('Ship'):
if s.mission != 'haul':
continue
occupied_resources.add(s.mission_state['resource'])
best = None best = None
for resource, markets in prices.items(): for resource, markets in prices.items():
if resource in occupied_resources:
continue
source = sorted(markets, key=lambda x: x['buy'])[0] source = sorted(markets, key=lambda x: x['buy'])[0]
dest = sorted(markets, key=lambda x: x['sell'])[-1] dest = sorted(markets, key=lambda x: x['sell'])[-1]
margin = dest['sell'] -source['buy'] margin = dest['sell'] -source['buy']
if margin < 0:
continue
dist = source['wp'].distance(dest['wp']) dist = source['wp'].distance(dest['wp'])
dist = max(dist, 0.0001) dist = max(dist, 0.0001)
score = margin / dist score = margin / dist
if margin < 0:
continue
o = TradeOption(resource, source['wp'], dest['wp'], margin, dist, score) o = TradeOption(resource, source['wp'], dest['wp'], margin, dist, score)
if best is None or best.score < o.score: if best is None or best.score < o.score:
best = o best = o
return best return best
def market_scan(self, system):
m = [w.waypoint for w in self.store.all_members(system, Marketplace) if not w.is_fuel()]
ms = len(m)
path = self.solve_tsp(m)
cur = Point(0,0)
for w in path:
print(w, w.distance(cur))
cur = w
far = self.store.get(Waypoint, 'X1-NN7-B7')
for w in path:
print(w, w.distance(far))

View File

@ -178,6 +178,7 @@ class Api:
def navigate(self, ship, wp): def navigate(self, ship, wp):
data = {'waypointSymbol': str(wp)} data = {'waypointSymbol': str(wp)}
response = self.request('post', f'my/ships/{ship}/navigate', data) response = self.request('post', f'my/ships/{ship}/navigate', data)
ship.log(f'nav to {wp}')
ship.update(response) ship.update(response)
def dock(self, ship): def dock(self, ship):
@ -193,6 +194,7 @@ class Api:
def flight_mode(self, ship, mode): def flight_mode(self, ship, mode):
data = {'flightMode': mode} data = {'flightMode': mode}
data = self.request('patch', f'my/ships/{ship}/nav', data) data = self.request('patch', f'my/ships/{ship}/nav', data)
ship.update(data)
return data return data
def jump(self, ship, waypoint): def jump(self, ship, waypoint):
@ -243,6 +245,7 @@ class Api:
'units': units 'units': units
} }
data = self.request('post', f'my/ships/{ship}/sell', data) data = self.request('post', f'my/ships/{ship}/sell', data)
ship.log(f'sell {units} of {typ}')
if 'cargo' in data: if 'cargo' in data:
ship.update(data) ship.update(data)
if 'agent' in data: if 'agent' in data:
@ -255,6 +258,7 @@ class Api:
'units': amt 'units': amt
} }
data = self.request('post', f'my/ships/{ship}/purchase', data) data = self.request('post', f'my/ships/{ship}/purchase', data)
ship.log(f'buy {amt} of {typ} at {ship.location}')
if 'cargo' in data: if 'cargo' in data:
ship.update(data) ship.update(data)
if 'agent' in data: if 'agent' in data:
@ -271,6 +275,7 @@ class Api:
'units': units 'units': units
} }
data = self.request('post', f'my/ships/{ship.symbol}/jettison', data) data = self.request('post', f'my/ships/{ship.symbol}/jettison', data)
ship.log(f'drop {units} of {typ}')
if 'cargo' in data: if 'cargo' in data:
ship.update(data) ship.update(data)
if 'agent' in data: if 'agent' in data:

View File

@ -1,8 +1,8 @@
from nullptr.command_line import CommandLine from nullptr.command_line import CommandLine
from nullptr.store import Store from nullptr.store import Store
from nullptr.analyzer import Analyzer, Point from nullptr.analyzer import Analyzer, Point, path_dist
import argparse import argparse
from nullptr.models import * from nullptr.models import *
from nullptr.api import Api from nullptr.api import Api
from .util import * from .util import *
from time import sleep, time from time import sleep, time
@ -11,7 +11,7 @@ from nullptr.central_command import CentralCommand
import readline import readline
import os import os
from copy import copy from copy import copy
class CommandError(Exception): class CommandError(Exception):
pass pass
@ -33,23 +33,27 @@ class Commander(CommandLine):
self.stop_auto= False self.stop_auto= False
super().__init__() super().__init__()
######## INFRA #########
def handle_eof(self): def handle_eof(self):
self.store.close() self.store.close()
readline.write_history_file(self.hist_file) readline.write_history_file(self.hist_file)
print("Goodbye!") print("Goodbye!")
def do_pp(self):
pprint(self.api.last_result)
def prompt(self): def prompt(self):
if self.ship: if self.ship:
return f'{self.ship.symbol}> ' return f'{self.ship.symbol}> '
else: else:
return '> ' return '> '
def has_ship(self): def after_cmd(self):
if self.ship is not None: self.store.flush()
return True
else: def do_auto(self):
print('set a ship') self.centcom.run_interactive()
######## Resolvers #########
def ask_obj(self, typ, prompt): def ask_obj(self, typ, prompt):
obj = None obj = None
while obj is None: while obj is None:
@ -58,7 +62,13 @@ class Commander(CommandLine):
if obj is None: if obj is None:
print('not found') print('not found')
return obj return obj
def has_ship(self):
if self.ship is not None:
return True
else:
print('set a ship')
def select_agent(self): def select_agent(self):
agents = self.store.all(Agent) agents = self.store.all(Agent)
agent = next(agents, None) agent = next(agents, None)
@ -66,6 +76,27 @@ class Commander(CommandLine):
agent = self.agent_setup() agent = self.agent_setup()
return agent return agent
def resolve(self, typ, arg):
arg = arg.upper()
matches = [c for c in self.store.all(typ) if c.symbol.startswith(arg)]
if len(matches) == 1:
return matches[0]
elif len(matches) > 1:
raise CommandError('multiple matches')
else:
raise CommandError('not found')
def resolve_system(self, system_str):
if type(system_str) == System:
return system_str
if system_str == '':
if not self.has_ship(): return
system = self.ship.location.system
else:
system = self.store.get(System, system_str)
return system
######## First run #########
def agent_setup(self): def agent_setup(self):
symbol = input('agent name: ') symbol = input('agent name: ')
agent = self.store.get(Agent, symbol, create=True) agent = self.store.get(Agent, symbol, create=True)
@ -90,38 +121,186 @@ class Commander(CommandLine):
self.store.flush() self.store.flush()
return agent return agent
def resolve(self, typ, arg): def do_token(self):
arg = arg.upper() print(self.agent.token)
matches = [c for c in self.store.all(typ) if c.symbol.startswith(arg)]
if len(matches) == 1:
return matches[0]
elif len(matches) > 1:
raise CommandError('multiple matches')
else:
raise CommandError('not found')
def resolve_system(self, system_str): def do_register(self, faction):
if type(system_str) == System: self.api.register(faction.upper())
return system_str pprint(self.api.agent)
if system_str == '':
if not self.has_ship(): return ######## Fleet #########
system = self.ship.location.system
else:
system = self.store.get(System, system_str)
return system
def after_cmd(self):
self.store.flush()
def do_info(self, arg=''): def do_info(self, arg=''):
if arg.startswith('r'): if arg.startswith('r'):
self.api.info() self.api.info()
pprint(self.agent, 100) pprint(self.agent, 100)
def do_auto(self): def do_ships(self, arg=''):
self.centcom.run_interactive() if arg.startswith('r'):
r = self.api.list_ships()
else:
r = sorted(list(self.store.all('Ship')))
pprint(r)
def do_ship(self, arg=''):
if arg != '':
symbol = f'{self.agent.symbol}-{arg}'
ship = self.store.get('Ship', symbol)
if ship is None:
print('not found')
return
else:
self.ship = ship
pprint(self.ship, 5)
######## Atlas #########
def do_systems(self, page=1):
r = self.api.list_systems(int(page))
pprint(self.api.last_meta)
def do_catalog(self, system_str=''):
system = self.resolve_system(system_str)
r = self.api.list_waypoints(system)
for w in r:
if 'MARKETPLACE' in w.traits:
self.api.marketplace(w)
if w.type == 'JUMP_GATE':
self.api.jumps(w)
def do_system(self, system_str):
system = self.store.get(System, system_str)
r = self.api.list_waypoints(system)
pprint(r)
def do_waypoints(self, system_str=''):
loc = None
if system_str == '':
if not self.has_ship(): return
loc = self.ship.location
system = loc.system
else:
system = self.store.get(System, system_str)
print(f'=== waypoints in {system}')
r = self.store.all_members(system, 'Waypoint')
for w in r:
wname = w.symbol.split('-')[2]
traits = ", ".join(w.traits())
typ = w.type[0]
if typ not in ['F','J'] and len(traits) == 0:
continue
if loc:
dist = loc.distance(w)
print(f'{wname:4} {typ} {dist:6} {traits}')
else:
print(f'{wname:4} {typ} {traits}')
def do_members(self):
if not self.has_ship(): return
system = self.ship.location.system
pprint(list(self.store.all_members(system)))
def do_wp(self, s=''):
self.do_waypoints(s)
######## Specials #########
def do_market(self, arg=''):
if arg == '':
if not self.has_ship(): return
waypoint = self.ship.location
else:
waypoint = self.resolve('Waypoint', arg)
r = self.api.marketplace(waypoint)
pprint(r, 3)
def do_atlas(self, state=None):
atlas = self.store.get(Atlas, 'ATLAS')
if state is not None:
atlas.enabled = True if state == 'on' else 'off'
pprint(atlas, 5)
def do_jumps(self, waypoint_str=None):
if waypoint_str is None:
if not self.has_ship(): return
waypoint = self.ship.location
else:
waypoint = self.store.get(Waypoint, waypoint_str.upper())
r = self.api.jumps(waypoint)
pprint(r)
def do_shipyard(self):
if not self.has_ship(): return
location = self.ship.location
data = self.api.shipyard(location)
for s in must_get(data, 'ships'):
print(s['type'], s['purchasePrice'])
######## Commerce #########
def do_refuel(self, source='market'):
if not self.has_ship(): return
from_cargo = source != 'market'
r = self.api.refuel(self.ship, from_cargo=from_cargo)
pprint(r)
def do_cargo(self):
if not self.has_ship(): return
print(f'== Cargo {self.ship.cargo_units}/{self.ship.cargo_capacity} ==')
for c, units in self.ship.cargo.items():
print(f'{units:4d} {c}')
def do_buy(self, resource, amt=None):
if not self.has_ship(): return
if amt is None:
amt = self.ship.cargo_capacity - self.ship.cargo_units
self.api.buy(self.ship, resource.upper(), amt)
self.do_cargo()
def do_sell(self, resource, amt=None):
if not self.has_ship(): return
self.api.sell(self.ship, resource.upper(), amt)
self.do_cargo()
def do_dump(self, resource):
if not self.has_ship(): return
self.api.jettison(self.ship, resource.upper())
self.do_cargo()
def do_purchase(self, ship_type):
if not self.has_ship(): return
location = self.ship.location
ship_type = ship_type.upper()
if not ship_type.startswith('SHIP'):
ship_type = 'SHIP_' + ship_type
s = self.api.purchase(ship_type, location)
pprint(s)
######## Mining #########
def do_siphon(self):
if not self.has_ship(): return
data = self.api.siphon(self.ship)
pprint(data)
def do_survey(self):
if not self.has_ship(): return
r = self.api.survey(self.ship)
pprint(r)
def do_surveys(self):
pprint(list(self.store.all('Survey')))
def do_extract(self, survey_str=''):
if not self.has_ship(): return
survey = None
if survey_str != '':
survey = self.resolve('Survey', survey_str)
result = self.api.extract(self.ship, survey)
symbol = mg(result,'extraction.yield.symbol')
units = mg(result,'extraction.yield.units')
print(units, symbol)
######## Missions #########
def print_mission(self): def print_mission(self):
print(f'mission: {self.ship.mission} ({self.ship.mission_status})') print(f'mission: {self.ship.mission} ({self.ship.mission_status})')
pprint(self.ship.mission_state) pprint(self.ship.mission_state)
@ -155,11 +334,45 @@ class Commander(CommandLine):
if not self.has_ship(): return if not self.has_ship(): return
self.centcom.set_mission_param(self.ship, nm, val) self.centcom.set_mission_param(self.ship, nm, val)
######## Contracts #########
def active_contract(self): def active_contract(self):
for c in self.store.all('Contract'): for c in self.store.all('Contract'):
if c.accepted and not c.fulfilled: return c if c.accepted and not c.fulfilled: return c
raise CommandError('no active contract') raise CommandError('no active contract')
def do_contracts(self, arg=''):
if arg.startswith('r'):
r = self.api.list_contracts()
else:
r = list(self.store.all('Contract'))
pprint(r)
def do_negotiate(self):
if not self.has_ship(): return
r = self.api.negotiate(self.ship)
pprint(r)
def do_accept(self, c):
contract = self.resolve('Contract', c)
r = self.api.accept_contract(contract)
pprint(r)
def do_deliver(self):
if not self.has_ship(): return
site = self.ship.location
contract = self.active_contract()
delivery = contract.unfinished_delivery()
if delivery is None:
raise CommandError('no delivery')
resource = delivery['trade_symbol']
self.api.deliver(self.ship, resource, contract)
pprint(contract)
def do_fulfill(self):
contract = self.active_contract()
self.api.fulfill(contract)
######## Automissions #########
def do_cmine(self): def do_cmine(self):
if not self.has_ship(): return if not self.has_ship(): return
site = self.ship.location site = self.ship.location
@ -219,203 +432,24 @@ class Commander(CommandLine):
self.centcom.set_mission_param(self.ship, 'hops', markets) self.centcom.set_mission_param(self.ship, 'hops', markets)
self.print_mission() self.print_mission()
def totaldist(self, m):
t = 0
o = Point(0,0)
for w in m:
t +=w.distance(o)
o = w
return t
def do_sprobe(self): def do_sprobe(self):
if not self.has_ship(): return if not self.has_ship(): return
system = self.ship.location.system system = self.ship.location.system
m = [m.waypoint for m in self.store.all_members(system, 'Marketplace')] m = [m.waypoint for m in self.store.all_members(system, 'Marketplace')]
print("pre", self.totaldist(m)) print("pre", path_dist(m))
m = self.analyzer.solve_tsp(m) m = self.analyzer.solve_tsp(m)
print("post", self.totaldist(m)) print("post", path_dist(m))
hops = [w.symbol for w in m] hops = [w.symbol for w in m]
self.centcom.init_mission(self.ship, 'probe') self.centcom.init_mission(self.ship, 'probe')
self.centcom.set_mission_param(self.ship, 'hops', hops) self.centcom.set_mission_param(self.ship, 'hops', hops)
self.print_mission() self.print_mission()
######## Travel #########
def do_travel(self, dest): def do_travel(self, dest):
dest = self.resolve('Waypoint', dest) dest = self.resolve('Waypoint', dest)
self.centcom.init_mission(self.ship, 'travel') self.centcom.init_mission(self.ship, 'travel')
self.centcom.set_mission_param(self.ship, 'dest', dest) self.centcom.set_mission_param(self.ship, 'dest', dest)
self.print_mission() self.print_mission()
def do_register(self, faction):
self.api.register(faction.upper())
pprint(self.api.agent)
def do_systems(self, page=1):
r = self.api.list_systems(int(page))
pprint(self.api.last_meta)
def do_stats(self):
total = 0
for t in self.store.data:
num = len(self.store.data[t])
nam = t.__name__
total += num
print(f'{num:5d} {nam}')
print(f'{total:5d} total')
def do_defrag(self):
self.store.defrag()
def do_catalog(self, system_str=''):
system = self.resolve_system(system_str)
r = self.api.list_waypoints(system)
for w in r:
if 'MARKETPLACE' in w.traits:
self.api.marketplace(w)
if w.type == 'JUMP_GATE':
self.api.jumps(w)
def do_market_scan(self):
if not self.has_ship(): return
loc = self.ship.location.system
pprint(self.analyzer.market_scan(loc))
def do_system(self, system_str):
system = self.store.get(System, system_str)
r = self.api.list_waypoints(system)
pprint(r)
def do_waypoints(self, system_str=''):
loc = None
if system_str == '':
if not self.has_ship(): return
loc = self.ship.location
system = loc.system
else:
system = self.store.get(System, system_str)
print(f'=== waypoints in {system}')
r = self.store.all_members(system, 'Waypoint')
for w in r:
traits = []
if w.type == 'JUMP_GATE':
traits.append('JUMP')
if w.type == 'GAS_GIANT':
traits.append('GAS')
if 'SHIPYARD' in w.traits:
traits.append('SHIPYARD')
if 'MARKETPLACE' in w.traits:
traits.append('MARKET')
if w.type == 'ASTEROID':
if 'COMMON_METAL_DEPOSITS' in w.traits:
traits.append('METAL')
if 'PRECIOUS_METAL_DEPOSITS' in w.traits:
traits.append('GOLD')
if 'MINERAL_DEPOSITS' in w.traits:
traits.append('MINS')
if 'STRIPPED' in w.traits:
traits.append('STRIPPED')
wname = w.symbol.split('-')[2]
traits = ', '.join(traits)
typ = w.type[0]
if typ not in ['F','J'] and len(traits) == 0:
continue
if loc:
dist = loc.distance(w)
print(f'{wname:4} {typ} {dist:6} {traits}')
else:
print(f'{wname:4} {typ} {traits}')
def do_members(self):
if not self.has_ship(): return
system = self.ship.location.system
pprint(list(self.store.all_members(system)))
def do_wp(self, s=''):
self.do_waypoints(s)
def do_marketplace(self, waypoint_str):
waypoint = self.store.get(Waypoint, waypoint_str.upper())
r = self.api.marketplace(waypoint)
def do_atlas(self, state=None):
atlas = self.store.get(Atlas, 'ATLAS')
if state is not None:
atlas.enabled = True if state == 'on' else 'off'
pprint(atlas, 5)
def do_jumps(self, waypoint_str=None):
if waypoint_str is None:
if not self.has_ship(): return
waypoint = self.ship.location
else:
waypoint = self.store.get(Waypoint, waypoint_str.upper())
r = self.api.jumps(waypoint)
pprint(r)
def do_query(self, resource):
if not self.has_ship(): return
location = self.ship.location
resource = resource.upper()
print('Found markets:')
for typ, m, d, plen in self.analyzer.find_closest_markets(resource, 'buy,exchange',location):
price = '?'
if resource in m.prices:
price = m.prices[resource]['buy']
print(m, typ[0], f'{plen-1:3} hops {price}')
def do_path(self):
orig = self.ask_obj(System, 'from: ')
dest = self.ask_obj(System, 'to: ')
# orig = self.store.get(System, 'X1-KS52')
# dest = self.store.get(System, 'X1-DA90')
path = self.analyzer.find_path(orig, dest)
pprint(path)
def do_ships(self, arg=''):
if arg.startswith('r'):
r = self.api.list_ships()
else:
r = sorted(list(self.store.all('Ship')))
pprint(r)
def do_contracts(self, arg=''):
if arg.startswith('r'):
r = self.api.list_contracts()
else:
r = list(self.store.all('Contract'))
pprint(r)
def do_deliver(self):
if not self.has_ship(): return
site = self.ship.location
contract = self.active_contract()
delivery = contract.unfinished_delivery()
if delivery is None:
raise CommandError('no delivery')
resource = delivery['trade_symbol']
self.api.deliver(self.ship, resource, contract)
pprint(contract)
def do_fulfill(self):
contract = self.active_contract()
self.api.fulfill(contract)
def do_ship(self, arg=''):
if arg != '':
symbol = f'{self.agent.symbol}-{arg}'
ship = self.store.get('Ship', symbol)
if ship is None:
print('not found')
return
else:
self.ship = ship
pprint(self.ship)
def do_pp(self):
pprint(self.api.last_result)
def do_go(self, arg): def do_go(self, arg):
if not self.has_ship(): return if not self.has_ship(): return
@ -435,19 +469,6 @@ class Commander(CommandLine):
self.api.orbit(self.ship) self.api.orbit(self.ship)
pprint(self.ship) pprint(self.ship)
def do_siphon(self):
if not self.has_ship(): return
data = self.api.siphon(self.ship)
pprint(data)
def do_negotiate(self):
if not self.has_ship(): return
r = self.api.negotiate(self.ship)
pprint(r)
def do_token(self):
print(self.agent.token)
def do_speed(self, speed): def do_speed(self, speed):
if not self.has_ship(): return if not self.has_ship(): return
speed = speed.upper() speed = speed.upper()
@ -455,27 +476,44 @@ class Commander(CommandLine):
if speed not in speeds: if speed not in speeds:
print('please choose from:', speeds) print('please choose from:', speeds)
self.api.flight_mode(self.ship, speed) self.api.flight_mode(self.ship, speed)
def do_refuel(self, source='market'):
if not self.has_ship(): return
from_cargo = source != 'market'
r = self.api.refuel(self.ship, from_cargo=from_cargo)
pprint(r)
def do_accept(self, c):
contract = self.resolve('Contract', c)
r = self.api.accept_contract(contract)
pprint(r)
def do_market(self, arg=''): def do_jump(self, waypoint_str):
if arg == '': if not self.has_ship(): return
if not self.has_ship(): return w = self.resolve('Waypoint', waypoint_str)
waypoint = self.ship.location self.api.jump(self.ship, w)
else: pprint(self.ship)
waypoint = self.resolve('Waypoint', arg)
r = self.api.marketplace(waypoint) ######## Analysis #########
pprint(r, 3) def do_stats(self):
total = 0
for t in self.store.data:
num = len(self.store.data[t])
nam = t.__name__
total += num
print(f'{num:5d} {nam}')
print(f'{total:5d} total')
def do_defrag(self):
self.store.defrag()
def do_query(self, resource):
if not self.has_ship(): return
location = self.ship.location
resource = resource.upper()
print('Found markets:')
for typ, m, d, plen in self.analyzer.find_closest_markets(resource, 'buy,exchange',location):
price = '?'
if resource in m.prices:
price = m.prices[resource]['buy']
print(m, typ[0], f'{plen-1:3} hops {price}')
def do_findtrade(self):
if not self.has_ship(): return
system = self.ship.location.system
t = self.analyzer.find_trade(system)
pprint(t)
def do_prices(self, resource=None): def do_prices(self, resource=None):
if not self.has_ship(): return if not self.has_ship(): return
system = self.ship.location.system system = self.ship.location.system
@ -484,68 +522,3 @@ class Commander(CommandLine):
pprint(prices[resource.upper()]) pprint(prices[resource.upper()])
else: else:
pprint(prices) pprint(prices)
def do_cargo(self):
if not self.has_ship(): return
print(f'== Cargo {self.ship.cargo_units}/{self.ship.cargo_capacity} ==')
for c, units in self.ship.cargo.items():
print(f'{units:4d} {c}')
def do_buy(self, resource, amt=None):
if not self.has_ship(): return
if amt is None:
amt = self.ship.cargo_capacity - self.ship.cargo_units
self.api.buy(self.ship, resource.upper(), amt)
self.do_cargo()
def do_sell(self, resource, amt=None):
if not self.has_ship(): return
self.api.sell(self.ship, resource.upper(), amt)
self.do_cargo()
def do_dump(self, resource):
if not self.has_ship(): return
self.api.jettison(self.ship, resource.upper())
self.do_cargo()
def do_shipyard(self):
if not self.has_ship(): return
location = self.ship.location
data = self.api.shipyard(location)
for s in must_get(data, 'ships'):
print(s['type'], s['purchasePrice'])
def do_jump(self, waypoint_str):
if not self.has_ship(): return
w = self.resolve('Waypoint', waypoint_str)
self.api.jump(self.ship, w)
pprint(self.ship)
def do_purchase(self, ship_type):
if not self.has_ship(): return
location = self.ship.location
ship_type = ship_type.upper()
if not ship_type.startswith('SHIP'):
ship_type = 'SHIP_' + ship_type
s = self.api.purchase(ship_type, location)
pprint(s)
def do_survey(self):
if not self.has_ship(): return
r = self.api.survey(self.ship)
pprint(r)
def do_surveys(self):
pprint(list(self.store.all('Survey')))
def do_extract(self, survey_str=''):
if not self.has_ship(): return
survey = None
if survey_str != '':
survey = self.resolve('Survey', survey_str)
result = self.api.extract(self.ship, survey)
symbol = mg(result,'extraction.yield.symbol')
units = mg(result,'extraction.yield.units')
print(units, symbol)

View File

@ -22,13 +22,20 @@ class Base:
identifier = 'symbol' identifier = 'symbol'
def __init__(self, symbol, store): def __init__(self, symbol, store):
self.disable_dirty = True self._disable_dirty = True
self.file_offset = None self._file_offset = None
self.store = store self.store = store
self.symbol = symbol self.symbol = symbol
self.define() self.define()
self.disable_dirty = False self._disable_dirty = False
def __setstate__(self, d):
self.__init__(d['symbol'], d['store'])
self.__dict__.update(d)
def __getstate__(self):
return {k:v for k,v in self.__dict__.items() if not k.startswith('_')}
@classmethod @classmethod
def ext(cls): def ext(cls):
raise NotImplementedError('no ext') raise NotImplementedError('no ext')
@ -73,7 +80,7 @@ class Base:
setattr(self, attr, lst) setattr(self, attr, lst)
def __setattr__(self, name, value): def __setattr__(self, name, value):
if name not in ['symbol','store','disable_dirty', 'file_offset'] and not self.disable_dirty: if not name.startswith('_') and not self._disable_dirty:
self.store.dirty(self) self.store.dirty(self)
if issubclass(type(value), Base): if issubclass(type(value), Base):
value = Reference.create(value) value = Reference.create(value)
@ -91,11 +98,6 @@ class Base:
def is_expired(self): def is_expired(self):
return False return False
def load(self, d):
self.disable_dirty = True
self.__dict__.update(d)
self.disable_dirty = False
def type(self): def type(self):
return self.__class__.__name__ return self.__class__.__name__

View File

@ -2,6 +2,7 @@ from .base import Base
from time import time from time import time
from nullptr.util import * from nullptr.util import *
from nullptr.models import Waypoint from nullptr.models import Waypoint
import os
class Ship(Base): class Ship(Base):
def define(self): def define(self):
@ -18,7 +19,19 @@ class Ship(Base):
self.mission:str = None self.mission:str = None
self.mission_status:str = 'init' self.mission_status:str = 'init'
self.role = None self.role = None
self.frame = ''
self.speed = "CRUISE"
self._log_file = None
def log(self, m):
if self._log_file is None:
fn = os.path.join(self.store.data_dir, f'{self.symbol}.{self.ext()}.log')
self._log_file = open(fn, 'a')
ts = int(time())
m = m.strip()
self._log_file.write(f'{ts} {m}\n')
self._log_file.flush()
@classmethod @classmethod
def ext(self): def ext(self):
return 'shp' return 'shp'
@ -30,6 +43,8 @@ class Ship(Base):
def update(self, d): def update(self, d):
self.seta('status', d, 'nav.status') self.seta('status', d, 'nav.status')
self.seta('speed', d, "nav.flightMode")
self.seta('frame', d, 'frame.name')
getter = self.store.getter(Waypoint, create=True) getter = self.store.getter(Waypoint, create=True)
self.seta('location', d, 'nav.waypointSymbol', interp=getter) self.seta('location', d, 'nav.waypointSymbol', interp=getter)
self.seta('cargo_capacity', d, 'cargo.capacity') self.seta('cargo_capacity', d, 'cargo.capacity')
@ -104,16 +119,45 @@ class Ship(Base):
self.update_timers() self.update_timers()
arrival = int(self.arrival - time()) arrival = int(self.arrival - time())
cooldown = int(self.cooldown - time()) cooldown = int(self.cooldown - time())
r = self.symbol
if detail > 1: role = self.role
if self.role is not None: if role is None:
r += f' {self.role}' role = 'none'
r += ' ' + self.status mstatus = self.mission_status
r += f' [{self.fuel_current}/{self.fuel_capacity}]' if mstatus == 'error':
r += ' ' + str(self.location) mstatus = mstatus.upper()
status = self.status.lower()
if status.startswith('in_'):
status = status[3:]
if detail < 2:
r = self.symbol
elif detail == 2:
symbol = self.symbol.split('-')[1]
r = f'{symbol:<2} {role:7} {mstatus:8} {str(self.location):11}'
if self.is_travelling(): if self.is_travelling():
r += f' [A: {arrival}]' r += f' [A: {arrival}]'
if self.is_cooldown(): if self.is_cooldown():
r += f' [C: {cooldown}]' r += f' [C: {cooldown}]'
else:
r = f'== {self.symbol} {self.frame} ==\n'
r += f'Role: {role}\n'
r += f'Mission: {self.mission} ({mstatus})\n'
for k, v in self.mission_state.items():
r += f' {k}: {v}\n'
adj = 'to' if self.status == 'IN_TRANSIT' else 'at'
r += f'Status {self.status} {adj} {self.location}\n'
r += f'Fuel: {self.fuel_current}/{self.fuel_capacity}\n'
r += f'Speed: {self.speed}\n'
r += f'Cargo: {self.cargo_units}/{self.cargo_capacity}\n'
for res, u in self.cargo.items():
r += f' {res}: {u}\n'
if self.is_travelling():
r += f'Arrival: {arrival} seconds\n'
if self.is_cooldown():
r += f'Cooldown: {cooldown} seconds \n'
return r return r

View File

@ -32,3 +32,26 @@ class Waypoint(Base):
def ext(self): def ext(self):
return 'way' return 'way'
def traits(self):
traits = []
if self.type == 'JUMP_GATE':
traits.append('JUMP')
if self.type == 'GAS_GIANT':
traits.append('GAS')
if 'SHIPYARD' in self.traits:
traits.append('SHIPYARD')
if 'MARKETPLACE' in self.traits:
traits.append('MARKET')
if self.type == 'ASTEROID':
if 'COMMON_METAL_DEPOSITS' in self.traits:
traits.append('METAL')
if 'PRECIOUS_METAL_DEPOSITS' in self.traits:
traits.append('GOLD')
if 'MINERAL_DEPOSITS' in self.traits:
traits.append('MINS')
if 'STRIPPED' in self.traits:
traits.append('STRIPPED')
return traits

View File

@ -63,6 +63,7 @@ class ChunkHeader:
class Store: class Store:
def __init__(self, data_file, verbose=False): def __init__(self, data_file, verbose=False):
self.init_models() self.init_models()
self.data_dir = os.path.dirname(data_file)
self.fil = open_file(data_file) self.fil = open_file(data_file)
self.data = {m: {} for m in self.models} self.data = {m: {} for m in self.models}
self.system_members = {} self.system_members = {}
@ -102,8 +103,7 @@ class Store:
buf = BytesIO(data) buf = BytesIO(data)
p = StoreUnpickler(buf, self) p = StoreUnpickler(buf, self)
obj = p.load() obj = p.load()
obj.file_offset = offset obj._file_offset = offset
obj.disable_dirty = False
self.hold(obj) self.hold(obj)
def load(self): def load(self):
@ -143,27 +143,31 @@ class Store:
h = ChunkHeader() h = ChunkHeader()
h.size = sz h.size = sz
h.used = used h.used = used
h.offset = self.fil.tell() h.offset = offset
h.write(self.fil) h.write(self.fil)
return offset, h return offset, h
def purge(self, obj): def purge(self, obj):
if obj.file_offset is None: if obj._file_offset is None:
return return
self.fil.seek(obj.file_offset) self.fil.seek(obj._file_offset)
hdr = ChunkHeader.parse(self.fil) hdr = ChunkHeader.parse(self.fil)
hdr.in_use = False hdr.in_use = False
self.fil.seek(obj.file_offset) self.fil.seek(obj._file_offset)
hdr.write(self.fil) hdr.write(self.fil)
obj.file_offset = None if type(obj) in self.data and obj.symbol in self.data[type(obj)]:
del self.data[type(obj)][obj.symbol]
if obj in self.dirty_objects:
self.dirty_objects.remove(obj)
obj._file_offset = None
def store(self, obj): def store(self, obj):
data = self.dump_object(obj) data = self.dump_object(obj)
osize = len(data) osize = len(data)
# is there an existing chunk for this obj? # is there an existing chunk for this obj?
if obj.file_offset is not None: if obj._file_offset is not None:
# read chunk hdr # read chunk hdr
self.fil.seek(obj.file_offset) self.fil.seek(obj._file_offset)
hdr = ChunkHeader.parse(self.fil) hdr = ChunkHeader.parse(self.fil)
csize = hdr.size csize = hdr.size
# if the chunk is too small # if the chunk is too small
@ -171,15 +175,15 @@ class Store:
# free the chunk # free the chunk
hdr.in_use = False hdr.in_use = False
# force a new chunk # force a new chunk
obj.file_offset = None obj._file_offset = None
else: else:
# if it is big enough, update the used field # if it is big enough, update the used field
hdr.used = osize hdr.used = osize
self.fil.seek(hdr.offset) self.fil.seek(hdr.offset)
hdr.write(self.fil) hdr.write(self.fil)
if obj.file_offset is None: if obj._file_offset is None:
obj.file_offset, hdr = self.allocate_chunk(osize) obj._file_offset, hdr = self.allocate_chunk(osize)
# print(type(obj).__name__, hdr) # print(type(obj).__name__, hdr)
self.fil.write(data) self.fil.write(data)
slack = b'\x00' * (hdr.size - hdr.used) slack = b'\x00' * (hdr.size - hdr.used)
@ -294,14 +298,17 @@ class Store:
self.fil.flush() self.fil.flush()
self.dirty_objects = set() self.dirty_objects = set()
dur = time() - start_time dur = time() - start_time
# print(f'flush done {it} items {dur:.2f}') self.p(f'flush done {it} items {dur:.2f}')
def defrag(self): def defrag(self):
nm = self.fil.name nm = self.fil.name
self.fil.close() self.fil.close()
bakfile = nm+'.bak'
if os.path.isfile(bakfile):
os.remove(bakfile)
os.rename(nm, nm + '.bak') os.rename(nm, nm + '.bak')
self.fil = open(nm, 'ab+') self.fil = open_file(nm)
for t in self.data: for t in self.data:
for o in self.all(t): for o in self.all(t):
o.file_offset = None o._file_offset = None
self.store(o) self.store(o)

58
nullptr/store_analyzer.py Normal file
View File

@ -0,0 +1,58 @@
from nullptr.store import CHUNK_MAGIC, ChunkHeader, StoreUnpickler
from hexdump import hexdump
from io import BytesIO
class FakeStore:
def get(self, typ, sym, create=False):
return None
class StoreAnalyzer:
def __init__(self, verbose=False):
self.verbose = verbose
def load_obj(self, f, sz):
buf = BytesIO(f.read(sz))
p = StoreUnpickler(buf, FakeStore())
obj = p.load()
return obj
print(obj.symbol, type(obj).__name__)
def run(self, f):
lastpos = 0
pos = 0
objs = {}
result = True
f.seek(0)
while True:
lastpos = pos
pos = f.tell()
m = f.read(8)
if len(m) < 8:
break
if m != CHUNK_MAGIC:
print(f'missing magic at {pos}')
result = False
self.investigate(f, lastpos)
break
f.seek(-8, 1)
h = ChunkHeader.parse(f)
if self.verbose:
print(h, pos)
if h.in_use:
obj = self.load_obj(f, h.used)
kobj = obj.symbol, type(obj).__name__
if kobj in objs:
print(f'Double object {kobj} prev {objs[kobj]} latest {h}')
result = False
objs[kobj] = h
else:
f.seek(h.used, 1)
f.seek(h.size - h.used, 1)
return result
def investigate(self, f, lastpos):
print(f'dumping 1024 bytes from {lastpos}')
f.seek(lastpos, 0)
d = f.read(1024)
hexdump(d)
print(d.index(CHUNK_MAGIC))

View File

@ -1,7 +1,10 @@
import unittest import unittest
import tempfile import tempfile
from nullptr.store import Store from nullptr.store import Store, ChunkHeader
from nullptr.models import Base from nullptr.models import Base
from io import BytesIO
import os
from nullptr.store_analyzer import StoreAnalyzer
class Dummy(Base): class Dummy(Base):
def define(self): def define(self):
@ -16,7 +19,7 @@ class Dummy(Base):
return 'dum' return 'dum'
def f(self, detail=1): def f(self, detail=1):
r = super().f(detail) r = super().f(detail) + '.' + self.ext()
if detail >2: if detail >2:
r += f' c:{self.count}' r += f' c:{self.count}'
return r return r
@ -50,10 +53,19 @@ class TestStore(unittest.TestCase):
dum2 = self.s.get(Dummy, "7",create=True) dum2 = self.s.get(Dummy, "7",create=True)
self.reopen() self.reopen()
dum = self.s.get(Dummy, "5") dum = self.s.get(Dummy, "5")
old_off = dum._file_offset
self.assertTrue(old_off is not None)
dum.data = "A" * 1000 dum.data = "A" * 1000
dum.count = 1337 dum.count = 1337
self.s.flush()
new_off = dum._file_offset
self.assertTrue(new_off is not None)
self.assertNotEqual(old_off, new_off)
self.reopen() self.reopen()
dum = self.s.get(Dummy, "5") dum = self.s.get(Dummy, "5")
newer_off = dum._file_offset
self.assertTrue(newer_off is not None)
self.assertEqual(new_off, newer_off)
self.assertEqual(1337, dum.count) self.assertEqual(1337, dum.count)
def test_purge(self): def test_purge(self):
@ -64,6 +76,8 @@ class TestStore(unittest.TestCase):
self.s.flush() self.s.flush()
self.s.purge(dum) self.s.purge(dum)
self.reopen() self.reopen()
dum = self.s.get(Dummy, "5")
self.assertIsNone(dum)
dum2 = self.s.get(Dummy, "7") dum2 = self.s.get(Dummy, "7")
self.assertEqual(1337, dum2.count) self.assertEqual(1337, dum2.count)
@ -97,5 +111,60 @@ class TestStore(unittest.TestCase):
self.assertIsNone(dum2) self.assertIsNone(dum2)
dum3 = self.s.get(Dummy, "9") dum3 = self.s.get(Dummy, "9")
self.assertEqual(1338, dum3.count) self.assertEqual(1338, dum3.count)
def test_dont_relocate(self):
dum = self.s.get(Dummy, "5", create=True)
dum.data = "A"
self.s.flush()
old_off = dum._file_offset
self.reopen()
dum2 = self.s.get(Dummy, "5")
dum2.data = "BCDE"
self.s.flush()
new_off = dum._file_offset
self.assertEqual(old_off, new_off)
def test_chunk_header(self):
a = ChunkHeader()
a.size = 123
a.used = 122
a.in_use = True
b = BytesIO()
a.write(b)
b.seek(0)
c = ChunkHeader.parse(b)
self.assertEqual(c.size, a.size)
self.assertEqual(c.used, a.used)
self.assertEqual(c.in_use, True)
c.in_use = False
b.seek(0)
c.write(b)
b.seek(0)
d = ChunkHeader.parse(b)
self.assertEqual(d.size, a.size)
self.assertEqual(d.used, a.used)
self.assertEqual(d.in_use, False)
def test_mass(self):
num = 50
for i in range(num):
dum = self.s.get(Dummy, str(i), create=True)
dum.data = str(i)
dum.count = 0
self.reopen()
sz = os.stat(self.store_file.name).st_size
for j in range(50):
for i in range(num):
dum = self.s.get(Dummy, str(i))
# this works because j is max 49, and the slack is 64
# so no growing is needed
self.assertEqual(dum.data, "B" * j + str(i))
self.assertEqual(dum.count, j)
dum.data = "B" * (j+1) + str(i)
dum.count += 1
self.reopen()
sz2 = os.stat(self.store_file.name).st_size
self.assertEqual(sz, sz2)
an = StoreAnalyzer().run(self.store_file)
self.assertTrue(an)

View File

@ -1,2 +1,3 @@
requests requests
readline readline
hexdump