Compare commits

..

36 Commits

Author SHA1 Message Date
Richard
4daf8cfb7d fix the generals problems 2024-02-11 18:24:16 +01:00
Richard
53867a3257 general leads the startup 2024-02-11 14:37:46 +01:00
Richard
cf930fe24b working on startup 2024-02-10 19:29:11 +01:00
Richard
74ce884b05 major command staff restructure
Heads had to roll
2024-02-09 15:52:30 +01:00
Richard
fb3b6162fc shipyards 2024-02-03 21:20:04 +01:00
Richard
02f206d078 small improvementss 2024-02-01 18:51:27 +01:00
Richard
b5b736df63 fix marketplace history 2024-01-27 20:47:12 +01:00
Richard
5d47efdbda mission step priorities, fixed the store again 2024-01-27 15:05:33 +01:00
Richard
f913d23c06 mining working again 2024-01-25 19:57:49 +01:00
Richard
d8eb1c4954 crews 2024-01-24 19:03:57 +01:00
Richard
b0ef68a721 rewrote hauling and highscores 2024-01-21 20:21:38 +01:00
Richard
3f7a416fdc siphoning and hauling 2024-01-20 20:33:50 +01:00
Richard
592c628a46 trading debug and ship logs 2024-01-16 19:13:10 +01:00
Richard
560ac056ff trade routes instead of resources 2024-01-15 19:39:08 +01:00
Richard
7d92a45d12 store fixes and probe role 2024-01-13 21:42:49 +01:00
Richard
188ef320cc historical marketprices
api.py
2024-01-13 11:27:32 +01:00
Richard
08ab3f0999 obj function 2024-01-09 20:39:11 +01:00
Richard
237dcc8c14 progress
Commander cleanup
First impl of ship logs
Ship display improved
Store debugged
2024-01-09 20:07:27 +01:00
Richard
2181583843 improving haulers 2024-01-06 07:17:53 +01:00
Richard
524ba45639 8 byte magic and store docs 2024-01-04 22:11:23 +01:00
Richard
1b7a528655 haulin goods 2024-01-04 21:34:31 +01:00
Richard
b47fa44cb0 mission and cli improvements 2024-01-02 06:35:26 +01:00
Richard
6118772a63 fix market display 2023-12-30 17:23:37 +01:00
Richard
a287897da9 implementing api changes since 5 months ago 2023-12-28 19:49:00 +01:00
Richard
1ba10260c0 up arrow 2023-12-25 08:05:18 +01:00
Richard
bc8d565fc3 test and fix the store 2023-12-25 07:54:19 +01:00
Richard
7038e8f852 exit gracefully on ctrlc 2023-12-25 07:53:38 +01:00
Richard
7fd6b6ab51 fix api extract endpoint 2023-12-25 07:51:45 +01:00
Richard Bronkhorst
74a9c391e9 Update central_command.py 2023-07-18 12:44:53 +02:00
Richard Bronkhorst
2716fbf1aa Update atlas_builder.py, central_command.py and one other file 2023-07-18 12:43:31 +02:00
Richard Bronkhorst
71f8eb9ed8 Update probe.py 2023-07-16 21:57:39 +02:00
Richard Bronkhorst
6ddddd6fb1 Update atlas_builder.py 2023-07-16 21:06:25 +02:00
Richard Bronkhorst
e0f73f837b Update commander.py 2023-07-16 21:01:00 +02:00
Richard Bronkhorst
1f4a1a48de Update api.py, commander.py and one other file 2023-07-16 20:50:30 +02:00
Richard Bronkhorst
e5c384caa9 Rewrite atlas builder to be re-entrant. Rolled into automode. 2023-07-16 18:48:45 +02:00
Richard Bronkhorst
f644027750 Expiry and defragmentation 2023-07-14 12:33:31 +02:00
47 changed files with 2373 additions and 727 deletions

View File

@ -8,5 +8,6 @@ RUN pip3 install -r requirements.txt
ADD --chown=user . /app ADD --chown=user . /app
RUN chmod +x /app/main.py RUN chmod +x /app/main.py
VOLUME /data VOLUME /data
ENTRYPOINT [ "python3", "/app/main.py"] #ENTRYPOINT bash
CMD ["-s", "/data/store.npt"] RUN echo "python3 /app/main.py -d /data" > ~/.bash_history
CMD ["/bin/sh", "-c", "python3 /app/main.py -d /data ; bash -i"]

17
main.py Executable file → Normal file
View File

@ -1,16 +1,23 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import argparse import argparse
from nullptr.commander import Commander from nullptr.commander import Commander
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):
c = Commander(args.store_file) if not os.path.isdir(args.data_dir):
os.makedirs(args.data_dir )
if args.analyze:
a = StoreAnalyzer(verbose=True)
a.run(args.analyze)
else:
c = Commander(args.data_dir, auto=args.auto)
c.run() c.run()
# X1-AG74-41076A
# X1-KS52-51429E
if __name__ == '__main__': if __name__ == '__main__':
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('-s', '--store-file', default='data/store.npt') parser.add_argument('-d', '--data-dir', default='data')
parser.add_argument('--analyze', type=argparse.FileType('rb'))
parser.add_argument('-a', '--auto', action='store_true')
args = parser.parse_args() args = parser.parse_args()
main(args) main(args)

View File

@ -3,6 +3,34 @@ from nullptr.models.jumpgate import Jumpgate
from nullptr.models.system import System from nullptr.models.system import System
from nullptr.models.waypoint import Waypoint from nullptr.models.waypoint import Waypoint
from dataclasses import dataclass from dataclasses import dataclass
from nullptr.util import pprint
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
y: int
@dataclass
class TradeOption:
resource: str
source: Waypoint
dest: Waypoint
buy: int
margin: int
dist: int
score: float
@dataclass @dataclass
class SearchNode: class SearchNode:
@ -24,12 +52,9 @@ class SearchNode:
def __repr__(self): def __repr__(self):
return self.system.symbol return self.system.symbol
class Analyzer:
def __init__(self, store):
self.store = store
def find_markets(self, resource, sellbuy): def find_markets(c, resource, sellbuy):
for m in self.store.all(Marketplace): for m in c.store.all(Marketplace):
if 'sell' in sellbuy and resource in m.imports: if 'sell' in sellbuy and resource in m.imports:
yield ('sell', m) yield ('sell', m)
@ -39,10 +64,10 @@ class Analyzer:
elif 'exchange' in sellbuy and resource in m.exchange: elif 'exchange' in sellbuy and resource in m.exchange:
yield ('exchange', m) yield ('exchange', m)
def find_closest_markets(self, resource, sellbuy, location): def find_closest_markets(c, resource, sellbuy, location):
if type(location) == str: if type(location) == str:
location = self.store.get(Waypoint, location) location = c.store.get(Waypoint, location)
mkts = self.find_markets(resource, sellbuy) mkts = find_markets(resource, sellbuy)
candidates = [] candidates = []
origin = location.system origin = location.system
for typ, m in mkts: for typ, m in mkts:
@ -54,36 +79,157 @@ class Analyzer:
results = [] results = []
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 = find_jump_path(origin, system)
if p is None: continue if p is None: continue
results.append((typ,m,d,len(p))) results.append((typ,m,d,len(p)))
return results return results
def solve_tsp(self, waypoints): def solve_tsp(c, waypoints):
# todo actually try to solve it wps = copy(waypoints)
return waypoints path = []
cur = Point(0,0)
while len(wps) > 0:
closest = wps[0]
for w in wps:
if w.distance(cur) < closest.distance(cur):
closest = w
cur = closest
path.append(closest)
wps.remove(closest)
return path
def get_jumpgate(self, system): def get_jumpgate(c, system):
gates = self.store.all_members(system, Jumpgate) gates = c.store.all_members(system, Jumpgate)
return next(gates, None) return next(gates, None)
def find_path(self, orig, to, depth=100, seen=None): # dijkstra shmijkstra
def find_nav_path(c, orig, to, ran):
path = []
mkts = [m.waypoint for m in c.store.all_members(orig.system, Marketplace)]
cur = orig
if orig == to:
return []
while cur != to:
best = cur
bestdist = cur.distance(to)
if bestdist < ran:
path.append(to)
break
for m in mkts:
dist = m.distance(to)
if dist < bestdist and cur.distance(m) < ran:
best = m
bestdist = dist
if best == cur:
raise AnalyzerException(f'no path to {to}')
cur = best
path.append(cur)
return path
def find_jump_path(c, orig, to, depth=100, seen=None):
if depth < 1: return None if depth < 1: return None
if seen is None: if seen is None:
seen = set() seen = set()
if type(orig) == System: if type(orig) == System:
orig = set([SearchNode(orig,None)]) orig = set([SearchNode(orig,None)])
result = [n for n in orig if n.system==to] result = [n for n in orig if n==to]
if len(result) > 0: if len(result) > 0:
return result[0].path() return result[0].path()
dest = set() dest = set()
for o in orig: for o in orig:
jg = self.get_jumpgate(o.system) jg = get_jumpgate(o)
if jg is None: continue if jg is None: continue
for s in jg.systems: for s in jg.connections:
if s in seen: continue if s in seen: continue
seen.add(s) seen.add(s)
dest.add(SearchNode(s, o)) dest.add(SearchNode(s, o))
if len(dest) == 0: if len(dest) == 0:
return None return None
return self.find_path(dest, to, depth-1, seen) return find_jump_path(dest, to, depth-1, seen)
def prices(c, system):
prices = {}
for m in c.store.all_members(system, Marketplace):
for r, p in m.prices.items():
if not r in prices:
prices[r] = []
prices[r].append({
'wp': m.waypoint,
'buy': p.buy,
'sell': p.sell,
'volume': p.volume,
'category': m.rtype(r)
})
return prices
def find_trade(c, system):
max_traders = 3
pcs= prices(c, system)
occupied_routes = dict()
for s in c.store.all('Ship'):
if s.mission != 'trade':
continue
k = (s.mission_state['site'], s.mission_state['dest'])
if k in occupied_routes:
occupied_routes[k] += 1
else:
occupied_routes[k] = 1
best = None
for resource, markets in pcs.items():
source = sorted(markets, key=lambda x: x['buy'])[0]
dest = sorted(markets, key=lambda x: x['sell'])[-1]
swp = source['wp']
dwp = dest['wp']
margin = dest['sell'] -source['buy']
k = (swp.symbol,dwp.symbol)
if k in occupied_routes and occupied_routes[k] > max_traders:
continue
dist = swp.distance(dwp)
dist = max(dist, 0.0001)
score = margin / dist
if margin < 2:
continue
o = TradeOption(resource, swp, dwp, source['buy'], margin, dist, score)
if best is None or best.score < o.score:
best = o
return best
def find_deal(c, smkt, dmkt):
best_margin = 0
best_resource = None
for r, sp in smkt.prices.items():
if not r in dmkt.prices:
continue
dp = dmkt.prices[r]
margin = dp.sell - sp.buy
if margin > best_margin:
best_margin = margin
best_resource = r
return best_resource
def best_sell_market(c, system, r):
best_price = 0
best_market = None
for m in c.store.all_members(system, Marketplace):
if r not in m.prices: continue
price = m.prices[r].sell
if price > best_price:
best_price = price
best_market = m
return best_market
def find_gas(c, system):
m = [w for w in c.store.all_members(system, 'Waypoint') if w.type == 'GAS_GIANT']
if len(m)==0:
raise AnalyzerException('no gas giant found')
return m[0]
def find_metal(c, system):
m = [w for w in c.store.all_members(system, Waypoint) if 'COMMON_METAL_DEPOSITS' in w.traits]
if len(m) == 0:
return None
origin = Point(0,0)
m = sorted(m, key=lambda w: w.distance(origin))
return m[0]

View File

