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

View File

@ -8,6 +8,14 @@ from copy import copy
class AnalyzerException(Exception):
pass
def path_dist(m):
t = 0
o = Point(0,0)
for w in m:
t +=w.distance(o)
o = w
return t
@dataclass
class Point:
x: int
@ -70,7 +78,6 @@ class Analyzer:
possibles = sorted(candidates, key=lambda m: m[2])
possibles = possibles[:10]
results = []
print(len(possibles))
for typ,m,d in possibles:
system = m.waypoint.system
p = self.find_path(origin, system)
@ -101,7 +108,8 @@ class Analyzer:
path = []
mkts = [m.waypoint for m in self.store.all_members(orig.system, Marketplace)]
cur = orig
if orig == to:
return []
while cur != to:
best = cur
bestdist = cur.distance(to)
@ -156,30 +164,25 @@ class Analyzer:
def find_trade(self, 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
for resource, markets in prices.items():
if resource in occupied_resources:
continue
source = sorted(markets, key=lambda x: x['buy'])[0]
dest = sorted(markets, key=lambda x: x['sell'])[-1]
margin = dest['sell'] -source['buy']
if margin < 0:
continue
dist = source['wp'].distance(dest['wp'])
dist = max(dist, 0.0001)
score = margin / dist
if margin < 0:
continue
o = TradeOption(resource, source['wp'], dest['wp'], margin, dist, score)
if best is None or best.score < o.score:
best = o
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):
data = {'waypointSymbol': str(wp)}
response = self.request('post', f'my/ships/{ship}/navigate', data)
ship.log(f'nav to {wp}')
ship.update(response)
def dock(self, ship):
@ -193,6 +194,7 @@ class Api:
def flight_mode(self, ship, mode):
data = {'flightMode': mode}
data = self.request('patch', f'my/ships/{ship}/nav', data)
ship.update(data)
return data
def jump(self, ship, waypoint):
@ -243,6 +245,7 @@ class Api:
'units': units
}
data = self.request('post', f'my/ships/{ship}/sell', data)
ship.log(f'sell {units} of {typ}')
if 'cargo' in data:
ship.update(data)
if 'agent' in data:
@ -255,6 +258,7 @@ class Api:
'units': amt
}
data = self.request('post', f'my/ships/{ship}/purchase', data)
ship.log(f'buy {amt} of {typ} at {ship.location}')
if 'cargo' in data:
ship.update(data)
if 'agent' in data:
@ -271,6 +275,7 @@ class Api:
'units': units
}
data = self.request('post', f'my/ships/{ship.symbol}/jettison', data)
ship.log(f'drop {units} of {typ}')
if 'cargo' in data:
ship.update(data)
if 'agent' in data:

View File

