diff --git a/main.py b/main.py index e0b2cc1..5990b98 100644 --- a/main.py +++ b/main.py @@ -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() + if args.analyze: + 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__': 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) diff --git a/nullptr/analyzer.py b/nullptr/analyzer.py index 017ddc3..4b36a67 100644 --- a/nullptr/analyzer.py +++ b/nullptr/analyzer.py @@ -7,6 +7,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: @@ -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)) - \ No newline at end of file diff --git a/nullptr/api.py b/nullptr/api.py index 2a64ba8..7ceda25 100644 --- a/nullptr/api.py +++ b/nullptr/api.py @@ -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: diff --git a/nullptr/commander.py b/nullptr/commander.py index 84a4b25..c23faf9 100644 --- a/nullptr/commander.py +++ b/nullptr/commander.py @@ -1,8 +1,8 @@ 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.models import * from nullptr.api import Api from .util import * from time import sleep, time @@ -11,7 +11,7 @@ from nullptr.central_command import CentralCommand import readline import os from copy import copy - + class CommandError(Exception): pass @@ -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: @@ -58,7 +62,13 @@ class Commander(CommandLine): if obj is None: 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,203 +432,24 @@ 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 @@ -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() @@ -455,27 +476,44 @@ class Commander(CommandLine): if speed not in speeds: print('please choose from:', speeds) 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=''): - 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_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) + + ######## 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_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 system = self.ship.location.system @@ -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) diff --git a/nullptr/models/base.py b/nullptr/models/base.py index 892bb40..db92485 100644 --- a/nullptr/models/base.py +++ b/nullptr/models/base.py @@ -22,13 +22,20 @@ 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): raise NotImplementedError('no ext') @@ -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__ diff --git a/nullptr/models/ship.py b/nullptr/models/ship.py index 82cf99c..1c7bd3a 100644 --- a/nullptr/models/ship.py +++ b/nullptr/models/ship.py @@ -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,7 +19,19 @@ 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): return 'shp' @@ -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 diff --git a/nullptr/models/waypoint.py b/nullptr/models/waypoint.py index d4e3d9f..b336a5a 100644 --- a/nullptr/models/waypoint.py +++ b/nullptr/models/waypoint.py @@ -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 + \ No newline at end of file diff --git a/nullptr/store.py b/nullptr/store.py index 51788c5..c0c1cd7 100644 --- a/nullptr/store.py +++ b/nullptr/store.py @@ -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) diff --git a/nullptr/store_analyzer.py b/nullptr/store_analyzer.py new file mode 100644 index 0000000..aa4feb4 --- /dev/null +++ b/nullptr/store_analyzer.py @@ -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)) \ No newline at end of file diff --git a/nullptr/test_store.py b/nullptr/test_store.py index a6d732f..855286b 100644 --- a/nullptr/test_store.py +++ b/nullptr/test_store.py @@ -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) @@ -97,5 +111,60 @@ class TestStore(unittest.TestCase): self.assertIsNone(dum2) 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) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 1bb406c..d4602fa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ requests readline +hexdump