@ -4,9 +4,11 @@ from nullptr.models.waypoint import Waypoint
from nullptr.models.marketplace import Marketplace from nullptr.models.marketplace import Marketplace
from nullptr.models.jumpgate import Jumpgate from nullptr.models.jumpgate import Jumpgate
from nullptr.models.ship import Ship from nullptr.models.ship import Ship
from nullptr.models.shipyard import Shipyard
from .util import * from .util import *
from time import sleep from time import sleep, time
class ApiError(Exception):
class ApiError(AppError):
def __init__(self, msg, code): def __init__(self, msg, code):
super().__init__(msg) super().__init__(msg)
self.code = code self.code = code
@ -15,11 +17,11 @@ class ApiLimitError(Exception):
pass pass
class Api: class Api:
def __init__(self, store, agent): def __init__(self, c, agent):
self.agent = agent self.agent = agent
self.store = store self.store = c.store
self.requests_sent = 0 self.requests_sent = 0
self.meta = None self.last_meta = None
self.last_result = None self.last_result = None
self.root = 'https://api.spacetraders.io/v2/' self.root = 'https://api.spacetraders.io/v2/'
@ -30,9 +32,13 @@ class Api:
def request(self, method, path, data=None, need_token=True, params={}): def request(self, method, path, data=None, need_token=True, params={}):
try: try:
return self.request_once(method, path, data, need_token, params) start = time()
result = self.request_once(method, path, data, need_token, params)
dur = time() - start
# print(f'api {dur:.03}')
return result
except (ApiLimitError, requests.exceptions.Timeout): except (ApiLimitError, requests.exceptions.Timeout):
print('oops, hit the limit. take a break') # print('oops, hit the limit. take a break')
sleep(10) sleep(10)
return self.request_once(method, path, data, need_token, params) return self.request_once(method, path, data, need_token, params)
@ -61,6 +67,7 @@ class Api:
self.last_error = 0 self.last_error = 0
return result['data'] return result['data']
######## Account #########
def register(self, faction): def register(self, faction):
callsign = self.agent.symbol callsign = self.agent.symbol
data = { data = {
@ -72,11 +79,19 @@ class Api:
self.agent.update(mg(result, 'agent')) self.agent.update(mg(result, 'agent'))
self.agent.token = token self.agent.token = token
def status(self):
try:
self.request('get', '')
except ApiError:
pass
return self.last_result
def info(self): def info(self):
data = self.request('get', 'my/agent') data = self.request('get', 'my/agent')
self.agent.update(data) self.agent.update(data)
return self.agent return self.agent
######## Atlas #########
def list_systems(self, page=1): def list_systems(self, page=1):
data = self.request('get', 'systems', params={'page': page}) data = self.request('get', 'systems', params={'page': page})
#pprint(self.last_meta) #pprint(self.last_meta)
@ -87,6 +102,9 @@ class Api:
def list_waypoints(self, system): def list_waypoints(self, system):
data = self.request('get', f'systems/{system}/waypoints/') data = self.request('get', f'systems/{system}/waypoints/')
tp = total_pages(self.last_meta)
for p in range(tp):
data += self.request('get', f'systems/{system}/waypoints/', params={'page': p+1})
# pprint(data) # pprint(data)
return self.store.update_list(Waypoint, data) return self.store.update_list(Waypoint, data)
@ -100,10 +118,39 @@ class Api:
symbol = str(waypoint) symbol = str(waypoint)
return self.store.update(Jumpgate, data, symbol) return self.store.update(Jumpgate, data, symbol)
def shipyard(self, wp):
data = self.request('get', f'systems/{wp.system}/waypoints/{wp}/shipyard')
symbol = str(wp)
return self.store.update(Shipyard, data, symbol)
######## Fleet #########
def list_ships(self): def list_ships(self):
data = self.request('get', 'my/ships') data = self.request('get', 'my/ships')
tp = total_pages(self.last_meta)
for p in range(1, tp):
data += self.request('get', 'my/ships', params={'page': p+1})
return self.store.update_list(Ship, data) return self.store.update_list(Ship, data)
def refuel(self, ship, from_cargo=False):
fuel_need = ship.fuel_capacity - ship.fuel_current
fuel_avail = ship.get_cargo('FUEL') * 100
units = fuel_need
if from_cargo:
units = min(units, fuel_avail)
data = {'fromCargo': from_cargo, 'units': units }
data = self.request('post', f'my/ships/{ship}/refuel', data)
self.log_transaction(data)
if from_cargo:
boxes = ceil(float(units) / 100)
ship.take_cargo('FUEL', boxes)
if 'fuel' in data:
ship.update(data)
if 'agent' in data:
self.agent.update(data['agent'])
return data
######## Contract #########
def list_contracts(self): def list_contracts(self):
data = self.request('get', 'my/contracts') data = self.request('get', 'my/contracts')
return self.store.update_list('Contract', data) return self.store.update_list('Contract', data)
@ -114,6 +161,14 @@ class Api:
contract = self.store.update('Contract', data['contract']) contract = self.store.update('Contract', data['contract'])
return contract return contract
def accept_contract(self, contract):
data = self.request('post', f'my/contracts/{contract.symbol.lower()}/accept')
if 'contract' in data:
contract.update(data['contract'])
if 'agent' in data:
self.agent.update(data['agent'])
return contract
def deliver(self, ship, typ, contract): def deliver(self, ship, typ, contract):
units = ship.get_cargo(typ) units = ship.get_cargo(typ)
if units == 0: if units == 0:
@ -139,9 +194,11 @@ class Api:
self.agent.update(data['agent']) self.agent.update(data['agent'])
return contract return contract
######## Nav #########
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):
@ -154,29 +211,88 @@ class Api:
ship.update(data) ship.update(data)
return data return data
def refuel(self, ship): def flight_mode(self, ship, mode):
data = self.request('post', f'my/ships/{ship}/refuel') data = {'flightMode': mode}
if 'fuel' in data: data = self.request('patch', f'my/ships/{ship}/nav', data)
ship.update(data) ship.update({'nav':data})
if 'agent' in data:
self.agent.update(data['agent'])
return data return data
def accept_contract(self, contract): def jump(self, ship, waypoint):
data = self.request('post', f'my/contracts/{contract.symbol.lower()}/accept') if type(waypoint) == Waypoint:
if 'contract' in data: waypoint = waypoint.symbol
contract.update(data['contract']) data = {
if 'agent' in data: "waypointSymbol": waypoint
self.agent.update(data['agent']) }
return contract data = self.request('post', f'my/ships/{ship}/jump', data)
if 'nav' in data:
ship.update(data)
return ship
def sell(self, ship, typ): ######## Extraction #########
def siphon(self, ship):
data = self.request('post', f'my/ships/{ship}/siphon')
ship.update(data)
amt = mg(data, 'siphon.yield.units')
rec = mg(data, 'siphon.yield.symbol')
ship.log(f"siphoned {amt} {rec}")
ship.location.extracted += amt
return data['siphon']
def extract(self, ship, survey=None):
data = {}
url = f'my/ships/{ship}/extract'
if survey is not None:
data= survey.api_dict()
url += '/survey'
try:
data = self.request('post', url, data=data)
except ApiError as e:
if e.code in [ 4221, 4224]:
survey.exhausted = True
else:
raise e
ship.update(data)
amt = sg(data, 'extraction.yield.units', 0)
rec = sg(data, 'extraction.yield.symbol', 'nothing')
ship.log(f"extracted {amt} {rec}")
ship.location.extracted += amt
return data
def survey(self, ship):
data = self.request('post', f'my/ships/{ship}/survey')
ship.update(data)
result = self.store.update_list('Survey', mg(data, 'surveys'))
return result
######## Commerce #########
def transaction_cost(self, data):
if not 'transaction' in data: return 0
act = mg(data,'transaction.type')
minus = -1 if act == 'PURCHASE' else 1
units = mg(data, 'transaction.units')
ppu = mg(data, 'transaction.pricePerUnit')
return ppu * units * minus
def log_transaction(self, data):
if not 'transaction' in data: return
typ = mg(data, 'transaction.tradeSymbol')
ppu = mg(data, 'transaction.pricePerUnit')
shipsym = mg(data, 'transaction.shipSymbol')
ship = self.store.get('Ship', shipsym)
units = mg(data, 'transaction.units')
act = mg(data,'transaction.type')
ship.log(f'{act} {units} of {typ} for {ppu} at {ship.location}')
def sell(self, ship, typ,units=None):
if units is None:
units = ship.get_cargo(typ) units = ship.get_cargo(typ)
data = { data = {
'symbol': typ, 'symbol': typ,
'units': units 'units': units
} }
data = self.request('post', f'my/ships/{ship}/sell', data) data = self.request('post', f'my/ships/{ship}/sell', data)
self.log_transaction(data)
if 'cargo' in data: if 'cargo' in data:
ship.update(data) ship.update(data)
if 'agent' in data: if 'agent' in data:
@ -189,6 +305,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)
self.log_transaction(data)
if 'cargo' in data: if 'cargo' in data:
ship.update(data) ship.update(data)
if 'agent' in data: if 'agent' in data:
@ -205,12 +322,26 @@ 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:
self.agent.update(data['agent']) self.agent.update(data['agent'])
return data return data
def transfer(self, sship, dship, typ, amt):
data = {
'tradeSymbol': typ,
'units': amt,
'shipSymbol': dship.symbol
}
data = self.request('post', f'my/ships/{sship.symbol}/transfer', data)
sship.log(f'tra {amt} {typ} to {dship}')
dship.log(f'rec {amt} {typ} from {sship}', 10)
if 'cargo' in data:
sship.update(data)
dship.put_cargo(typ, amt)
def purchase(self, typ, wp): def purchase(self, typ, wp):
data = { data = {
'shipType': typ, 'shipType': typ,
@ -222,38 +353,3 @@ class Api:
if 'ship' in data: if 'ship' in data:
ship = self.store.update('Ship', data['ship']) ship = self.store.update('Ship', data['ship'])
return ship return ship
def jump(self, ship, system):
if type(system) == System:
system = system.symbol
data = {
"systemSymbol": system
}
data = self.request('post', f'my/ships/{ship}/jump', data)
if 'nav' in data:
ship.update(data)
return ship
def shipyard(self, wp):
return self.request('get', f'systems/{wp.system()}/waypoints/{wp}/shipyard')
def extract(self, ship, survey=None):
data = {}
if survey is not None:
data['survey'] = survey.api_dict()
try:
data = self.request('post', f'my/ships/{ship}/extract', data=data)
except ApiError as e:
if e.code in [ 4221, 4224]:
survey.exhausted = True
else:
raise e
ship.update(data)
return data
def survey(self, ship):
data = self.request('post', f'my/ships/{ship}/survey')
ship.update(data)
result = self.store.update_list('Survey', mg(data, 'surveys'))
return result

View File

@ -1,66 +1,74 @@
from time import sleep from time import sleep, time
from nullptr.util import * from nullptr.util import *
from threading import Thread from threading import Thread
from nullptr.models.atlas import Atlas
from functools import partial
from nullptr.models import System
class AtlasBuilder: class AtlasBuilder:
def __init__(self, store, api): def __init__(self, store, api):
self.store = store self.store = store
self.api = api self.api = api
self.stop_auto = False self.work = []
self.max_work = 100
self.unch_interval = 86400
self.atlas = self.store.get(Atlas, 'ATLAS', create=True)
def wait_for_stop(self): def find_work(self):
try: if not self.atlas.enabled:
input() return
except EOFError: first_page = self.atlas.total_pages == 0
pass pages_left = self.atlas.total_pages > self.atlas.seen_pages
self.stop_auto = True
print('stopping...')
def run(self, page=1): if first_page or pages_left:
print('universe mode. hit enter to stop') self.sched(self.get_systems)
t = Thread(target=self.wait_for_stop) return
t.daemon = True for s in self.store.all(System):
t.start() if len(self.work) > self.max_work:
self.all_systems(int(page))
print('manual mode')
def all_specials(self, waypoints):
for w in waypoints:
if self.stop_auto:
break break
if not s.uncharted: continue
if s.last_crawl > time() - self.unch_interval:
continue
self.sched(self.get_waypoints, s)
def do_work(self):
if len(self.work) == 0:
self.find_work()
if len(self.work) == 0:
return
work = self.work.pop()
work()
def get_systems(self):
page = 1
if self.atlas.seen_pages > 0:
page = self.atlas.seen_pages + 1
if page > self.atlas.total_pages:
return
# print('systems', page)
data = self.api.list_systems(page)
self.atlas.total_pages = total_pages(self.api.last_meta)
self.atlas.seen_pages = page
def get_waypoints(self, system):
wps = self.api.list_waypoints(system)
system.last_crawl = time()
system.uncharted = len([1 for w in wps if w.uncharted]) > 0
self.schedule_specials(wps)
def sched(self, fun, *args):
self.work.append(partial(fun, *args))
def schedule_specials(self, waypoints):
for w in waypoints:
if 'UNCHARTED' in w.traits: if 'UNCHARTED' in w.traits:
continue continue
if 'MARKETPLACE' in w.traits: if 'MARKETPLACE' in w.traits:
print(f'marketplace at {w}') #print(f'marketplace at {w}')
self.api.marketplace(w) self.sched(self.api.marketplace, w)
sleep(0.5)
if w.type == 'JUMP_GATE': if w.type == 'JUMP_GATE':
print(f'jumpgate at {w}') #print(f'jumpgate at {w}')
self.api.jumps(w) self.sched(self.api.jumps, w)
if 'SHIPYARD' in w.traits:
def all_waypoints(self, systems): self.sched(self.api.shipyard, w)
for s in systems:
if self.stop_auto:
break
r = self.api.list_waypoints(s)
self.all_specials(r)
sleep(0.5)
def all_systems(self, start_page):
self.stop_auto = False
data = self.api.list_systems(start_page)
pages = total_pages(self.api.last_meta)
print(f'{pages} pages of systems')
print(f'page {1}: {len(data)} results')
self.all_waypoints(data)
self.store.flush()
for p in range(start_page+1, pages+1):
if self.stop_auto:
break
data = self.api.list_systems(p)
print(f'page {p}: {len(data)} systems')
self.all_waypoints(data)
sleep(0.5)
self.store.flush()

View File

@ -1,29 +1,53 @@
from nullptr.store import Store from nullptr.store import Store
from nullptr.models.ship import Ship from nullptr.models.ship import Ship
from nullptr.missions import create_mission, get_mission_class from nullptr.missions import create_mission, get_mission_class
from random import choice from nullptr.models.waypoint import Waypoint
from time import sleep from random import choice, randrange
from time import sleep, time
from threading import Thread from threading import Thread
from nullptr.atlas_builder import AtlasBuilder
from nullptr.general import General
from nullptr.util import *
from nullptr.roles import assign_mission
class CentralCommandError(Exception): class CentralCommandError(AppError):
pass pass
class CentralCommand: class Captain:
def __init__(self, store, api): def __init__(self, context):
self.missions = {} self.missions = {}
self.stopping = False self.stopping = False
self.store = store self.store = context.store
self.api = api self.c = context
self.general = context.general
self.api = context.api
self.general = context.general
self.atlas_builder = AtlasBuilder(self.store, self.api)
def setup(self):
self.update_missions() self.update_missions()
def get_ready_missions(self): def get_ready_missions(self):
result = [] result = []
prio = 1
for ship, mission in self.missions.items(): for ship, mission in self.missions.items():
if mission.is_ready(): p = mission.is_ready()
if p == prio:
result.append(ship) result.append(ship)
elif p > prio:
prio = p
result = [ship]
return result return result
def single_step(self, ship):
if ship not in self.missions:
print('ship has no mission')
mission = self.missions[ship]
mission.step()
def tick(self): def tick(self):
self.general.tick()
self.update_missions()
missions = self.get_ready_missions() missions = self.get_ready_missions()
if len(missions) == 0: return False if len(missions) == 0: return False
ship = choice(missions) ship = choice(missions)
@ -39,7 +63,6 @@ class CentralCommand:
self.run() self.run()
print('manual mode') print('manual mode')
def wait_for_stop(self): def wait_for_stop(self):
try: try:
input() input()
@ -48,16 +71,26 @@ class CentralCommand:
self.stopping = True self.stopping = True
print('stopping...') print('stopping...')
def run(self): def run(self):
self.update_missions()
while not self.stopping: while not self.stopping:
# any new orders?
self.c.general.tick()
did_step = True did_step = True
request_counter = self.api.requests_sent request_counter = self.api.requests_sent
start = time()
while request_counter == self.api.requests_sent and did_step: while request_counter == self.api.requests_sent and did_step:
did_step = self.tick() did_step = self.tick()
if request_counter == self.api.requests_sent:
self.atlas_builder.do_work()
else:
pass # print('nowork')
self.store.flush() self.store.flush()
sleep(0.5) dur = time() - start
# print(f'step {dur:.03}')
zs = 0.5 - dur
if zs > 0:
sleep(zs)
self.stopping = False self.stopping = False
def stop(self): def stop(self):
@ -80,16 +113,22 @@ class CentralCommand:
return return
ship.set_mission_state(nm, parsed_val) ship.set_mission_state(nm, parsed_val)
def smipa(self,s,n,v):
self.set_mission_param(s,n,v)
def update_missions(self): def update_missions(self):
for s in self.store.all(Ship): for s in self.store.all(Ship):
if s.mission_status == 'done':
s.mission = None
if s.mission is None: if s.mission is None:
if s in self.missions: if s in self.missions:
self.stop_mission(s) self.stop_mission(s)
elif s not in self.missions: if s.mission is None:
assign_mission(self.c, s)
if s.mission is not None and s not in self.missions:
self.start_mission(s) self.start_mission(s)
if s in self.missions: if s in self.missions:
m = self.missions[s] m = self.missions[s]
m.next_step = max(s.cooldown, s.arrival)
def init_mission(self, s, mtyp): def init_mission(self, s, mtyp):
if mtyp == 'none': if mtyp == 'none':
@ -106,12 +145,20 @@ class CentralCommand:
s.mission_state = {k: v.default for k,v in mclass.params().items()} s.mission_state = {k: v.default for k,v in mclass.params().items()}
self.start_mission(s) self.start_mission(s)
def restart_mission(self, s, status='init'):
if s not in self.missions:
raise CentralCommandError("no mission assigned")
s.mission_status = status
def start_mission(self, s): def start_mission(self, s):
mtype = s.mission mtype = s.mission
m = create_mission(mtype, s, self.store, self.api) m = create_mission(mtype, s, self.c)
self.missions[s] = m self.missions[s] = m
m.status(s.mission_status)
return m return m
def stop_mission(self, s): def stop_mission(self, s):
if s in self.missions: if s in self.missions:
del self.missions[s] del self.missions[s]

View File

@ -3,6 +3,7 @@ import inspect
import sys import sys
import importlib import importlib
import logging import logging
from nullptr.util import AppError
def func_supports_argcount(f, cnt): def func_supports_argcount(f, cnt):
argspec = inspect.getargspec(f) argspec = inspect.getargspec(f)
@ -41,7 +42,7 @@ class CommandLine:
print(f'command not found; {c}') print(f'command not found; {c}')
def handle_error(self, cmd, args, e): def handle_error(self, cmd, args, e):
logging.error(e, exc_info=type(e).__name__ not in ['ApiError','CommandError', 'CentralCommandError']) logging.error(e, exc_info=not issubclass(type(e), AppError))
def handle_empty(self): def handle_empty(self):
pass pass
@ -87,11 +88,13 @@ class CommandLine:
p = self.prompt() p = self.prompt()
try: try:
c = input(p) c = input(p)
except EOFError: except (EOFError, KeyboardInterrupt):
self.handle_eof() self.handle_eof()
break break
try: try:
self.handle_cmd(c) self.handle_cmd(c)
except Exception as e: except KeyboardInterrupt:
print("Interrupted")
except (Exception) as e:
logging.error(e, exc_info=True) logging.error(e, exc_info=True)

View File

@ -1,42 +1,76 @@
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 from nullptr.analyzer import *
from nullptr.context import Context
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
from threading import Thread from threading import Thread
from nullptr.atlas_builder import AtlasBuilder from nullptr.captain import Captain
from nullptr.central_command import CentralCommand from nullptr.general import General
class CommandError(Exception): import readline
import os
from copy import copy
class CommandError(AppError):
pass pass
class Commander(CommandLine): class Commander(CommandLine):
def __init__(self, store_file='data/store.npt'): def __init__(self, data_dir='data', auto=False):
self.store = Store(store_file) store_file = os.path.join(data_dir, 'store.npt')
hist_file = os.path.join(data_dir, 'cmd.hst')
self.cred_file = os.path.join(data_dir, 'creds.txt')
self.hist_file = hist_file
if os.path.isfile(hist_file):
readline.read_history_file(hist_file)
self.store = Store(store_file, True)
self.c = Context(self.store)
self.agent = self.select_agent() self.agent = self.select_agent()
self.api = Api(self.store, self.agent) self.c.api = self.api = Api(self.c, self.agent)
self.atlas_builder = AtlasBuilder(self.store, self.api) self.c.general = self.general = General(self.c)
self.centcom = CentralCommand(self.store, self.api) self.c.captain = self.captain = Captain(self.c)
self.analyzer = Analyzer(self.store)
self.general.setup()
self.captain.setup()
self.api.info()
self.ship = None self.ship = None
self.stop_auto = False self.stop_auto = False
if auto:
self.do_auto()
super().__init__() 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): 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:
print('set a ship')
def do_auto(self):
self.captain.run_interactive()
def do_log(self, level):
ship = self.has_ship()
ship._log_level = int(level)
######## 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:
@ -46,6 +80,12 @@ class Commander(CommandLine):
print('not found') print('not found')
return obj return obj
def has_ship(self):
if self.ship is not None:
return self.ship
else:
raise CommandError('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)
@ -53,22 +93,6 @@ class Commander(CommandLine):
agent = self.agent_setup() agent = self.agent_setup()
return agent return agent
def agent_setup(self):
symbol = input('agent name: ')
agent = self.store.get(Agent, symbol, create=True)
api = Api(self.store, agent)
self.api = api
faction = input('faction: ')
api.register(faction.upper().strip())
print('=== agent:')
print(agent)
print('=== ships')
self.do_ships('r')
print('=== contracts')
self.do_contracts('r')
self.store.flush()
return agent
def resolve(self, typ, arg): def resolve(self, typ, arg):
arg = arg.upper() arg = arg.upper()
matches = [c for c in self.store.all(typ) if c.symbol.startswith(arg)] matches = [c for c in self.store.all(typ) if c.symbol.startswith(arg)]
@ -77,118 +101,413 @@ class Commander(CommandLine):
elif len(matches) > 1: elif len(matches) > 1:
raise CommandError('multiple matches') raise CommandError('multiple matches')
else: else:
raise CommandError('not found') raise CommandError(f'{arg} not found')
def after_cmd(self): def resolve_system(self, system_str):
if type(system_str) == System:
return system_str
if system_str == '':
ship = self.has_ship()
system = ship.location.system
else:
system = self.store.get(System, system_str)
return system
def resolve_waypoint(self, w):
if type(w) == Waypoint:
return w
if w == '':
ship = self.has_ship()
return ship.location
p = w.split('-')
if len(p) == 1:
ship = self.has_ship()
s = ship.location.system
w = f'{s}-{w}'
r = self.store.get(Waypoint, w)
if r is None:
raise CommandError(f'{w} not found')
return r
def resolve_ship(self, arg):
symbol = f'{self.agent.symbol}-{arg}'
ship = self.store.get('Ship', symbol)
if ship is None:
raise CommandError(f'ship {arg} not found')
return ship
######## First run #########
def agent_setup(self):
symbol = input('agent name: ')
agent = self.store.get(Agent, symbol, create=True)
self.agent = agent
api = Api(self.c, agent)
self.api = api
faction = input('faction or token: ')
if len(faction) > 50:
self.agent.token = faction
else:
self.do_register(faction)
print('=== agent:')
print(agent)
print('=== ships')
self.do_ships('r')
print('=== contracts')
self.do_contracts('r')
ship = self.store.get(Ship, symbol.upper() + '-2')
print("=== catalog initial system")
self.do_catalog(ship.location.system)
self.do_stats()
self.store.flush() self.store.flush()
return agent
def do_token(self):
print(self.agent.token)
def do_register(self, faction):
self.api.register(faction.upper())
with open(self.cred_file, 'w') as f:
f.write(self.api.agent.symbol)
f.write('\n')
f.write(self.api.agent.token)
pprint(self.api.agent)
def do_reset(self, really):
if really != 'yes':
print('really? type: reset yes')
self.api.list_ships()
for s in self.store.all('Ship'):
self.dump(s, 'all')
self.captain.init_mission(s, 'none')
######## Fleet #########
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 != '':
ship = self.resolve_ship(arg)
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)
if 'SHIPYARD' in w.traits:
self.api.shipyard(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, grep=''):
loc = None
ship = self.has_ship()
loc = ship.location
system = loc.system
print(f'=== waypoints in {system}')
r = self.store.all_members(system, 'Waypoint')
for w in r:
wname = w.symbol.split('-')[2]
traits = ", ".join(w.itraits())
typ = w.type[0]
if typ not in ['F','J'] and len(traits) == 0:
continue
output = ''
if loc:
dist = loc.distance(w)
output = f'{wname:4} {typ} {dist:6} {traits}'
else:
output = f'{wname:4} {typ} {traits}'
if grep == '' or grep.lower() in output.lower():
print(output)
def do_members(self):
ship = self.has_ship()
system = ship.location.system
pprint(list(self.store.all_members(system)))
def do_wp(self, grep=''):
self.do_waypoints(grep)
######## Specials #########
def do_market(self, arg=''):
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:
ship = self.has_ship()
waypoint = ship.location
else:
waypoint = self.store.get(Waypoint, waypoint_str.upper())
r = self.api.jumps(waypoint)
pprint(r, 5)
def do_shipyard(self, w=''):
location = self.resolve_waypoint(w)
if location is None:
raise CommandError(f'waypoint {w} not found')
sy = self.api.shipyard(location)
pprint(sy, 5)
######## Commerce #########
def do_refuel(self, source='market'):
ship = self.has_ship()
from_cargo = source != 'market'
r = self.api.refuel(ship, from_cargo=from_cargo)
pprint(r)
def do_cargo(self):
ship = self.has_ship()
print(f'== Cargo {ship.cargo_units}/{ship.cargo_capacity} ==')
for c, units in ship.cargo.items():
print(f'{units:4d} {c}')
def do_buy(self, resource, amt=None):
ship = self.has_ship()
if amt is None:
amt = ship.cargo_capacity - ship.cargo_units
self.api.buy(ship, resource.upper(), amt)
self.do_cargo()
def do_sell(self, resource, amt=None):
ship = self.has_ship()
self.api.sell(ship, resource.upper(), amt)
self.do_cargo()
def dump(self, ship, resource):
if resource == 'all':
for r in ship.cargo.keys():
self.api.jettison(ship, r)
else:
self.api.jettison(ship, resource.upper())
def do_dump(self, resource):
ship = self.has_ship()
self.dump(ship, resource)
self.do_cargo()
def do_transfer(self, resource, dship, amount=None):
ship = self.has_ship()
resource = resource.upper()
avail = ship.get_cargo(resource)
if amount is None: amount = avail
amount = int(amount)
if avail < amount:
raise CommandError('resource not in cargo')
dship = self.resolve_ship(dship)
self.api.transfer(ship, dship, resource, amount)
def do_purchase(self, ship_type):
ship = self.has_ship()
location = 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):
ship = self.has_ship()
data = self.api.siphon(ship)
def do_survey(self):
ship = self.has_ship()
r = self.api.survey(ship)
pprint(r)
def do_surveys(self):
pprint(list(self.store.all('Survey')))
def do_extract(self, survey_str=''):
ship = self.has_ship()
survey = None
if survey_str != '':
survey = self.resolve('Survey', survey_str)
result = self.api.extract(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)
def do_role(self, role):
roles = [None, 'trader', 'probe', 'siphon', 'hauler', 'surveyor', 'miner']
ship = self.has_ship()
if role == 'none':
role = None
if role not in roles:
print(f'role {role} not found. Choose from {roles}')
return
ship.role = role
def do_mission(self, arg=''): def do_mission(self, arg=''):
if not self.has_ship(): return ship = self.has_ship()
if arg: if arg:
self.centcom.init_mission(self.ship, arg) self.captain.init_mission(ship, arg)
self.print_mission()
def do_mrestart(self, status='init'):
ship = self.has_ship()
self.captain.restart_mission(ship, status)
self.print_mission()
def do_mstep(self):
ship = self.has_ship()
self.captain.single_step(ship)
self.print_mission() self.print_mission()
def do_mreset(self): def do_mreset(self):
if not self.has_ship(): return ship = self.has_ship()
self.ship.mission_state = {} ship.mission_state = {}
def do_mset(self, nm, val): def do_mset(self, nm, val):
if not self.has_ship(): return ship = self.has_ship()
self.centcom.set_mission_param(self.ship, nm, val) self.captain.set_mission_param(ship, nm, val)
def do_crew(self, arg):
ship = self.has_ship()
crew = self.resolve('Crew', arg)
ship.crew = crew
pprint(ship)
def do_phase(self, phase):
self.agent.phase = phase
######## Crews #########
def do_create_crews(self):
crews = self.captain.create_default_crews()
for c in crews:
print(f'{c.symbol:15s} {c.site}')
######## 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_cmine(self): def do_contracts(self, arg=''):
if not self.has_ship(): return if arg.startswith('r'):
site = self.ship.location r = self.api.list_contracts()
else:
r = list(self.store.all('Contract'))
pprint(r)
def do_negotiate(self):
ship = self.has_ship()
r = self.api.negotiate(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):
ship = self.has_ship()
site = ship.location
contract = self.active_contract() contract = self.active_contract()
delivery = contract.unfinished_delivery() delivery = contract.unfinished_delivery()
if delivery is None: if delivery is None:
raise CommandError('no delivery') raise CommandError('no delivery')
resource = delivery['trade_symbol'] resource = delivery['trade_symbol']
destination = delivery['destination'] self.api.deliver(ship, resource, contract)
self.centcom.init_mission(self.ship, 'mine') pprint(contract)
self.centcom.set_mission_param(self.ship, 'site', site)
self.centcom.set_mission_param(self.ship, 'resource', resource)
self.centcom.set_mission_param(self.ship, 'dest', destination)
self.centcom.set_mission_param(self.ship, 'contract', contract)
self.print_mission()
def do_chaul(self): def do_fulfill(self):
if not self.has_ship(): return
if len(self.ship.cargo) > 0:
raise CommandError('please dump cargo first')
contract = self.active_contract() contract = self.active_contract()
delivery = contract.unfinished_delivery() self.api.fulfill(contract)
if delivery is None:
raise CommandError('no delivery')
resource = delivery['trade_symbol']
destination = delivery['destination']
m = self.analyzer.find_closest_markets(resource, 'buy', destination)
if len(m) == 0:
m = self.analyzer.find_closest_markets(resource, 'exchange', destination)
if len(m) == 0:
print('no market found')
return
_, m, _, _ = m[0]
site = self.store.get(Waypoint, m.symbol)
self.centcom.init_mission(self.ship, 'haul')
self.centcom.set_mission_param(self.ship, 'site', site)
self.centcom.set_mission_param(self.ship, 'resource', resource)
self.centcom.set_mission_param(self.ship, 'dest', destination)
self.centcom.set_mission_param(self.ship, 'contract', contract)
self.print_mission()
def do_cprobe(self):
if not self.has_ship(): return
contract = self.active_contract()
delivery = contract.unfinished_delivery()
if delivery is None:
raise CommandError('no delivery')
resource = delivery['trade_symbol']
destination = delivery['destination']
m = self.analyzer.find_closest_markets(resource, 'buy,exchange', destination)
if len(m) is None:
print('no market found')
return
markets = [ mkt[1] for mkt in m]
markets = self.analyzer.solve_tsp(markets)
self.centcom.init_mission(self.ship, 'probe')
self.centcom.set_mission_param(self.ship, 'hops', markets)
self.print_mission()
######## Travel #########
def do_travel(self, dest): def do_travel(self, dest):
ship = self.has_ship()
dest = self.resolve('Waypoint', dest) dest = self.resolve('Waypoint', dest)
self.centcom.init_mission(self.ship, 'travel') self.captain.init_mission(ship, 'travel')
self.centcom.set_mission_param(self.ship, 'dest', dest) self.captain.set_mission_param(ship, 'dest', dest)
self.print_mission() self.print_mission()
def do_register(self, faction): def do_go(self, arg):
self.api.register(faction.upper()) ship = self.has_ship()
pprint(self.api.agent) system = ship.location.system
symbol = f'{system}-{arg}'
dest = self.resolve('Waypoint', symbol)
self.api.navigate(ship, dest)
pprint(ship)
def do_universe(self, page=1): def do_dock(self):
self.atlas_builder.run(page) ship = self.has_ship()
self.api.dock(ship)
pprint(ship)
def do_systems(self, page=1): def do_orbit(self):
r = self.api.list_systems(int(page)) ship = self.has_ship()
pprint(self.api.last_meta) self.api.orbit(ship)
pprint(ship)
def do_speed(self, speed):
ship = self.has_ship()
speed = speed.upper()
speeds = ['DRIFT', 'STEALTH','CRUISE','BURN']
if speed not in speeds:
print('please choose from:', speeds)
self.api.flight_mode(ship, speed)
def do_jump(self, waypoint_str):
ship = self.has_ship()
w = self.resolve('Waypoint', waypoint_str)
self.api.jump(ship, w)
pprint(ship)
######## Analysis #########
def do_server(self):
data = self.api.status()
pprint(data)
def do_highscore(self):
data = self.api.status()
leaders = mg(data, 'leaderboards.mostCredits')
for l in leaders:
a = mg(l,'agentSymbol')
c = mg(l, 'credits')
print(f'{a:15s} {c}')
def do_stats(self): def do_stats(self):
total = 0 total = 0
@ -199,208 +518,67 @@ class Commander(CommandLine):
print(f'{num:5d} {nam}') print(f'{num:5d} {nam}')
print(f'{total:5d} total') print(f'{total:5d} total')
def do_waypoints(self, system_str=''): def do_defrag(self):
if system_str == '': self.store.defrag()
if not self.has_ship(): return
system = self.ship.location.system def do_obj(self, oid):
if not '.' in oid:
print('Usage: obj SYMBOL.ext')
return
symbol, ext = oid.split('.')
symbol = symbol.upper()
if not ext in self.store.extensions:
raise CommandError('unknown extension')
typ = self.store.extensions[ext]
obj = self.store.get(typ, symbol)
if obj is None:
raise CommandError('object not found')
pprint(obj.__getstate__())
print('=== store ===')
h = self.store.get_header(obj)
if h:
pprint(h, 3)
else: else:
system = self.store.get(System, system_str) print('Not stored')
print(f'=== waypoints in {system}') print('Dirty: ', obj in self.store.dirty_objects)
r = self.store.all_members(system, 'Waypoint')
for w in r:
traits = []
if 'MARKETPLACE' in w.traits:
traits.append('MARKET')
if 'SHIPYARD' in w.traits:
traits.append('SHIPYARD')
if w.type == 'JUMP_GATE':
traits.append('JUMP')
if w.type == 'ASTEROID_FIELD':
traits.append('ASTROIDS')
print(w.symbol.split('-')[2], ', '.join(traits))
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_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): def do_query(self, resource):
if not self.has_ship(): return ship = self.has_ship()
location = self.ship.location location = ship.location
resource = resource.upper() resource = resource.upper()
print('Found markets:') print('Found markets:')
for typ, m, d, plen in self.analyzer.find_closest_markets(resource, 'buy,exchange',location): for typ, m, d, plen in find_closest_markets(self.c, resource, 'buy,exchange',location):
price = '?' price = '?'
if resource in m.prices: if resource in m.prices:
price = m.prices[resource]['buy'] price = m.prices[resource]['buy']
print(m, typ[0], f'{plen-1:3} hops {price}') print(m, typ[0], f'{plen-1:3} hops {price}')
def do_path(self): def do_findtrade(self):
orig = self.ask_obj(System, 'from: ') ship = self.has_ship()
dest = self.ask_obj(System, 'to: ') system = ship.location.system
# orig = self.store.get(System, 'X1-KS52') t = find_trade(self.c, system)
# dest = self.store.get(System, 'X1-DA90') pprint(t)
path = self.analyzer.find_path(orig, dest)
pprint(path)
def do_ships(self, arg=''): def do_prices(self, resource=None):
if arg.startswith('r'): ship = self.has_ship()
r = self.api.list_ships() system = ship.location.system
else: prices = prices(self.c, system)
r = list(self.store.all('Ship')) if resource is not None:
pprint(r) prices = {resource: prices[resource.upper()]}
def do_contracts(self, arg=''): for res, p in prices.items():
if arg.startswith('r'): print('==' + res)
r = self.api.list_contracts() for m in p:
else: print(f"{m['wp'].symbol:12s} {m['category']} {m['volume']:5d} {m['buy']:5d} {m['sell']:5d}")
r = list(self.store.all('Contract'))
pprint(r)
def do_deliver(self): def do_path(self, waypoint_str):
if not self.has_ship(): return ship = self.has_ship()
site = self.ship.location w = self.resolve('Waypoint', waypoint_str)
contract = self.active_contract() p = find_nav_path(self.c, ship.location, w, ship.fuel_capacity)
delivery = contract.unfinished_delivery() pprint(p)
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): def do_list(self, klass):
contract = self.active_contract() ship = self.has_ship()
self.api.fulfill(contract) for o in self.store.all_members(klass, ship.location.system):
print(o)
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
symbol = f'{system}-{arg}'
dest = self.resolve('Waypoint', symbol)
self.api.navigate(self.ship, dest)
pprint(self.ship)
def do_dock(self):
if not self.has_ship(): return
self.api.dock(self.ship)
pprint(self.ship)
def do_orbit(self):
if not self.has_ship(): return
self.api.orbit(self.ship)
pprint(self.ship)
def do_negotiate(self):
if not self.has_ship(): return
r = self.api.negotiate(self.ship)
pprint(r)
def do_refuel(self):
if not self.has_ship(): return
r = self.api.refuel(self.ship)
pprint(self.ship)
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)
def do_cargo(self):
if not self.has_ship(): return
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):
if not self.has_ship(): return
self.api.sell(self.ship, resource.upper())
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, system_str):
if not self.has_ship(): return
if '-' not in system_str:
sector = self.ship.location.system.sector.symbol
system_str = f'{sector}-{system_str}'
system = self.resolve('System', system_str)
self.api.jump(self.ship, system)
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)