@ -1,6 +1,6 @@
from nullptr.command_line import CommandLine
from nullptr.store import Store
from nullptr.analyzer import Analyzer, Point
from nullptr.analyzer import Analyzer, Point, path_dist
import argparse
from nullptr.models import *
from nullptr.api import Api
@ -33,23 +33,27 @@ class Commander(CommandLine):
self.stop_auto= False
super().__init__()
######## INFRA #########
def handle_eof(self):
self.store.close()
readline.write_history_file(self.hist_file)
print("Goodbye!")
def do_pp(self):
pprint(self.api.last_result)
def prompt(self):
if self.ship:
return f'{self.ship.symbol}> '
else:
return '> '
def has_ship(self):
if self.ship is not None:
return True
else:
print('set a ship')
def after_cmd(self):
self.store.flush()
def do_auto(self):
self.centcom.run_interactive()
######## Resolvers #########
def ask_obj(self, typ, prompt):
obj = None
while obj is None:
@ -59,6 +63,12 @@ class Commander(CommandLine):
print('not found')
return obj
def has_ship(self):
if self.ship is not None:
return True
else:
print('set a ship')
def select_agent(self):
agents = self.store.all(Agent)
agent = next(agents, None)
@ -66,6 +76,27 @@ class Commander(CommandLine):
agent = self.agent_setup()
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):
symbol = input('agent name: ')
agent = self.store.get(Agent, symbol, create=True)
@ -90,38 +121,186 @@ class Commander(CommandLine):
self.store.flush()
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 do_token(self):
print(self.agent.token)
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
def after_cmd(self):
self.store.flush()
def do_register(self, faction):
self.api.register(faction.upper())
pprint(self.api.agent)
######## Fleet #########
def do_info(self, arg=''):
if arg.startswith('r'):
self.api.info()
pprint(self.agent, 100)
def do_auto(self):
self.centcom.run_interactive()
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_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):
print(f'mission: {self.ship.mission} ({self.ship.mission_status})')
pprint(self.ship.mission_state)
@ -155,11 +334,45 @@ class Commander(CommandLine):
if not self.has_ship(): return
self.centcom.set_mission_param(self.ship, nm, val)
######## Contracts #########
def active_contract(self):
for c in self.store.all('Contract'):
if c.accepted and not c.fulfilled: return c
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):
if not self.has_ship(): return
site = self.ship.location
@ -219,204 +432,25 @@ class Commander(CommandLine):
self.centcom.set_mission_param(self.ship, 'hops', markets)
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):
if not self.has_ship(): return
system = self.ship.location.system
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)
print("post", self.totaldist(m))
print("post", path_dist(m))
hops = [w.symbol for w in m]
self.centcom.init_mission(self.ship, 'probe')
self.centcom.set_mission_param(self.ship, 'hops', hops)
self.print_mission()
######## Travel #########
def do_travel(self, dest):
dest = self.resolve('Waypoint', dest)
self.centcom.init_mission(self.ship, 'travel')
self.centcom.set_mission_param(self.ship, 'dest', dest)
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):
if not self.has_ship(): return
system = self.ship.location.system
@ -435,19 +469,6 @@ class Commander(CommandLine):
self.api.orbit(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):
if not self.has_ship(): return
speed = speed.upper()
@ -456,25 +477,42 @@ class Commander(CommandLine):
print('please choose from:', speeds)
self.api.flight_mode(self.ship, speed)
def do_refuel(self, source='market'):
def do_jump(self, waypoint_str):
if not self.has_ship(): return
from_cargo = source != 'market'
r = self.api.refuel(self.ship, from_cargo=from_cargo)
pprint(r)
w = self.resolve('Waypoint', waypoint_str)
self.api.jump(self.ship, w)
pprint(self.ship)
def do_accept(self, c):
contract = self.resolve('Contract', c)
r = self.api.accept_contract(contract)
pprint(r)
######## Analysis #########
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_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_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):
if not self.has_ship(): return
@ -484,68 +522,3 @@ class Commander(CommandLine):
pprint(prices[resource.upper()])
else:
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,12 +22,19 @@ class Base:
identifier = 'symbol'
def __init__(self, symbol, store):
self.disable_dirty = True
self.file_offset = None
self._disable_dirty = True
self._file_offset = None
self.store = store
self.symbol = symbol
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
def ext(cls):
@ -73,7 +80,7 @@ class Base:
setattr(self, attr, lst)
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)
if issubclass(type(value), Base):
value = Reference.create(value)
@ -91,11 +98,6 @@ class Base:
def is_expired(self):
return False
def load(self, d):
self.disable_dirty = True
self.__dict__.update(d)
self.disable_dirty = False
def type(self):
return self.__class__.__name__

View File

@ -2,6 +2,7 @@ from .base import Base
from time import time
from nullptr.util import *
from nullptr.models import Waypoint
import os
class Ship(Base):
def define(self):
@ -18,6 +19,18 @@ class Ship(Base):
self.mission:str = None
self.mission_status:str = 'init'
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
def ext(self):
@ -30,6 +43,8 @@ class Ship(Base):
def update(self, d):
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)
self.seta('location', d, 'nav.waypointSymbol', interp=getter)
self.seta('cargo_capacity', d, 'cargo.capacity')
@ -104,16 +119,45 @@ class Ship(Base):
self.update_timers()
arrival = int(self.arrival - time())
cooldown = int(self.cooldown - time())
r = self.symbol
if detail > 1:
if self.role is not None:
r += f' {self.role}'
r += ' ' + self.status
r += f' [{self.fuel_current}/{self.fuel_capacity}]'
r += ' ' + str(self.location)
role = self.role
if role is None:
role = 'none'
mstatus = self.mission_status
if mstatus == 'error':
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():
r += f' [A: {arrival}]'
if self.is_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