6
nullptr/context.py Normal file
View File

@ -0,0 +1,6 @@
class Context:
def __init__(self, store, api=None, captain=None, general=None):
self.store = store
self.api = api
self.captain = captain
self.general = general

128
nullptr/general.py Normal file
View File

@ -0,0 +1,128 @@
from nullptr.util import *
from nullptr.analyzer import find_gas, find_metal
class GeneralError(AppError):
pass
class General:
def __init__(self, context):
self.store = context.store
self.api = context.api
self.c = context
agents = self.store.all('Agent')
self.agent = next(agents, None)
self.phases = {
'init': self.phase_startup,
'probes': self.phase_probes,
'trade': self.phase_trade,
'mine': self.phase_mine,
'siphon': self.phase_siphon,
'rampup': self.phase_rampup,
'gate': self.phase_gate
}
def setup(self):
self.create_default_crews()
def find_shipyard(self, stype):
occ = [s.location.symbol for s in self.store.all('Ship') if s.status != 'IN_TRANSIT']
best_price = -1
best_yard = None
for shipyard in self.store.all('Shipyard'):
if stype in shipyard.prices:
price = shipyard.prices[stype]
if shipyard.symbol in occ:
if best_yard is None or price < best_price:
best_yard = shipyard
best_price = price
return best_yard, best_price
def maybe_purchase(self, stype, role):
sy, price = self.find_shipyard(stype)
if sy is None:
return False
traders = [s for s in self.store.all('Ship') if s.role == 'trader']
safe_buffer = len(traders) * 100000 + 100000
#print(safe_buffer, price, sy)
if self.agent.credits < safe_buffer + price:
return # cant afford it!
ship = self.c.api.purchase(stype, sy)
ship.role = role
def tick(self):
phase = self.agent.phase
if phase not in self.phases:
raise GeneralError('Invalid phase')
hdl = self.phases[phase]
new_phase = hdl()
if new_phase:
self.agent.phase = new_phase
def phase_startup(self):
# * first pricing info
# * probe at shipyard that sells probes
ag = self.agent.symbol
command = self.store.get('Ship', f'{ag}-1')
probe = self.store.get('Ship', f'{ag}-2')
if command.role is None:
command.role = 'probe'
if probe.role is None:
probe.role = 'sitter'
system = command.location.system
markets = list(self.store.all_members(system, 'Marketplace'))
discovered = len([m for m in markets if m.last_prices > 0])
if discovered > len(markets) // 2:
return 'probes'
def phase_probes(self):
ag = self.agent.symbol
command = self.store.get('Ship', f'{ag}-1')
# * probes on all markets
if command.role != 'trader':
command.role = 'trader'
self.c.captain.init_mission(command, 'none')
self.maybe_purchase('SHIP_PROBE', 'sitter')
sitters = [s for s in self.store.all('Ship') if s.role == 'sitter']
markets = [m for m in self.store.all('Marketplace')]
if len(sitters) >= len(markets):
return 'trade'
def phase_trade(self):
self.maybe_purchase('SHIP_LIGHT_HAULER', 'trader')
traders = list([s for s in self.store.all('Ship') if s.role == 'trader'])
if len(traders) >= 19:
return 'mine'
def phase_mine(self):
# metal mining crew
pass
def phase_siphon(self):
# siphon crew
pass
def phase_rampup(self):
# stimulate markets for gate building
pass
def phase_gate(self):
# build the gate
pass
def create_default_crews(self):
system = self.api.agent.headquarters.system
gas_w = find_gas(self.c, system)
metal_w = find_metal(self.c, system)
metal = self.store.get('Crew', 'METAL', create=True)
metal.site = metal_w
metal.resources = ['COPPER_ORE','IRON_ORE','ALUMINUM_ORE']
gas = self.store.get('Crew', 'GAS', create=True)
gas.site = gas_w
gas.resources = ['HYDROCARBON','LIQUID_HYDROGEN','LIQUID_NITROGEN']
return [gas, metal]

View File

@ -1,23 +1,32 @@
from nullptr.missions.survey import SurveyMission from nullptr.missions.survey import SurveyMission
from nullptr.missions.mine import MiningMission from nullptr.missions.mine import MiningMission
from nullptr.missions.haul import HaulMission from nullptr.missions.trade import TradeMission
from nullptr.missions.travel import TravelMission from nullptr.missions.travel import TravelMission
from nullptr.missions.probe import ProbeMission from nullptr.missions.probe import ProbeMission
from nullptr.missions.idle import IdleMission
from nullptr.missions.siphon import SiphonMission
from nullptr.missions.haul import HaulMission
from nullptr.missions.sit import SitMission
def get_mission_class( mtype): def get_mission_class( mtype):
types = { types = {
'survey': SurveyMission, 'survey': SurveyMission,
'mine': MiningMission, 'mine': MiningMission,
'haul': HaulMission, 'trade': TradeMission,
'travel': TravelMission, 'travel': TravelMission,
'probe': ProbeMission 'probe': ProbeMission,
'idle': IdleMission,
'siphon': SiphonMission,
'haul': HaulMission,
'sit': SitMission,
} }
if mtype not in types: if mtype not in types:
raise ValueError(f'invalid mission type {mtype}') raise ValueError(f'invalid mission type {mtype}')
return types[mtype] return types[mtype]
def create_mission(mtype, ship, store, api): def create_mission(mtype, ship, c):
typ = get_mission_class(mtype) typ = get_mission_class(mtype)
m = typ(ship, store, api) m = typ(ship, c)
return m return m

View File