View File

@ -32,3 +32,26 @@ class Waypoint(Base):
def ext(self):
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:
def __init__(self, data_file, verbose=False):
self.init_models()
self.data_dir = os.path.dirname(data_file)
self.fil = open_file(data_file)
self.data = {m: {} for m in self.models}
self.system_members = {}
@ -102,8 +103,7 @@ class Store:
buf = BytesIO(data)
p = StoreUnpickler(buf, self)
obj = p.load()
obj.file_offset = offset
obj.disable_dirty = False
obj._file_offset = offset
self.hold(obj)
def load(self):
@ -143,27 +143,31 @@ class Store:
h = ChunkHeader()
h.size = sz
h.used = used
h.offset = self.fil.tell()
h.offset = offset
h.write(self.fil)
return offset, h
def purge(self, obj):
if obj.file_offset is None:
if obj._file_offset is None:
return
self.fil.seek(obj.file_offset)
self.fil.seek(obj._file_offset)
hdr = ChunkHeader.parse(self.fil)
hdr.in_use = False
self.fil.seek(obj.file_offset)
self.fil.seek(obj._file_offset)
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):
data = self.dump_object(obj)
osize = len(data)
# 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
self.fil.seek(obj.file_offset)
self.fil.seek(obj._file_offset)
hdr = ChunkHeader.parse(self.fil)
csize = hdr.size
# if the chunk is too small
@ -171,15 +175,15 @@ class Store:
# free the chunk
hdr.in_use = False
# force a new chunk
obj.file_offset = None
obj._file_offset = None
else:
# if it is big enough, update the used field
hdr.used = osize
self.fil.seek(hdr.offset)
hdr.write(self.fil)
if obj.file_offset is None:
obj.file_offset, hdr = self.allocate_chunk(osize)
if obj._file_offset is None:
obj._file_offset, hdr = self.allocate_chunk(osize)
# print(type(obj).__name__, hdr)
self.fil.write(data)
slack = b'\x00' * (hdr.size - hdr.used)
@ -294,14 +298,17 @@ class Store:
self.fil.flush()
self.dirty_objects = set()
dur = time() - start_time
# print(f'flush done {it} items {dur:.2f}')
self.p(f'flush done {it} items {dur:.2f}')
def defrag(self):
nm = self.fil.name
self.fil.close()
bakfile = nm+'.bak'
if os.path.isfile(bakfile):
os.remove(bakfile)
os.rename(nm, nm + '.bak')
self.fil = open(nm, 'ab+')
self.fil = open_file(nm)
for t in self.data:
for o in self.all(t):
o.file_offset = None
o._file_offset = None
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 tempfile
from nullptr.store import Store
from nullptr.store import Store, ChunkHeader
from nullptr.models import Base
from io import BytesIO
import os
from nullptr.store_analyzer import StoreAnalyzer
class Dummy(Base):
def define(self):
@ -16,7 +19,7 @@ class Dummy(Base):
return 'dum'
def f(self, detail=1):
r = super().f(detail)
r = super().f(detail) + '.' + self.ext()
if detail >2:
r += f' c:{self.count}'
return r
@ -50,10 +53,19 @@ class TestStore(unittest.TestCase):
dum2 = self.s.get(Dummy, "7",create=True)
self.reopen()
dum = self.s.get(Dummy, "5")
old_off = dum._file_offset
self.assertTrue(old_off is not None)
dum.data = "A" * 1000
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()
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)
def test_purge(self):
@ -64,6 +76,8 @@ class TestStore(unittest.TestCase):
self.s.flush()
self.s.purge(dum)
self.reopen()
dum = self.s.get(Dummy, "5")
self.assertIsNone(dum)
dum2 = self.s.get(Dummy, "7")
self.assertEqual(1337, dum2.count)
@ -98,4 +112,59 @@ class TestStore(unittest.TestCase):
dum3 = self.s.get(Dummy, "9")
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
readline
hexdump