@ -5,12 +5,13 @@ from nullptr.models.contract import Contract
from nullptr.models.system import System from nullptr.models.system import System
from nullptr.models.survey import Survey from nullptr.models.survey import Survey
from nullptr.models.ship import Ship from nullptr.models.ship import Ship
from nullptr.analyzer import Analyzer from nullptr.analyzer import *
from time import time from time import time
from functools import partial from functools import partial
import logging import logging
from nullptr.util import * from nullptr.util import *
class MissionError(Exception): class MissionError(Exception):
pass pass
@ -47,12 +48,17 @@ class Mission:
} }
def __init__(self, ship, store, api): def __init__(self, ship, context):
self.ship = ship self.ship = ship
self.store = store self.c = context
self.api = api self.store = context.store
self.api = context.api
self.wait_for = None
self.next_step = 0 self.next_step = 0
self.analyzer = Analyzer(self.store) self.setup()
def setup(self):
pass
def sts(self, nm, v): def sts(self, nm, v):
if issubclass(type(v), Base): if issubclass(type(v), Base):
@ -61,6 +67,8 @@ class Mission:
def rst(self, typ, nm): def rst(self, typ, nm):
symbol = self.st(nm) symbol = self.st(nm)
if symbol is None:
return None
return self.store.get(typ, symbol) return self.store.get(typ, symbol)
def st(self, nm): def st(self, nm):
@ -72,6 +80,16 @@ class Mission:
if nw is None: if nw is None:
return self.ship.mission_status return self.ship.mission_status
else: else:
steps = self.steps()
if nw in ['init','done', 'error']:
self.ship.mission_status = nw
return
elif nw not in steps:
self.ship.log(f"Invalid mission status {nw}", 1)
self.ship.mission_status = 'error'
return
wait_for = steps[nw][2] if len(steps[nw]) > 2 else None
self.wait_for = wait_for
self.ship.mission_status = nw self.ship.mission_status = nw
def start_state(self): def start_state(self):
@ -94,16 +112,26 @@ class Mission:
} }
def step_done(self): def step_done(self):
logging.info(f'mission finished for {self.ship}') self.ship.log(f'mission {type(self).__name__} finished with balance {self.balance()}', 3)
def get_prio(self):
if self.next_step > time() or self.ship.cooldown > time() or self.ship.arrival > time():
return 0
if self.wait_for is not None:
p = int(self.wait_for())
if p > 0:
self.wait_for = None
return p
return 3
def is_waiting(self):
return self.next_step > time()
def is_finished(self): def is_finished(self):
return self.status() in ['done','error'] return self.status() in ['done','error']
def is_ready(self): def is_ready(self):
return not self.is_waiting() and not self.is_finished() if self.is_finished():
return 0
return self.get_prio()
def step(self): def step(self):
steps = self.steps() steps = self.steps()
@ -111,28 +139,45 @@ class Mission:
self.init_state() self.init_state()
status = self.status() status = self.status()
if not status in steps: if not status in steps:
logging.warning(f"Invalid mission status {status}") self.ship.log(f"Invalid mission status {status}", 1)
self.status('error') self.status('error')
return return
handler, next_step = steps[status]
handler = steps[status][0]
next_step = steps[status][1]
try: try:
result = handler() result = handler()
except Exception as e: except Exception as e:
logging.error(e, exc_info=True) self.ship.log(fmtex(e))
self.ship.log(self.api.last_result)
self.status('error') self.status('error')
return return
if type(next_step) == str: if type(next_step) == str:
self.status(next_step) self.status(next_step)
elif type(next_step) == dict: elif type(next_step) == dict:
if result not in next_step: if result not in next_step:
logging.warning(f'Invalid step result {result}') self.ship.log(f'Invalid step result {result}', 1)
self.status('error') self.status('error')
return return
else: else:
if result is None: result=''
self.status(next_step[result]) self.status(next_step[result])
print(f'{self.ship} {status} -> {self.status()}') self.ship.log(f'{status} {result} -> {self.status()}', 8)
class BaseMission(Mission): class BaseMission(Mission):
def balance(self, amt=0):
if type(amt) == dict:
amt = self.api.transaction_cost(amt)
balance = self.st('balance')
if balance is None: balance = 0
balance += amt
self.sts('balance', balance)
return balance
def step_pass(self):
pass
def step_go_dest(self): def step_go_dest(self):
destination = self.rst(Waypoint, 'destination') destination = self.rst(Waypoint, 'destination')
if self.ship.location() == destination: if self.ship.location() == destination:
@ -147,11 +192,20 @@ class BaseMission(Mission):
self.api.navigate(self.ship, site) self.api.navigate(self.ship, site)
self.next_step = self.ship.arrival self.next_step = self.ship.arrival
def step_market(self):
loc = self.ship.location
self.api.marketplace(loc)
def step_shipyard(self):
loc = self.ship.location
if 'SHIPYARD' in loc.traits:
self.api.shipyard(loc)
def step_unload(self): def step_unload(self):
contract = self.rst(Contract, 'contract')
delivery = self.st('delivery') delivery = self.st('delivery')
if delivery == 'sell': if delivery == 'sell':
return self.step_sell(False) return self.step_sell(False)
contract = self.rst(Contract, 'contract')
typs = self.ship.deliverable_cargo(contract) typs = self.ship.deliverable_cargo(contract)
if len(typs) == 0: if len(typs) == 0:
return 'done' return 'done'
@ -162,33 +216,30 @@ class BaseMission(Mission):
return 'more' return 'more'
def step_sell(self, except_resource=True): def step_sell(self, except_resource=True):
target = self.st('resource') market = self.store.get('Marketplace', self.ship.location.symbol)
market = self.store.get('Marketplace', self.ship.location_str)
sellables = market.sellable_items(self.ship.cargo.keys()) sellables = market.sellable_items(self.ship.cargo.keys())
if target in sellables and except_resource:
sellables.remove(target)
if len(sellables) == 0: if len(sellables) == 0:
return 'done' return 'done'
self.api.sell(self.ship, sellables[0]) resource = sellables[0]
if len(sellables) == 1: volume = market.volume(resource)
amt_cargo = self.ship.get_cargo(resource)
amount = min(amt_cargo, volume)
res = self.api.sell(self.ship, resource, amount)
self.balance(res)
if len(sellables) == 1 and amt_cargo == amount:
return 'done' return 'done'
else: else:
return 'more' return 'more'
def step_load(self):
cargo_space = self.ship.cargo_capacity - self.ship.cargo_units
resource = self.st('resource')
self.api.buy(self.ship, resource, cargo_space)
def step_travel(self): def step_travel(self):
traject = self.st('traject') traject = self.st('traject')
if traject is None or traject == []: if traject is None or traject == []:
return 'done' return
dest = traject[-1] dest = traject[-1]
loc = self.ship.location loc = self.ship.location
if dest == loc:
self.sts('traject', None)
return 'done'
hop = traject.pop(0) hop = traject.pop(0)
if type(hop) == Waypoint: if type(hop) == Waypoint:
self.api.navigate(self.ship, hop) self.api.navigate(self.ship, hop)
@ -196,9 +247,19 @@ class BaseMission(Mission):
else: else:
self.api.jump(self.ship, hop) self.api.jump(self.ship, hop)
self.next_step = self.ship.cooldown self.next_step = self.ship.cooldown
if traject == []:
traject= None
self.sts('traject', traject) self.sts('traject', traject)
def step_navigate_traject(self):
traject = self.st('traject')
loc = self.ship.location
if traject is None or traject == []:
return 'done'
dest =traject[-1]
if dest == loc:
return 'done'
return 'more' return 'more'
def step_calculate_traject(self, dest): def step_calculate_traject(self, dest):
@ -206,15 +267,16 @@ class BaseMission(Mission):
dest = self.store.get(Waypoint, dest) dest = self.store.get(Waypoint, dest)
loc = self.ship.location loc = self.ship.location
loc_sys = loc.system loc_sys = loc.system
loc_jg = self.analyzer.get_jumpgate(loc_sys)
loc_jg = get_jumpgate(self.c, loc_sys)
loc_jg_wp = self.store.get(Waypoint, loc_jg.symbol) loc_jg_wp = self.store.get(Waypoint, loc_jg.symbol)
dest_sys = dest.system dest_sys = dest.system
dest_jg = self.analyzer.get_jumpgate(dest_sys) dest_jg = get_jumpgate(self.c, dest_sys)
if dest_sys == loc_sys: if dest_sys == loc_sys:
result = [dest] result = find_nav_path(self.c, loc, dest, self.ship.range())
self.sts('traject', result) self.sts('traject', result)
return return 'done' if len(result) == 0 else 'more'
path = self.analyzer.find_path(loc_sys, dest_sys) path = find_jump_path(self.c, loc_sys, dest_sys)
result = [] result = []
if loc.symbol != loc_jg.symbol: if loc.symbol != loc_jg.symbol:
result.append(loc_jg_wp) result.append(loc_jg_wp)
@ -223,33 +285,54 @@ class BaseMission(Mission):
result.append(dest) result.append(dest)
self.sts('traject', result) self.sts('traject', result)
print(result) print(result)
return result return 'more'
def step_dock(self): def step_dock(self):
if self.ship.status == 'DOCKED':
return
self.api.dock(self.ship) self.api.dock(self.ship)
def step_refuel(self): def step_refuel(self):
if self.ship.fuel_capacity == 0: if self.ship.fuel_capacity == 0:
return return
if self.ship.fuel_current / self.ship.fuel_capacity < 0.5: #if self.ship.fuel_capacity - self.ship.fuel_current > 100:
try: try:
self.api.refuel(self.ship) self.api.refuel(self.ship)
except Exception as e: except Exception as e:
pass pass
def step_orbit(self): def step_orbit(self):
if self.ship.status != 'DOCKED':
return
self.api.orbit(self.ship) self.api.orbit(self.ship)
def travel_steps(self, nm, destination, next_step): def travel_steps(self, nm, destination, next_step):
destination = self.st(destination) destination = self.st(destination)
calc = partial(self.step_calculate_traject, destination) calc = partial(self.step_calculate_traject, destination)
return { steps = {
f'travel-{nm}': (self.step_orbit, f'calc-trav-{nm}'),
f'calc-trav-{nm}': (calc, f'go-{nm}'), f'travel-{nm}': (calc, {
f'go-{nm}': (self.step_travel, { 'more': f'dock-{nm}',
'done': f'dock-{nm}', 'done': next_step
'more': f'go-{nm}'
}), }),
f'dock-{nm}': (self.step_dock, f'refuel-{nm}'), f'dock-{nm}': (self.step_dock, f'refuel-{nm}'),
f'refuel-{nm}': (self.step_refuel, next_step) f'refuel-{nm}': (self.step_refuel, f'orbit-{nm}'),
f'orbit-{nm}': (self.step_orbit, f'go-{nm}'),
f'go-{nm}': (self.step_travel, f'nav-{nm}'),
f'nav-{nm}': (self.step_navigate_traject, {
'done': next_step,
'more': f'dock-{nm}'
})
} }
if self.ship.fuel_capacity == 0:
steps = {
f'travel-{nm}': (calc, f'orbit-{nm}'),
f'orbit-{nm}': (self.step_orbit, f'go-{nm}'),
f'go-{nm}': (self.step_travel, f'nav-{nm}'),
f'nav-{nm}': (self.step_navigate_traject, {
'done': next_step,
'more': f'go-{nm}'
}),
}
return steps

View File

@ -0,0 +1,28 @@
from nullptr.missions.base import BaseMission
class ExtractionMission(BaseMission):
def find_hauler(self, r):
for s in self.store.all('Ship'):
if s.mission != 'haul': continue
if s.location != self.ship.location:
continue
if s.mission_status != 'load':
continue
if r not in s.mission_state['resources']: continue
return s
return None
def step_unload(self):
if len(self.ship.cargo) == 0:
return 'done'
r = list(self.ship.cargo.keys())[0]
amt = self.ship.cargo[r]
h = self.find_hauler(r)
if h is None:
self.api.jettison(self.ship, r)
else:
space = h.cargo_space()
amt = min(space, amt)
if amt > 0:
self.api.transfer(self.ship, h, r, amt)
return 'more'

View File

@ -1,25 +1,52 @@
from nullptr.missions.base import BaseMission, MissionParam from nullptr.missions.base import BaseMission, MissionParam
from nullptr.models.waypoint import Waypoint from nullptr.models.waypoint import Waypoint
from nullptr.models.survey import Survey
from nullptr.models.contract import Contract
class HaulMission(BaseMission): class HaulMission(BaseMission):
def start_state(self): def start_state(self):
return 'travel-to' return 'travel-to'
def step_turn(self):
self.ship.log('starting haul load')
def wait_turn(self):
for s in self.store.all('Ship'):
if s.mission != 'haul': continue
if s.location != self.ship.location:
continue
if s.mission_state['dest'] != self.st('dest'):
continue
if s.mission_status != 'load':
continue
return 0
return 5
def step_load(self):
pass
def cargo_full(self):
if self.ship.cargo_space() == 0:
return 5
return 0
@classmethod @classmethod
def params(cls): def params(cls):
return { return {
'site': MissionParam(Waypoint, True), 'site': MissionParam(Waypoint, True),
'resource': MissionParam(str, True),
'dest': MissionParam(Waypoint, True), 'dest': MissionParam(Waypoint, True),
'delivery': MissionParam(str, True, 'deliver'), 'resources': MissionParam(list, True)
'contract': MissionParam(Contract, False)
} }
def steps(self): def steps(self):
return { return {
**self.travel_steps('to', 'site', 'load'), **self.travel_steps('to', 'site', 'wait-turn'),
'load': (self.step_load, 'travel-back'), 'wait-turn': (self.step_turn, 'load', self.wait_turn),
**self.travel_steps('back', 'dest', 'unload'), 'load': (self.step_load, 'travel-back', self.cargo_full),
'unload': (self.step_unload, 'travel-to'), **self.travel_steps('back', 'dest', 'dock-dest'),
'dock-dest': (self.step_dock, 'unload'),
'unload': (self.step_sell, {
'more': 'unload',
'done': 'market-dest'
}),
'market-dest': (self.step_market, 'report'),
'report': (self.step_done, 'done')
} }

26
nullptr/missions/idle.py Normal file
View File

@ -0,0 +1,26 @@
from nullptr.missions.base import BaseMission, MissionParam
import time
class IdleMission(BaseMission):
def start_state(self):
return 'start'
def step_wait(self):
self.next_step = int(time.time()) + self.st('seconds')
def step_idle(self):
pass
@classmethod
def params(cls):
return {
'seconds': MissionParam(int, True)
}
def steps(self):
return {
'start': (self.step_wait, 'wait'),
'wait': (self.step_idle, 'done')
}

View File

@ -3,16 +3,14 @@ from nullptr.models.waypoint import Waypoint
from nullptr.models.survey import Survey from nullptr.models.survey import Survey
from nullptr.models.contract import Contract from nullptr.models.contract import Contract
from nullptr.util import * from nullptr.util import *
from nullptr.missions.extraction import ExtractionMission
class MiningMission(BaseMission): class MiningMission(ExtractionMission):
@classmethod @classmethod
def params(cls): def params(cls):
return { return {
'site': MissionParam(Waypoint, True), 'site': MissionParam(Waypoint, True),
'resource': MissionParam(str, True), 'resources': MissionParam(list, True)
'dest': MissionParam(Waypoint, True),
'delivery': MissionParam(str, True, 'deliver'),
'contract': MissionParam(Contract, False)
} }
def start_state(self): def start_state(self):
@ -20,61 +18,43 @@ class MiningMission(BaseMission):
def steps(self): def steps(self):
return { return {
**self.travel_steps('to', 'site', 'orbit1'), **self.travel_steps('to', 'site', 'extract'),
'orbit1': (self.step_orbit, 'extract'),
'extract': (self.step_extract, { 'extract': (self.step_extract, {
'done': 'dock', 'more': 'extract',
'more': 'extract' 'done': 'unload'
}), }),
'dock': (self.step_dock, 'sell'),
'sell': (self.step_sell, {
'more': 'sell',
'done': 'orbit2',
}),
'orbit2': (self.step_orbit, 'jettison'),
'jettison': (self.step_dispose, {
'more': 'jettison',
'done': 'extract',
'full': 'travel-back'
}),
**self.travel_steps('back', 'dest', 'unload'),
'unload': (self.step_unload, { 'unload': (self.step_unload, {
'done': 'travel-to', 'more': 'unload',
'more': 'unload' 'done': 'done'
}), })
} }
def get_survey(self): def get_survey(self):
resource = self.st('resource') resources = self.st('resources')
site = self.rst(Waypoint,'site') site = self.rst(Waypoint,'site')
best_score = 0
best_survey = None
# todo optimize # todo optimize
for s in self.store.all(Survey): for s in self.store.all(Survey):
if resource in s.deposits and site.symbol == s.waypoint(): if site != s.waypoint:
return s continue
return None good = len([1 for r in s.deposits if r in resources])
total = len(s.deposits)
score = good / total
if score > best_score:
best_score = score
best_survey = s
return best_survey
def step_extract(self): def step_extract(self):
survey = self.get_survey() survey = self.get_survey()
print('using survey:', str(survey))
result = self.api.extract(self.ship, survey) result = self.api.extract(self.ship, survey)
symbol = sg(result,'extraction.yield.symbol') symbol = sg(result,'extraction.yield.symbol')
units = sg(result,'extraction.yield.units') units = sg(result,'extraction.yield.units')
print('extracted:', units, symbol)
self.next_step = self.ship.cooldown self.next_step = self.ship.cooldown
if self.ship.cargo_units < self.ship.cargo_capacity: if self.ship.cargo_space() > 5:
return 'more' return 'more'
else: else:
return 'done' return 'done'
def step_dispose(self):
contract = self.rst(Contract, 'contract')
typs = self.ship.nondeliverable_cargo(contract)
if len(typs) > 0:
self.api.jettison(self.ship, typs[0])
if len(typs) > 1:
return 'more'
elif self.ship.cargo_units > self.ship.cargo_capacity - 3:
return 'full'
else:
return 'done'

View File

@ -20,10 +20,6 @@ class ProbeMission(BaseMission):
} }
def step_market(self):
loc = self.ship.location()
self.api.marketplace(loc)
def step_next_hop(self): def step_next_hop(self):
hops = self.st('hops') hops = self.st('hops')
next_hop = self.st('next-hop') next_hop = self.st('next-hop')

View File

@ -0,0 +1,38 @@
from nullptr.missions.base import MissionParam
from nullptr.missions.extraction import ExtractionMission
from nullptr.models.waypoint import Waypoint
class SiphonMission(ExtractionMission):
def start_state(self):
return 'travel-to'
@classmethod
def params(cls):
return {
'site': MissionParam(Waypoint, True),
}
def step_siphon(self):
result = self.api.siphon(self.ship)
self.next_step = self.ship.cooldown
if self.ship.cargo_space() > 5:
return 'more'
else:
return 'full'
def steps(self):
return {
**self.travel_steps('to', 'site', 'siphon'),
'siphon': (self.step_siphon, {
'more': 'siphon',
'full': 'unload'
}),
'unload': (self.step_unload, {
'more': 'unload',
'done': 'done'
})
}

25
nullptr/missions/sit.py Normal file
View File

@ -0,0 +1,25 @@
from nullptr.missions.base import BaseMission, MissionParam
from nullptr.models.waypoint import Waypoint
from time import time
class SitMission(BaseMission):
def start_state(self):
return 'travel-to'
@classmethod
def params(cls):
return {
'dest': MissionParam(Waypoint, True)
}
def steps(self):
return {
**self.travel_steps('to', 'dest', 'market'),
'sit': (self.step_sit, 'market'),
'market': (self.step_market, 'shipyard'),
'shipyard': (self.step_shipyard, 'sit')
}
def step_sit(self):
self.next_step = time() + 15 * 60

View File

@ -1,11 +1,19 @@
from nullptr.missions.base import BaseMission, MissionParam from nullptr.missions.base import BaseMission, MissionParam
from nullptr.models.waypoint import Waypoint
class SurveyMission(BaseMission): class SurveyMission(BaseMission):
def start_state(self): def start_state(self):
return 'survey' return 'travel-to'
@classmethod
def params(cls):
return {
'site': MissionParam(Waypoint, True),
}
def steps(self): def steps(self):
return { return {
**self.travel_steps('to', 'site', 'survey'),
'survey': (self.step_survey, 'survey') 'survey': (self.step_survey, 'survey')
} }

54
nullptr/missions/trade.py Normal file
View File

@ -0,0 +1,54 @@
from nullptr.missions.base import BaseMission, MissionParam
from nullptr.models.waypoint import Waypoint
from nullptr.models.survey import Survey
from nullptr.models.contract import Contract
from nullptr.analyzer import find_deal
class TradeMission(BaseMission):
def start_state(self):
return 'travel-to'
def step_load(self):
credits = self.api.agent.credits
cargo_space = self.ship.cargo_capacity - self.ship.cargo_units
smkt = self.store.get('Marketplace', self.st('site'))
dmkt = self.store.get('Marketplace', self.st('dest'))
resource = find_deal(self.c, smkt, dmkt)
if resource is None:
return 'done'
price = smkt.buy_price(resource)
volume = smkt.volume(resource)
affordable = credits // price
amount = min(cargo_space, affordable, volume)
if amount == 0:
return 'done'
res = self.api.buy(self.ship, resource, amount)
self.balance(res)
return 'done' if amount == cargo_space else 'more'
@classmethod
def params(cls):
return {
'site': MissionParam(Waypoint, True),
'dest': MissionParam(Waypoint, True),
}
def steps(self):
return {
**self.travel_steps('to', 'site', 'dock'),
'dock': (self.step_dock, 'market-pre'),
'market-pre': (self.step_market, 'load'),
'load': (self.step_load, {
'more': 'market-pre',
'done': 'market-post'
}),
'market-post': (self.step_market, 'travel-back'),
**self.travel_steps('back', 'dest', 'dock-dest'),
'dock-dest': (self.step_dock, 'unload'),
'unload': (self.step_sell, {
'more': 'unload',
'done': 'market-dest'
}),
'market-dest': (self.step_market, 'report'),
'report': (self.step_done, 'done')
}

View File

@ -8,5 +8,8 @@ from nullptr.models.jumpgate import Jumpgate
from nullptr.models.ship import Ship from nullptr.models.ship import Ship
from nullptr.models.contract import Contract from nullptr.models.contract import Contract
from nullptr.models.survey import Survey from nullptr.models.survey import Survey
from nullptr.models.atlas import Atlas
from nullptr.models.crew import Crew
from nullptr.models.shipyard import Shipyard
__all__ = [ 'Waypoint', 'Sector', 'Ship', 'Survey', 'System', 'Agent', 'Marketplace', 'Jumpgate', 'Contract', 'Base' ] __all__ = [ 'Waypoint', 'Sector', 'Ship', 'Survey', 'System', 'Agent', 'Marketplace', 'Jumpgate', 'Contract', 'Base', 'Atlas', 'Crew', 'Shipyard' ]

View File

@ -1,12 +1,17 @@
from .base import Base from .base import Base
from nullptr.models.waypoint import Waypoint
class Agent(Base): class Agent(Base):
def define(self): def define(self):
self.token: str = None self.token: str = None
self.credits: int = 0 self.credits: int = 0
self.headquarters: Waypoint = None
self.phase = 'init'
def update(self, d): def update(self, d):
self.seta('credits', d) self.seta('credits', d)
getter = self.store.getter(Waypoint, create=True)
self.seta('headquarters', d, interp=getter)
@classmethod @classmethod
def ext(self): def ext(self):
@ -15,5 +20,6 @@ class Agent(Base):
def f(self, detail=1): def f(self, detail=1):
r = super().f(detail) r = super().f(detail)
if detail >2: if detail >2:
r += f' c:{self.credits}' r += f' c:{self.credits}\n'
r+= f'phase: {self.phase}'
return r return r

19
nullptr/models/atlas.py Normal file
View File

@ -0,0 +1,19 @@
from .base import Base
class Atlas(Base):
@classmethod
def ext(self):
return 'atl'
def define(self):
self.total_pages = 0
self.seen_pages = 0
self.enabled = False
def f(self, detail=1):
r = super().f(detail)
if detail >2:
if not self.enabled:
r += ' OFF'
r += f' {self.seen_pages}/{self.total_pages}'
return r

View File

@ -15,6 +15,9 @@ class Reference:
def resolve(self): def resolve(self):
return self.store.get(self.typ, self.symbol) return self.store.get(self.typ, self.symbol)
def f(self, detail):
return f'{self.symbol}.{self.typ.ext()}'
def __repr__(self): def __repr__(self):
return f'*REF*{self.symbol}.{self.typ.ext()}' return f'*REF*{self.symbol}.{self.typ.ext()}'
@ -22,12 +25,22 @@ 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('_')}
def dirty(self):
self.store.dirty(self)
@classmethod @classmethod
def ext(cls): def ext(cls):
@ -36,6 +49,9 @@ class Base:
def define(self): def define(self):
pass pass
def created(self):
pass
def __hash__(self): def __hash__(self):
return hash((str(type(self)), self.symbol)) return hash((str(type(self)), self.symbol))
@ -57,25 +73,33 @@ class Base:
val = interp(val) val = interp(val)
setattr(self, attr, val) setattr(self, attr, val)
def setlst(self, attr, d, name, member, interp=None): def __lt__(self, o):
return self.symbol < o.symbol
def setlst(self, attr, d, name, member=None, interp=None):
val = sg(d, name) val = sg(d, name)
if val is not None: if val is not None:
lst = [] lst = []
for x in val: for x in val:
val = sg(x, member) if member is not None:
x = sg(x, member)
if interp is not None: if interp is not None:
val = interp(val) x = interp(x)
lst.append(val) lst.append(x)
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.dirty()
if issubclass(type(value), Base): if issubclass(type(value), Base):
value = Reference.create(value) value = Reference.create(value)
super().__setattr__(name, value) super().__setattr__(name, value)
def __getattribute__(self, nm): def __getattribute__(self, nm):
if nm == 'system':
return self.get_system()
if nm == 'waypoint':
return self.get_waypoint()
val = super().__getattribute__(nm) val = super().__getattribute__(nm)
if type(val) == Reference: if type(val) == Reference:
val = val.resolve() val = val.resolve()
@ -87,11 +111,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__

16
nullptr/models/crew.py Normal file
View File

@ -0,0 +1,16 @@
from .base import Base
class Crew(Base):
@classmethod
def ext(self):
return 'crw'
def define(self):
self.site = None
self.resources = []
def f(self, detail=1):
r = super().f(detail)
if detail >2:
r += f'\nSite: {self.site}'
return r

View File

@ -1,27 +1,22 @@
from .base import Base from .base import Base
from .system import System from .waypoint import Waypoint
from dataclasses import field from dataclasses import field
class Jumpgate(Base): class Jumpgate(Base):
def define(self): def define(self):
self.range: int = 0 self.connections: list = []
self.faction: str = ''
self.systems: list = []
self.system = self.get_system()
def update(self, d): def update(self, d):
getter = self.store.getter(System, create=True) getter = self.store.getter(Waypoint, create=True)
self.setlst('systems', d, 'connectedSystems', 'symbol', interp=getter) self.setlst('connections', d, 'connections', interp=getter)
self.seta('faction', d, 'factionSymbol')
self.seta('range', d, 'jumpRange')
@classmethod @classmethod
def ext(self): def ext(self):
return 'jmp' return 'jmp'
def f(self, detail=1): def f(self, detail=1):
r = self.symbol r = super().f(detail)
if detail > 1: if detail > 2:
r += '\n' r += '\n'
r += '\n'.join([s.symbol for s in self.systems]) r += '\n'.join([s.symbol for s in self.connections])
return r return r

View File

@ -1,9 +1,40 @@
from .base import Base from .base import Base, Reference
from time import time from time import time
from nullptr.util import * from nullptr.util import *
from dataclasses import field from dataclasses import field, dataclass
from nullptr.models import Waypoint from nullptr.models import Waypoint
from typing import List, Tuple
SUPPLY = ['SCARCE','LIMITED','MODERATE','HIGH','ABUNDANT']
ACTIVITY =['RESTRICTED','WEAK','GROWING','STRONG']
class MarketEntry:
def __init__(self):
self.buy = 0
self.sell = 0
self.volume = 0
self.supply = 0
self.activity = 0
self.history = []
def upg(self):
if not hasattr(self, 'history'):
self.history = []
def f(self, detail=1):
self.upg()
return f'b: {self.buy} s:{self.sell} hist: {len(self.history)}'
def add(self, buy, sell, volume, supply, activity):
self.upg()
self.buy = buy
self.sell = sell
self.volume = volume
self.supply = supply
self.activity = activity
#self.history.append((int(time()), buy, sell, volume, supply, activity))
class Marketplace(Base): class Marketplace(Base):
def define(self): def define(self):
@ -12,12 +43,27 @@ class Marketplace(Base):
self.exchange:list = [] self.exchange:list = []
self.prices:dict = {} self.prices:dict = {}
self.last_prices:int = 0 self.last_prices:int = 0
self.set_waypoint()
self.system = self.get_system()
def set_waypoint(self): def get_waypoint(self):
waypoint = self.store.get(Waypoint, self.symbol, create=True) return self.store.get('Waypoint', self.symbol, create=True)
self.waypoint = waypoint
def is_fuel(self):
return self.imports + self.exports + self.exchange == ['FUEL']
def record_prices(self, data):
for g in data:
symbol= mg(g, 'symbol')
if symbol in self.prices:
e = self.prices[symbol]
else:
e = self.prices[symbol] = MarketEntry()
buy = mg(g, 'purchasePrice')
sell = mg(g, 'sellPrice')
volume = mg(g, 'tradeVolume')
supply = SUPPLY.index(mg(g, 'supply'))
activity = ACTIVITY.index(sg(g, 'activity','STRONG'))
e.add(buy, sell, volume, supply, activity)
self.dirty()
def update(self, d): def update(self, d):
self.setlst('imports', d, 'imports', 'symbol') self.setlst('imports', d, 'imports', 'symbol')
@ -25,16 +71,17 @@ class Marketplace(Base):
self.setlst('exchange', d, 'exchange', 'symbol') self.setlst('exchange', d, 'exchange', 'symbol')
if 'tradeGoods' in d: if 'tradeGoods' in d:
self.last_prices = time() self.last_prices = time()
prices = {} self.record_prices(mg(d, 'tradeGoods'))
for g in mg(d, 'tradeGoods'):
price = {} def buy_price(self, resource):
symbol= mg(g, 'symbol') if resource not in self.prices:
price['symbol'] = symbol return None
price['buy'] = mg(g, 'purchasePrice') return self.prices[resource].buy
price['sell'] = mg(g, 'sellPrice')
price['volume'] = mg(g, 'tradeVolume') def volume(self, resource):
prices[symbol] = price if resource not in self.prices:
self.prices = prices return None
return self.prices[resource].volume
def sellable_items(self, resources): def sellable_items(self, resources):
return [r for r in resources if r in self.prices] return [r for r in resources if r in self.prices]
@ -53,10 +100,18 @@ class Marketplace(Base):
return '?' return '?'
def f(self, detail=1): def f(self, detail=1):
r = self.symbol r = super().f(detail)
if detail > 1: if detail > 2:
r += '\n' r += '\n'
for p in self.prices.values(): if len(self.imports) > 0:
t = self.rtype(p['symbol']) r += 'I: ' + ', '.join(self.imports) + '\n'
r += f'{t} {p["symbol"]:25s} {p["sell"]:5d} {p["buy"]:5d}\n' if len(self.exports) > 0:
r += 'E: ' + ', '.join(self.exports) + '\n'
if len(self.exchange) > 0:
r += 'X: ' + ', '.join(self.exchange) + '\n'
r += '\n'
for res, p in self.prices.items():
t = self.rtype(res)
r += f'{t} {res:25s} {p.buy:5d} {p.sell:5d}\n'
return r return r

View File

@ -1,7 +1,8 @@
from .base import Base from .base import Base
from time import time from time import time, strftime
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):
@ -17,13 +18,41 @@ class Ship(Base):
self.fuel_capacity:int = 0 self.fuel_capacity:int = 0
self.mission:str = None self.mission:str = None
self.mission_status:str = 'init' self.mission_status:str = 'init'
self.role = None
self.crew = None
self.frame = ''
self.speed = "CRUISE"
self._log_file = None
self._log_level = 5
def log(self, m, l=3):
if m is None: return
if type(m) != str:
m = pretty(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 = strftime('%Y%m%d %H%M%S')
sts = strftime('%H%M%S')
m = m.strip()
self._log_file.write(f'{ts} {m}\n')
self._log_file.flush()
if l <= self._log_level:
print(f'{self} {sts} {m}')
@classmethod @classmethod
def ext(self): def ext(self):
return 'shp' return 'shp'
def range(self):
if self.fuel_capacity == 0:
return 100000
return self.fuel_capacity
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')
@ -55,16 +84,39 @@ class Ship(Base):
return 0 return 0
return self.cargo[typ] return self.cargo[typ]
def take_cargo(self, typ, amt):
if typ not in self.cargo:
return
if self.cargo[typ] <= amt:
del self.cargo[typ]
else:
self.cargo[typ] -= amt
self.cargo_units = sum(self.cargo.values())
def put_cargo(self, typ, amt):
if typ not in self.cargo:
self.cargo[typ] = amt
else:
self.cargo[typ] += amt
self.cargo_units = sum(self.cargo.values())
def load_cargo(self, cargo): def load_cargo(self, cargo):
result = {} result = {}
total = 0
for i in cargo: for i in cargo:
symbol = must_get(i, 'symbol') symbol = must_get(i, 'symbol')
units = must_get(i, 'units') units = must_get(i, 'units')
result[symbol] = units result[symbol] = units
total += units
self.cargo_units = total
self.cargo = result self.cargo = result
def deliverable_cargo(self, contract): def deliverable_cargo(self, contract):
result = [] result = []
if contract is None:
return result
for d in contract.deliveries: for d in contract.deliveries:
if self.get_cargo(d['trade_symbol']) > 0: if self.get_cargo(d['trade_symbol']) > 0:
result.append(d['trade_symbol']) result.append(d['trade_symbol'])
@ -76,6 +128,9 @@ class Ship(Base):
garbage = [c for c in cargo if c not in deliveries] garbage = [c for c in cargo if c not in deliveries]
return garbage return garbage
def cargo_space(self):
return self.cargo_capacity - self.cargo_units
def update_timers(self): def update_timers(self):
if self.status == 'IN_TRANSIT' and self.arrival < time(): if self.status == 'IN_TRANSIT' and self.arrival < time():
self.status = 'IN_ORBIT' self.status = 'IN_ORBIT'
@ -85,14 +140,52 @@ 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())
role = self.role
if role is None:
role = 'none'
crew = 'none'
if self.crew is not None:
crew = self.crew.symbol
mstatus = self.mission_status
if mstatus == 'error':
mstatus = mstatus.upper()
if mstatus is None:
mstatus = 'none'
status = self.status.lower()
if status.startswith('in_'):
status = status[3:]
if detail < 2:
r = self.symbol r = self.symbol
if detail > 1: elif detail == 2:
r += ' ' + self.status symbol = self.symbol.split('-')[1]
r += f' [{self.fuel_current}/{self.fuel_capacity}]'
r += ' ' + str(self.location) 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: {crew} / {role}\n'
r += f'Mission: {self.mission} ({mstatus})\n'
for k, v in self.mission_state.items():
if type(v) == list:
v = f'[{len(v)} 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

@ -0,0 +1,35 @@
from nullptr.models import Base
from time import time
from nullptr.util import *
class Shipyard(Base):
def define(self):
self.last_prices = 0
self.types = set()
self.prices:dict = {}
def get_waypoint(self):
return self.store.get('Waypoint', self.symbol, create=True)
@classmethod
def ext(self):
return 'syd'
def update(self, d):
if 'ships' in d:
self.last_prices = time()
for s in must_get(d, 'ships'):
self.prices[s['type']] = s['purchasePrice']
for s in must_get(d, 'shipTypes'):
self.types.add(s['type'])
def f(self, detail=1):
r = super().f(detail)
if detail > 2:
r += '\n'
for st in self.types:
price = "Unknown"
if st in self.prices:
price = self.prices[st]
r += f'{st:20} {price}\n'
return r

View File

@ -18,6 +18,11 @@ class Survey(Base):
def ext(cls): def ext(cls):
return 'svy' return 'svy'
def get_waypoint(self):
sym = '-'.join(self.symbol.split('-')[:3])
return self.store.get('Waypoint', sym, create=True)
def is_expired(self): def is_expired(self):
return time() > self.expires or self.exhausted return time() > self.expires or self.exhausted
@ -28,7 +33,7 @@ class Survey(Base):
def api_dict(self): def api_dict(self):
return { return {
'signature': self.symbol, 'signature': self.symbol,
'symbol': self.waypoint(), 'symbol': self.waypoint.symbol,
'deposits': [{'symbol': d} for d in self.deposits], 'deposits': [{'symbol': d} for d in self.deposits],
'expiration': self.expires_str, 'expiration': self.expires_str,
'size': size_names[self.size] 'size': size_names[self.size]

View File

@ -7,6 +7,8 @@ class System(Base):
self.x:int = 0 self.x:int = 0
self.y:int = 0 self.y:int = 0
self.type:str = 'unknown' self.type:str = 'unknown'
self.uncharted = True
self.last_crawl = 0
def update(self, d): def update(self, d):
self.seta('x', d) self.seta('x', d)

View File

@ -1,6 +1,8 @@
from .base import Base, Reference from .base import Base, Reference
from nullptr.models.system import System from nullptr.models.system import System
from nullptr.util import * from nullptr.util import *
from time import time
from math import sqrt
class Waypoint(Base): class Waypoint(Base):
def define(self): def define(self):
@ -9,16 +11,55 @@ class Waypoint(Base):
self.type:str = 'unknown' self.type:str = 'unknown'
self.traits:list = [] self.traits:list = []
self.faction:str = '' self.faction:str = ''
self.system = self.get_system() self.is_under_construction:bool = False
self.uncharted = True
self.extracted:int = 0
def update(self, d): def update(self, d):
self.seta('x', d) self.seta('x', d)
self.seta('y', d) self.seta('y', d)
self.seta('type', d) self.seta('type', d)
self.seta('faction', d, 'faction.symbol') self.seta('faction', d, 'faction.symbol')
self.seta('is_under_construction', d, 'isUnderConstruction')
self.setlst('traits', d, 'traits', 'symbol') self.setlst('traits', d, 'traits', 'symbol')
self.uncharted = 'UNCHARTED' in self.traits
def created(self):
self.get_system()
def distance(self, other):
return int(sqrt((self.x - other.x) ** 2 + (self.y - other.y) ** 2))
@classmethod @classmethod
def ext(self): def ext(self):
return 'way' return 'way'
def itraits(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 '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
def f(self, detail=1):
r = self.symbol
if detail > 3:
r += f'\n{self.x} {self.y}'
return r

23
nullptr/roles/__init__.py Normal file
View File

@ -0,0 +1,23 @@
from nullptr.roles.trader import assign_trader
from nullptr.roles.probe import assign_probe
from nullptr.roles.siphon import assign_siphon
from nullptr.roles.hauler import assign_hauler
from nullptr.roles.surveyor import assign_surveyor
from nullptr.roles.miner import assign_miner
from nullptr.roles.sitter import assign_sitter
def assign_mission(c, s):
if s.role == 'trader':
assign_trader(c, s)
elif s.role == 'probe':
assign_probe(c, s)
elif s.role == 'siphon':
assign_siphon(c, s)
elif s.role == 'hauler':
assign_hauler(c, s)
elif s.role == 'surveyor':
assign_surveyor(c, s)
elif s.role == 'miner':
assign_miner(c, s)
elif s.role == 'sitter':
assign_sitter(c, s)

16
nullptr/roles/hauler.py Normal file
View File

@ -0,0 +1,16 @@
from nullptr.util import AppError
from nullptr.analyzer import best_sell_market
from random import choice
def assign_hauler(c, s):
if s.crew is None:
raise AppError('ship has no crew')
w = s.crew.site
resources = s.crew.resources
resource = choice(resources)
m = best_sell_market(c,s.location.system, resource)
s.log(f'assigning haul mission from {w} to {m}')
c.captain.init_mission(s, 'haul')
c.captain.smipa(s, 'site', w)
c.captain.smipa(s, 'dest', m)
c.captain.smipa(s, 'resources', resources)

11
nullptr/roles/miner.py Normal file
View File

@ -0,0 +1,11 @@
from nullptr.util import AppError
def assign_miner(c, s):
if s.crew is None:
raise AppError('ship has no crew')
w = s.crew.site
resources = s.crew.resources
c.captain.init_mission(s, 'mine')
c.captain.smipa(s, 'site', w)
c.captain.smipa(s, 'resources', resources)

15
nullptr/roles/probe.py Normal file
View File

@ -0,0 +1,15 @@
from nullptr.analyzer import solve_tsp
from random import randrange
def assign_probe(c, s):
system = s.location.system
m = [m.waypoint for m in c.store.all_members(system, 'Marketplace')]
m = solve_tsp(c, m)
hops = [w.symbol for w in m]
start_hop = 0
s.log(f'Assigning {s} to probe {len(hops)} starting at {hops[start_hop]}')
c.captain.init_mission(s, 'probe')
c.captain.smipa(s, 'hops', hops)
c.captain.smipa(s, 'next-hop', start_hop)

8
nullptr/roles/siphon.py Normal file
View File

@ -0,0 +1,8 @@
from nullptr.util import AppError
def assign_siphon(c, s):
if s.crew is None:
raise AppError('ship has no crew')
w = s.crew.site
c.captain.init_mission(s, 'siphon')
c.captain.smipa(s, 'site', w)

24
nullptr/roles/sitter.py Normal file
View File

@ -0,0 +1,24 @@
from nullptr.analyzer import Point
def assign_sitter_at(c, s, w):
c.captain.init_mission(s, 'sit')
c.captain.smipa(s, 'dest', w.symbol)
def assign_sitter(c, s):
system = s.location.system
ships = c.store.all('Ship')
markets = c.store.all_members(system, 'Marketplace')
origin = Point(0, 0)
markets = sorted(markets, key=lambda m: m.waypoint.distance(origin))
shipyards = c.store.all_members(system, 'Shipyard')
occupied = [s.mission_state['dest'] for s in ships if s.mission=='sit']
probe_shipyard = [y for y in shipyards if 'SHIP_PROBE' in y.types][0]
if probe_shipyard.symbol not in occupied:
return assign_sitter_at(c, s, probe_shipyard)
for y in shipyards:
if y.symbol not in occupied:
return assign_sitter_at(c, s, y)
for m in markets:
if m.symbol not in occupied:
return assign_sitter_at(c, s, m)

10
nullptr/roles/surveyor.py Normal file
View File

@ -0,0 +1,10 @@
from nullptr.util import AppError
def assign_surveyor(c, s):
if s.crew is None:
raise AppError('ship has no crew')
w = s.crew.site
c.init_mission(s, 'survey')
c.smipa(s, 'site', w)

14
nullptr/roles/trader.py Normal file
View File

@ -0,0 +1,14 @@
from nullptr.analyzer import find_trade
def assign_trader(c, s):
t = find_trade(c, s.location.system)
if t is None:
print(f"No trade for {s} found. Idling")
c.captain.init_mission(s,'idle')
c.captain.smipa(s, 'seconds', 600)
return
s.log(f'assigning {s} to deliver {t.resource} from {t.source} to {t.dest} at a margin of {t.margin}')
c.captain.init_mission(s, 'trade')
c.captain.smipa(s, 'site', t.source)
c.captain.smipa(s, 'dest', t.dest)

View File

@ -9,6 +9,7 @@ import pickle
from struct import unpack, pack from struct import unpack, pack
from functools import partial from functools import partial
from io import BytesIO from io import BytesIO
from copy import copy
class StorePickler(pickle.Pickler): class StorePickler(pickle.Pickler):
def persistent_id(self, obj): def persistent_id(self, obj):
@ -24,9 +25,11 @@ class StoreUnpickler(pickle.Unpickler):
return self.store return self.store
raise pickle.UnpicklingError("I don know the persid!") raise pickle.UnpicklingError("I don know the persid!")
CHUNK_MAGIC = b'ChNkcHnK'
class ChunkHeader: class ChunkHeader:
def __init__(self): def __init__(self):
self.magic = CHUNK_MAGIC
self.offset = 0 self.offset = 0
self.in_use = True self.in_use = True
self.size = 0 self.size = 0
@ -35,14 +38,16 @@ class ChunkHeader:
@classmethod @classmethod
def parse(cls, fil): def parse(cls, fil):
offset = fil.tell() offset = fil.tell()
d = fil.read(16) d = fil.read(24)
if len(d) < 16: if len(d) < 24:
return None return None
o = cls() o = cls()
o.offset = offset o.offset = offset
d, o.used = unpack('<QQ', d) o.magic, d, o.used = unpack('<8sQQ', d)
o.size = d & 0x7fffffffffffffff o.size = d & 0x7fffffffffffffff
o.in_use = d & 0x8000000000000000 != 0 o.in_use = d & 0x8000000000000000 != 0
if o.magic != CHUNK_MAGIC:
raise ValueError(f"Invalid chunk magic: {o.magic}")
# print(o) # print(o)
return o return o
@ -50,15 +55,26 @@ class ChunkHeader:
d = self.size d = self.size
if self.in_use: if self.in_use:
d |= 1 << 63 d |= 1 << 63
d = pack('<QQ', d, self.used) d = pack('<8sQQ', self.magic, d, self.used)
f.write(d) f.write(d)
def __repr__(self): def __repr__(self):
return f'chunk {self.in_use} {self.size} {self.used}' return f'chunk {self.in_use} {self.size} {self.used}'
def f(self, detail=1):
if detail == 1:
return f'chunk {self.offset} {self.used}/{self.size}'
else:
r = f'Stored at: {self.offset}\n'
slack = self.size - self.used
r += f'Used: {self.used}/{self.size} (slack {slack})'
return r
class Store: class Store:
def __init__(self, data_file): def __init__(self, data_file, verbose=False):
self.init_models() self.init_models()
self.data_file = data_file
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 = {}
@ -68,8 +84,21 @@ class Store:
self.slack = 0.1 self.slack = 0.1
self.slack_min = 64 self.slack_min = 64
self.slack_max = 1024 self.slack_max = 1024
self.verbose = verbose
self.load() self.load()
def p(self, m):
if not self.verbose:
return
print(m)
def f(self, detail):
return f'Store {self.data_file}'
def close(self):
self.flush()
self.fil.close()
def init_models(self): def init_models(self):
self.models = all_subclasses(Base) self.models = all_subclasses(Base)
self.extensions = {c.ext(): c for c in self.models} self.extensions = {c.ext(): c for c in self.models}
@ -88,29 +117,39 @@ 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 x = self.get(type(obj), obj.symbol)
obj.disable_dirty = False if x is not None and x in self.dirty_objects:
self.dirty_objects.remove(obj)
obj._file_offset = offset
self.hold(obj) self.hold(obj)
def load(self): def load(self):
cnt = 0 cnt = 0
start_time = time() start_time = time()
total = 0
free = 0
self.fil.seek(0) self.fil.seek(0)
offset = 0 offset = 0
while (hdr := ChunkHeader.parse(self.fil)): while (hdr := ChunkHeader.parse(self.fil)):
# print(hdr) # self.p(hdr)
total += hdr.size
if not hdr.in_use: if not hdr.in_use:
# print(f"skip {hdr.size} {self.fil.tell()}")
self.fil.seek(hdr.size, 1) self.fil.seek(hdr.size, 1)
continue free += hdr.size
else:
data = self.fil.read(hdr.used) data = self.fil.read(hdr.used)
self.load_object(data, offset) self.load_object(data, offset)
# print(f"pad {hdr.size - hdr.used}")
self.fil.seek(hdr.size - hdr.used, 1) self.fil.seek(hdr.size - hdr.used, 1)
offset = self.fil.tell()
cnt += 1 cnt += 1
offset = self.fil.tell()
dur = time() - start_time dur = time() - start_time
print(f'loaded {cnt} objects in {dur:.2f} seconds') # just in case any temp objects were created
self.dirty_objects = set()
self.p(f'Loaded {cnt} objects in {dur:.2f} seconds')
self.p(f'Fragmented space: {free} / {total} bytes')
def allocate_chunk(self, sz): def allocate_chunk(self, sz):
used = sz used = sz
@ -123,17 +162,38 @@ 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 get_header(self, obj):
if obj._file_offset is None:
return None
self.fil.seek(obj._file_offset)
hdr = ChunkHeader.parse(self.fil)
return hdr
def purge(self, obj):
if obj._file_offset is not None:
self.fil.seek(obj._file_offset)
hdr = ChunkHeader.parse(self.fil)
hdr.in_use = False
self.fil.seek(obj._file_offset)
hdr.write(self.fil)
if type(obj) in self.data and obj.symbol in self.data[type(obj)]:
del self.data[type(obj)][obj.symbol]
self.remove_from_members(obj)
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
@ -141,26 +201,33 @@ 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)
self.fil.write(slack) self.fil.write(slack)
def remove_from_members(self, obj):
if type(obj).__name__ in ['Waypoint','Marketplace', 'Jumpgate', 'Survey']:
system_str = obj.system.symbol
if system_str not in self.system_members:
return
self.system_members[system_str].remove(obj)
def hold(self, obj): def hold(self, obj):
typ = type(obj) typ = type(obj)
symbol = obj.symbol symbol = obj.symbol
obj.store = self obj.store = self
self.data[typ][symbol] = obj self.data[typ][symbol] = obj
if hasattr(obj, 'system') and obj.system != None: if type(obj).__name__ in ['Waypoint','Marketplace', 'Jumpgate', 'Survey', 'Shipyard']:
system_str = obj.system.symbol system_str = obj.system.symbol
if system_str not in self.system_members: if system_str not in self.system_members:
self.system_members[system_str] = set() self.system_members[system_str] = set()
@ -168,6 +235,7 @@ class Store:
def create(self, typ, symbol): def create(self, typ, symbol):
obj = typ(symbol, self) obj = typ(symbol, self)
obj.created()
self.hold(obj) self.hold(obj)
self.dirty(obj) self.dirty(obj)
return obj return obj
@ -207,6 +275,9 @@ class Store:
typ = self.model_names[typ] typ = self.model_names[typ]
for m in self.data[typ].values(): for m in self.data[typ].values():
if m.is_expired():
self.dirty(m)
continue
yield m yield m
def all_members(self, system, typ=None): def all_members(self, system, typ=None):
@ -219,35 +290,57 @@ class Store:
if system not in self.system_members: if system not in self.system_members:
return return
garbage = set()
for m in self.system_members[system]: for m in self.system_members[system]:
if m.is_expired():
self.dirty(m)
garbage.add(m)
continue
if typ is None or type(m) == typ: if typ is None or type(m) == typ:
yield m yield m
for m in garbage:
self.system_members[system].remove(m)
def cleanup(self): def cleanup(self):
if time() < self.last_cleanup + self.cleanup_interval: self.last_cleanup = time()
return
start_time = time() start_time = time()
expired = list() expired = list()
for t in self.data: for t in self.data:
for o in self.all(t): for o in self.data[t].values():
if o.is_expired(): if o.is_expired():
expired.append(o) expired.append(o)
for o in expired: for o in expired:
self.purge(o)
# TODO
del self.data[type(o)][o.symbol]
dur = time() - start_time dur = time() - start_time
# print(f'cleaned {len(expired)} in {dur:.03f} seconds') # self.p(f'cleaned {len(expired)} in {dur:.03f} seconds')
def flush(self): def flush(self):
self.cleanup() self.cleanup()
it = 0 it = 0
start_time = time() start_time = time()
for obj in self.dirty_objects: for obj in copy(self.dirty_objects):
it += 1 it += 1
if obj.symbol not in self.data[type(obj)] or self.data[type(obj)][obj.symbol] != obj:
# print(f"Dirty object not in data {type(obj)} {obj.symbol} {obj}")
continue
self.store(obj) self.store(obj)
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):
self.flush()
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_file(nm)
for t in self.data:
for o in self.data[t].values():
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))

170
nullptr/test_store.py Normal file
View File

@ -0,0 +1,170 @@
import unittest
import tempfile
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):
self.count: int = 0
self.data: str = ""
def update(self, d):
self.seta('count', d)
@classmethod
def ext(self):
return 'dum'
def f(self, detail=1):
r = super().f(detail) + '.' + self.ext()
if detail >2:
r += f' c:{self.count}'
return r
class TestStore(unittest.TestCase):
def setUp(self):
self.store_file = tempfile.NamedTemporaryFile()
self.s = Store(self.store_file.name, False)
def tearDown(self):
self.s.close()
self.store_file.close()
def reopen(self):
self.s.flush()
self.s.close()
self.s = Store(self.store_file.name, False)
def test_single(self):
dum = self.s.get(Dummy, "5", create=True)
dum.count = 1337
dum.data = "A" * 1000
self.reopen()
dum = self.s.get(Dummy, "5")
self.assertEqual(1337, dum.count)
def test_grow(self):
dum = self.s.get(Dummy, "5", create=True)
dum.data = "A"
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):
dum = self.s.get(Dummy, "5", create=True)
dum.data = "A"
dum2 = self.s.get(Dummy, "7",create=True)
dum2.count = 1337
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)
def test_grow_last(self):
dum = self.s.get(Dummy, "5", create=True)
dum.data = "A"
dum2 = self.s.get(Dummy, "7",create=True)
self.reopen()
dum2 = self.s.get(Dummy, "7")
dum2.data = "A" * 1000
dum2.count = 1337
dum3 = self.s.get(Dummy, "9",create=True)
dum3.count = 1338
self.reopen()
dum2 = self.s.get(Dummy, "7")
self.assertEqual(1337, dum2.count)
dum3 = self.s.get(Dummy, "9")
self.assertEqual(1338, dum3.count)
def test_purge_last(self):
dum = self.s.get(Dummy, "5", create=True)
dum.data = "A"
dum2 = self.s.get(Dummy, "7",create=True)
self.reopen()
dum2 = self.s.get(Dummy, "7")
self.s.purge(dum2)
dum3 = self.s.get(Dummy, "9",create=True)
dum3.count = 1338
self.reopen()
dum2 = self.s.get(Dummy, "7")
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)

View File

@ -2,6 +2,10 @@ from datetime import datetime
from math import ceil from math import ceil
import os import os
from os.path import isfile, dirname from os.path import isfile, dirname
import traceback
class AppError(Exception):
pass
def open_file(fn): def open_file(fn):
d = dirname(fn) d = dirname(fn)
@ -82,3 +86,5 @@ def parse_timestamp(ts):
def render_timestamp(ts): def render_timestamp(ts):
return datetime.utcfromtimestamp(ts).isoformat() return datetime.utcfromtimestamp(ts).isoformat()
def fmtex(e):
return ''.join(traceback.TracebackException.from_exception(e).format())

View File

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

View File

@ -34,8 +34,8 @@ An index is a dict with a string as key and a list of objects as value. The dict
* store.load(fil) loads all objects * store.load(fil) loads all objects
* store.get(type, symbol, create=False) fetches the object. If create==False: None if it wasnt present * store.get(type, symbol, create=False) fetches the object. If create==False: None if it wasnt present
* store.all(type) generator for all objects of a goven type * store.all(type) generator for all objects of a goven type
* store.delete(typ, symbol) * store.purge(obj)
* store.cleanup() removes all expired objects * store.clean() removes all expired objects
* store.flush() writes all dirty objects to disk * store.flush() writes all dirty objects to disk
* store.defrag() consolidates the store file to minimize storage and loading time * store.defrag() consolidates the store file to minimize storage and loading time
@ -46,5 +46,18 @@ Until specified otherwise, all numbers are stored low-endian 64bit unsigned.
The store file is built up out of chunks. A chunk is either empty or houses exactly one file. If a file is updated and its size fits the chunk, it is updated in-place. If the new content does not fit the chunk, a new chunk is allocated at the end of the file. The old chunk is marked as empty. The store file is built up out of chunks. A chunk is either empty or houses exactly one file. If a file is updated and its size fits the chunk, it is updated in-place. If the new content does not fit the chunk, a new chunk is allocated at the end of the file. The old chunk is marked as empty.
A chunk starts with a chunk header. This is just a single field describing the size of the chunk in bytes, not including the header. The first bit of the field is the IN_USE flag. If it is not set, the contents of the chunk are ignored during loading. A chunk starts with a chunk header. The header consists of three 8-byte fields.
The first field is the magic. Its value is 'ChNkcHnK'. The magic can be used to recover from a corrupted file.
The second field is describing the size of the chunk in bytes, not including the header. The first bit of the field is the IN_USE flag. If it is not set, the contents of the chunk are ignored during loading.
The third field described how much of the chunk is occupied by content. This is typically less than the size of the chunk because we allocate slack for each object to grow. The slack prevents frequent reallocation.
# Future work
This format is far from perfect.
* file corruption sometimes occurs. The cause of this still has to be found
* Recovery of file corruption has not yet been implemented
* Diskspace improvements are possible by eliminating slack for non-changing objects such as waypoints and compressing the file
* Indices have not been implemented although a "member" index keeps track of which objects are in each system.