Compare commits
65 Commits
market-inf
...
integrated
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ddddd6fb1 | ||
|
|
e0f73f837b | ||
|
|
1f4a1a48de | ||
|
|
e5c384caa9 | ||
|
|
f644027750 | ||
|
|
537615e582 | ||
|
|
3d3ceeab91 | ||
|
|
00db50687a | ||
|
|
97296e1859 | ||
|
|
269b5cf537 | ||
|
|
ea34bcfab7 | ||
|
|
b2f2dc520e | ||
|
|
b1e3621490 | ||
|
|
6537db3c03 | ||
|
|
0553d9d6cc | ||
|
|
3010a8186d | ||
|
|
d6fe1cf183 | ||
|
|
bb64880822 | ||
|
|
9d124179bf | ||
|
|
9b9a149e3f | ||
|
|
9e6583ac24 | ||
|
|
6c98eec738 | ||
|
|
11031599cf | ||
|
|
7eea63ac82 | ||
|
|
dc862088cd | ||
|
|
35bc586b72 | ||
|
|
2a5680c16d | ||
|
|
4d51ad53c0 | ||
|
|
5fbce54285 | ||
|
|
27bd054e8b | ||
|
|
38a2ee7870 | ||
|
|
7c3eaa825f | ||
|
|
ddd693a66e | ||
|
|
b43568f476 | ||
|
|
ff4643d7ac | ||
|
|
0e3f939b9a | ||
|
|
2d792dffae | ||
|
|
4043c5585e | ||
|
|
b19e3ed2b2 | ||
|
|
b7d3347fac | ||
|
|
42e370fde5 | ||
|
|
b202b80541 | ||
|
|
b023718450 | ||
|
|
fbda97df61 | ||
|
|
707f142e7a | ||
|
|
35ea9e2e04 | ||
|
|
3a85c6c367 | ||
|
|
21f93f078d | ||
|
|
3a3e3b9da2 | ||
|
|
f849f871d2 | ||
|
|
9369c6982f | ||
|
|
8b29ca8f58 | ||
|
|
a229b9e300 | ||
|
|
46f9597e2e | ||
|
|
c2a1f787a2 | ||
|
|
9987481848 | ||
|
|
293b6d1c3e | ||
|
|
e31dce9861 | ||
|
|
2e45acc513 | ||
|
|
b5850bcb5f | ||
|
|
bb93950fa3 | ||
|
|
3f93d863a0 | ||
|
|
2c96cbb533 | ||
|
|
92a5a02180 | ||
|
|
ffd094df87 |
@@ -9,4 +9,4 @@ 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 [ "python3", "/app/main.py"]
|
||||||
CMD ["-s", "/data/"]
|
CMD ["-s", "/data/store.npt"]
|
||||||
|
|||||||
83
Readme.md
Normal file
83
Readme.md
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
# 0ptr script for spacetraders.io
|
||||||
|
This is my script for running spacetraders.io. It has been completely written and operated from an iphone.
|
||||||
|
|
||||||
|
Because i'm using a less-than-optimal development platform, some 'best-practice' code standards have been thrown out of the window. For instance, some variables have obscure names just to keep them short.
|
||||||
|
|
||||||
|
The script offers a command-line interface allowing you to do most ST operations by hand. More importantly, it offers the functionality to assign missions to ships and execute them automatically.
|
||||||
|
|
||||||
|
# Getting started
|
||||||
|
I'm using the [pythonista app](http://omz-software.com/pythonista/) to develop and test the script. Just open main.py and hit run.
|
||||||
|
|
||||||
|
To keep the script running for longer periods of time, I'm using a VPS with docker. Start the script like so:
|
||||||
|
|
||||||
|
`docker run -v 0ptr:/data -ti 0ptr`
|
||||||
|
|
||||||
|
The script will ask for an agent name. Make sure you choose a unique one. After hitting enter, you will be presented with the prompt: `>`
|
||||||
|
|
||||||
|
Now you'll want to register. You need to provide your faction:
|
||||||
|
|
||||||
|
`> register cosmic`
|
||||||
|
|
||||||
|
If all goes well, you should have an account now.
|
||||||
|
|
||||||
|
Next is building up our view of the universe: the atlas.
|
||||||
|
|
||||||
|
`> universe`
|
||||||
|
|
||||||
|
This command will iterate over all systems and obtain all waypoint information. **this will take several hours**. However, if you are impatient, you can stop the process after a couple of minutes by hitting enter. It should at least have indexed your local system to continue.
|
||||||
|
|
||||||
|
Now, let's see what ships we own.
|
||||||
|
|
||||||
|
`> ships refresh`
|
||||||
|
|
||||||
|
This will populate our internal cache of present ships and their state. After this, the state is updated continually by each command that alters it. You should be able to omit the 'refresh' after this.
|
||||||
|
|
||||||
|
We should also populate the cache of contracts:
|
||||||
|
|
||||||
|
`> contracts refresh`
|
||||||
|
|
||||||
|
Same here: you can omit the 'refresh' from now on and view your contracts with the bare `contracts` command without sending a http request.
|
||||||
|
|
||||||
|
You are now ready to begin operations.
|
||||||
|
|
||||||
|
# Operations
|
||||||
|
To get a ship to do something, you first have to select it:
|
||||||
|
|
||||||
|
```
|
||||||
|
> ship 1
|
||||||
|
KILO-1 DOCKED [1035/1200] X1-YU85-03282C
|
||||||
|
KILO-1>
|
||||||
|
```
|
||||||
|
|
||||||
|
This prints the current state of the ship. Notice that the prompt now includes the name of the ship. Any command you now give will be executed by that ship.
|
||||||
|
|
||||||
|
A quick, non-complete list of commands:
|
||||||
|
|
||||||
|
* go: intra-system travel to another waypoint
|
||||||
|
* orbit
|
||||||
|
* dock
|
||||||
|
* market: show local market data
|
||||||
|
* shipyard: show local shipyard data
|
||||||
|
* jumps: show local jumpgate data
|
||||||
|
* mission: set ships mission type
|
||||||
|
* mine: mine and deliver a resource
|
||||||
|
* probe: find resource prices in nearby markets
|
||||||
|
* survey: generate surveys in a loop
|
||||||
|
* travel: travel to any waypoint in the universe using jumpgates (assuming a route is found)
|
||||||
|
* haul: buy goods at a site and bring them to a destination
|
||||||
|
* mset: set a mission parameter
|
||||||
|
* purchase: purchase a ship at a shipyard
|
||||||
|
* buy: buy a resource at the local market
|
||||||
|
* sell: sell a resource at the local market
|
||||||
|
* jump: jump to a remote system using the local jumpgate
|
||||||
|
* cmine: configures a mining mission for the current contract
|
||||||
|
* chaul configures a hauling mission for the current contract
|
||||||
|
* cprobe: configures a probing mission for the current contract
|
||||||
|
* query: lists nearest markets and buy prices (if known) of a resource
|
||||||
|
|
||||||
|
Look in commander.py for more available commands.
|
||||||
|
|
||||||
|
# Data storage
|
||||||
|
The script stores each object in a separate json file in the data directory. The default location for this directory is in your workingdir. The docker image places the data dir in '/data'
|
||||||
|
|
||||||
|
If you want to re-register an agent, or there is a sever reset, just delete all data in that directory.
|
||||||
8
main.py
Normal file → Executable file
8
main.py
Normal file → Executable file
@@ -1,8 +1,9 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
import argparse
|
import argparse
|
||||||
from nullptr.commander import Commander
|
from nullptr.commander import Commander
|
||||||
|
from nullptr.models.base import Base
|
||||||
def main(args):
|
def main(args):
|
||||||
c = Commander(args.store_dir, args.agent)
|
c = Commander(args.store_file)
|
||||||
c.run()
|
c.run()
|
||||||
|
|
||||||
# X1-AG74-41076A
|
# X1-AG74-41076A
|
||||||
@@ -10,7 +11,6 @@ def main(args):
|
|||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument('-s', '--store-dir', default='data')
|
parser.add_argument('-s', '--store-file', default='data/store.npt')
|
||||||
parser.add_argument('-a', '--agent', default=None)
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
main(args)
|
main(args)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
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.system import System
|
from nullptr.models.system import System
|
||||||
|
from nullptr.models.waypoint import Waypoint
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -29,15 +30,43 @@ class Analyzer:
|
|||||||
|
|
||||||
def find_markets(self, resource, sellbuy):
|
def find_markets(self, resource, sellbuy):
|
||||||
for m in self.store.all(Marketplace):
|
for m in self.store.all(Marketplace):
|
||||||
resources = m.imports if sellbuy == 'sell' else m.exports
|
if 'sell' in sellbuy and resource in m.imports:
|
||||||
if resource in resources:
|
yield ('sell', m)
|
||||||
yield m
|
|
||||||
|
elif 'buy' in sellbuy and resource in m.exports:
|
||||||
|
yield ('buy', m)
|
||||||
|
|
||||||
|
elif 'exchange' in sellbuy and resource in m.exchange:
|
||||||
|
yield ('exchange', m)
|
||||||
|
|
||||||
|
def find_closest_markets(self, resource, sellbuy, location):
|
||||||
|
if type(location) == str:
|
||||||
|
location = self.store.get(Waypoint, location)
|
||||||
|
mkts = self.find_markets(resource, sellbuy)
|
||||||
|
candidates = []
|
||||||
|
origin = location.system
|
||||||
|
for typ, m in mkts:
|
||||||
|
system = m.waypoint.system
|
||||||
|
d = origin.distance(system)
|
||||||
|
candidates.append((typ, m, d))
|
||||||
|
possibles = sorted(candidates, key=lambda m: m[2])
|
||||||
|
possibles = possibles[:10]
|
||||||
|
results = []
|
||||||
|
for typ,m,d in possibles:
|
||||||
|
system = m.waypoint.system
|
||||||
|
p = self.find_path(origin, system)
|
||||||
|
if p is None: continue
|
||||||
|
results.append((typ,m,d,len(p)))
|
||||||
|
return results
|
||||||
|
|
||||||
|
def solve_tsp(self, waypoints):
|
||||||
|
# todo actually try to solve it
|
||||||
|
return waypoints
|
||||||
|
|
||||||
def get_jumpgate(self, system):
|
def get_jumpgate(self, system):
|
||||||
gates = self.store.all_members(system, Jumpgate)
|
gates = self.store.all_members(system, Jumpgate)
|
||||||
return next(gates, None)
|
return next(gates, None)
|
||||||
|
|
||||||
|
|
||||||
def find_path(self, orig, to, depth=100, seen=None):
|
def find_path(self, orig, to, depth=100, seen=None):
|
||||||
if depth < 1: return None
|
if depth < 1: return None
|
||||||
if seen is None:
|
if seen is None:
|
||||||
@@ -54,9 +83,7 @@ class Analyzer:
|
|||||||
for s in jg.systems:
|
for s in jg.systems:
|
||||||
if s in seen: continue
|
if s in seen: continue
|
||||||
seen.add(s)
|
seen.add(s)
|
||||||
system = self.store.get(System, s)
|
dest.add(SearchNode(s, o))
|
||||||
if system is None: continue
|
|
||||||
dest.add(SearchNode(system, o))
|
|
||||||
if len(dest) == 0:
|
if len(dest) == 0:
|
||||||
return None
|
return None
|
||||||
return self.find_path(dest, to, depth-1, seen)
|
return self.find_path(dest, to, depth-1, seen)
|
||||||
|
|||||||
179
nullptr/api.py
179
nullptr/api.py
@@ -3,6 +3,7 @@ from nullptr.models.system import System
|
|||||||
from nullptr.models.waypoint import Waypoint
|
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 .util import *
|
from .util import *
|
||||||
from time import sleep
|
from time import sleep
|
||||||
class ApiError(Exception):
|
class ApiError(Exception):
|
||||||
@@ -17,7 +18,9 @@ class Api:
|
|||||||
def __init__(self, store, agent):
|
def __init__(self, store, agent):
|
||||||
self.agent = agent
|
self.agent = agent
|
||||||
self.store = store
|
self.store = store
|
||||||
self.meta = None
|
self.requests_sent = 0
|
||||||
|
self.last_meta = None
|
||||||
|
self.last_result = None
|
||||||
self.root = 'https://api.spacetraders.io/v2/'
|
self.root = 'https://api.spacetraders.io/v2/'
|
||||||
|
|
||||||
def token(self):
|
def token(self):
|
||||||
@@ -28,7 +31,7 @@ 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)
|
return self.request_once(method, path, data, need_token, params)
|
||||||
except ApiLimitError:
|
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)
|
||||||
@@ -37,6 +40,7 @@ class Api:
|
|||||||
headers = {}
|
headers = {}
|
||||||
if need_token:
|
if need_token:
|
||||||
headers['Authorization'] = 'Bearer ' + self.token()
|
headers['Authorization'] = 'Bearer ' + self.token()
|
||||||
|
self.requests_sent += 1
|
||||||
if method == 'get':
|
if method == 'get':
|
||||||
params['limit'] = 20
|
params['limit'] = 20
|
||||||
r = requests.request(method, self.root+path, json=data, headers=headers, params=params)
|
r = requests.request(method, self.root+path, json=data, headers=headers, params=params)
|
||||||
@@ -65,6 +69,7 @@ class Api:
|
|||||||
}
|
}
|
||||||
result = self.request('post', 'register', data, need_token=False)
|
result = self.request('post', 'register', data, need_token=False)
|
||||||
token = mg(result, 'token')
|
token = mg(result, 'token')
|
||||||
|
self.agent.update(mg(result, 'agent'))
|
||||||
self.agent.token = token
|
self.agent.token = token
|
||||||
|
|
||||||
def info(self):
|
def info(self):
|
||||||
@@ -75,7 +80,10 @@ class Api:
|
|||||||
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)
|
||||||
return self.store.update_list(System, data)
|
systems = self.store.update_list(System, data)
|
||||||
|
for s in data:
|
||||||
|
self.store.update_list(Waypoint, mg(s, 'waypoints'))
|
||||||
|
return systems
|
||||||
|
|
||||||
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/')
|
||||||
@@ -83,14 +91,169 @@ class Api:
|
|||||||
return self.store.update_list(Waypoint, data)
|
return self.store.update_list(Waypoint, data)
|
||||||
|
|
||||||
def marketplace(self, waypoint):
|
def marketplace(self, waypoint):
|
||||||
system = waypoint.system()
|
system = waypoint.system
|
||||||
symbol = str(waypoint)
|
|
||||||
data = self.request('get', f'systems/{system}/waypoints/{waypoint}/market')
|
data = self.request('get', f'systems/{system}/waypoints/{waypoint}/market')
|
||||||
return self.store.update(Marketplace, symbol, data)
|
return self.store.update(Marketplace, data)
|
||||||
|
|
||||||
def jumps(self, waypoint):
|
def jumps(self, waypoint):
|
||||||
data = self.request('get', f'systems/{waypoint.system()}/waypoints/{waypoint}/jump-gate')
|
data = self.request('get', f'systems/{waypoint.system}/waypoints/{waypoint}/jump-gate')
|
||||||
symbol = str(waypoint)
|
symbol = str(waypoint)
|
||||||
return self.store.update(Jumpgate, symbol, data)
|
return self.store.update(Jumpgate, data, symbol)
|
||||||
|
|
||||||
|
def list_ships(self):
|
||||||
|
data = self.request('get', 'my/ships')
|
||||||
|
return self.store.update_list(Ship, data)
|
||||||
|
|
||||||
|
def list_contracts(self):
|
||||||
|
data = self.request('get', 'my/contracts')
|
||||||
|
return self.store.update_list('Contract', data)
|
||||||
|
|
||||||
|
def negotiate(self, ship):
|
||||||
|
data = self.request('post', f'my/ships/{ship}/negotiate/contract')
|
||||||
|
if data is not None and 'contract' in data:
|
||||||
|
contract = self.store.update('Contract', data['contract'])
|
||||||
|
return contract
|
||||||
|
|
||||||
|
def deliver(self, ship, typ, contract):
|
||||||
|
units = ship.get_cargo(typ)
|
||||||
|
if units == 0:
|
||||||
|
print("Resource not in cargo")
|
||||||
|
return {}
|
||||||
|
data = {
|
||||||
|
'shipSymbol': str(ship),
|
||||||
|
'tradeSymbol': typ.upper(),
|
||||||
|
'units': units
|
||||||
|
}
|
||||||
|
data = self.request('post', f'my/contracts/{contract.symbol.lower()}/deliver', data)
|
||||||
|
if 'cargo' in data:
|
||||||
|
ship.update(data)
|
||||||
|
if 'contract' in data:
|
||||||
|
contract.update(data['contract'])
|
||||||
|
return contract
|
||||||
|
|
||||||
|
def fulfill(self, contract):
|
||||||
|
data = self.request('post', f'my/contracts/{contract.symbol.lower()}/fulfill')
|
||||||
|
if 'contract' in data:
|
||||||
|
contract.update(data['contract'])
|
||||||
|
if 'agent' in data:
|
||||||
|
self.agent.update(data['agent'])
|
||||||
|
return contract
|
||||||
|
|
||||||
|
def navigate(self, ship, wp):
|
||||||
|
data = {'waypointSymbol': str(wp)}
|
||||||
|
response = self.request('post', f'my/ships/{ship}/navigate', data)
|
||||||
|
ship.update(response)
|
||||||
|
|
||||||
|
def dock(self, ship):
|
||||||
|
data = self.request('post', f'my/ships/{ship}/dock')
|
||||||
|
ship.update(data)
|
||||||
|
return data
|
||||||
|
|
||||||
|
def orbit(self, ship):
|
||||||
|
data = self.request('post', f'my/ships/{ship}/orbit')
|
||||||
|
ship.update(data)
|
||||||
|
return data
|
||||||
|
|
||||||
|
def refuel(self, ship):
|
||||||
|
data = self.request('post', f'my/ships/{ship}/refuel')
|
||||||
|
if 'fuel' in data:
|
||||||
|
ship.update(data)
|
||||||
|
if 'agent' in data:
|
||||||
|
self.agent.update(data['agent'])
|
||||||
|
return data
|
||||||
|
|
||||||
|
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 sell(self, ship, typ):
|
||||||
|
units = ship.get_cargo(typ)
|
||||||
|
data = {
|
||||||
|
'symbol': typ,
|
||||||
|
'units': units
|
||||||
|
}
|
||||||
|
data = self.request('post', f'my/ships/{ship}/sell', data)
|
||||||
|
if 'cargo' in data:
|
||||||
|
ship.update(data)
|
||||||
|
if 'agent' in data:
|
||||||
|
self.agent.update(data['agent'])
|
||||||
|
return data
|
||||||
|
|
||||||
|
def buy(self, ship, typ, amt):
|
||||||
|
data = {
|
||||||
|
'symbol': typ,
|
||||||
|
'units': amt
|
||||||
|
}
|
||||||
|
data = self.request('post', f'my/ships/{ship}/purchase', data)
|
||||||
|
if 'cargo' in data:
|
||||||
|
ship.update(data)
|
||||||
|
if 'agent' in data:
|
||||||
|
self.agent.update(data['agent'])
|
||||||
|
return data
|
||||||
|
|
||||||
|
def jettison(self, ship, typ):
|
||||||
|
units = ship.get_cargo(typ)
|
||||||
|
if units == 0:
|
||||||
|
print('cargo not found')
|
||||||
|
return
|
||||||
|
data = {
|
||||||
|
'symbol': typ,
|
||||||
|
'units': units
|
||||||
|
}
|
||||||
|
data = self.request('post', f'my/ships/{ship.symbol}/jettison', data)
|
||||||
|
if 'cargo' in data:
|
||||||
|
ship.update(data)
|
||||||
|
if 'agent' in data:
|
||||||
|
self.agent.update(data['agent'])
|
||||||
|
return data
|
||||||
|
|
||||||
|
def purchase(self, typ, wp):
|
||||||
|
data = {
|
||||||
|
'shipType': typ,
|
||||||
|
'waypointSymbol': str(wp)
|
||||||
|
}
|
||||||
|
data = self.request('post', 'my/ships', data)
|
||||||
|
if 'agent' in data:
|
||||||
|
self.agent.update(data['agent'])
|
||||||
|
if 'ship' in data:
|
||||||
|
ship = self.store.update('Ship', data['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
|
||||||
|
|
||||||
|
|||||||
@@ -1,66 +1,71 @@
|
|||||||
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:
|
first_page = self.atlas.total_pages == 0
|
||||||
input()
|
pages_left = self.atlas.total_pages < self.atlas.seen_pages
|
||||||
except EOFError:
|
if first_page or pages_left:
|
||||||
pass
|
self.sched(self.get_systems)
|
||||||
self.stop_auto = True
|
return
|
||||||
print('stopping...')
|
for s in self.store.all(System):
|
||||||
|
if len(self.work) > self.max_work:
|
||||||
def run(self, page=1):
|
|
||||||
print('universe mode. hit enter to stop')
|
|
||||||
t = Thread(target=self.wait_for_stop)
|
|
||||||
t.daemon = True
|
|
||||||
t.start()
|
|
||||||
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
|
||||||
|
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:
|
||||||
self.api.marketplace(w)
|
# print(f'marketplace at {w}')
|
||||||
print(f'marketplace at {w}')
|
self.sched(self.api.marketplace, w)
|
||||||
sleep(0.5)
|
|
||||||
if w.type == 'JUMP_GATE':
|
if w.type == 'JUMP_GATE':
|
||||||
self.api.jumps(w)
|
# print(f'jumpgate at {w}')
|
||||||
print(f'jumpgate at {w}')
|
self.sched(self.api.jumps, w)
|
||||||
|
if 'SHIPYARD' in w.traits:
|
||||||
def all_waypoints(self, systems):
|
# todo
|
||||||
for s in systems:
|
pass
|
||||||
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()
|
|
||||||
|
|||||||
122
nullptr/central_command.py
Normal file
122
nullptr/central_command.py
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
from nullptr.store import Store
|
||||||
|
from nullptr.models.ship import Ship
|
||||||
|
from nullptr.missions import create_mission, get_mission_class
|
||||||
|
from random import choice
|
||||||
|
from time import sleep
|
||||||
|
from threading import Thread
|
||||||
|
from nullptr.atlas_builder import AtlasBuilder
|
||||||
|
|
||||||
|
class CentralCommandError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class CentralCommand:
|
||||||
|
def __init__(self, store, api):
|
||||||
|
self.missions = {}
|
||||||
|
self.stopping = False
|
||||||
|
self.store = store
|
||||||
|
self.api = api
|
||||||
|
self.atlas_builder = AtlasBuilder(store, api)
|
||||||
|
self.update_missions()
|
||||||
|
|
||||||
|
def get_ready_missions(self):
|
||||||
|
result = []
|
||||||
|
for ship, mission in self.missions.items():
|
||||||
|
if mission.is_ready():
|
||||||
|
result.append(ship)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def tick(self):
|
||||||
|
missions = self.get_ready_missions()
|
||||||
|
if len(missions) == 0: return False
|
||||||
|
ship = choice(missions)
|
||||||
|
mission = self.missions[ship]
|
||||||
|
mission.step()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def run_interactive(self):
|
||||||
|
print('auto mode. hit enter to stop')
|
||||||
|
t = Thread(target=self.wait_for_stop)
|
||||||
|
t.daemon = True
|
||||||
|
t.start()
|
||||||
|
self.run()
|
||||||
|
print('manual mode')
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_stop(self):
|
||||||
|
try:
|
||||||
|
input()
|
||||||
|
except EOFError:
|
||||||
|
pass
|
||||||
|
self.stopping = True
|
||||||
|
print('stopping...')
|
||||||
|
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
self.update_missions()
|
||||||
|
while not self.stopping:
|
||||||
|
did_step = True
|
||||||
|
request_counter = self.api.requests_sent
|
||||||
|
while request_counter == self.api.requests_sent and did_step:
|
||||||
|
did_step = self.tick()
|
||||||
|
if request_counter == self.api.requests_sent:
|
||||||
|
self.atlas_builder.do_work()
|
||||||
|
|
||||||
|
self.store.flush()
|
||||||
|
sleep(0.5)
|
||||||
|
self.stopping = False
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.stopping = True
|
||||||
|
|
||||||
|
def set_mission_param(self, ship, nm, val):
|
||||||
|
if ship not in self.missions:
|
||||||
|
print('set a mission for this ship first')
|
||||||
|
return
|
||||||
|
mission = self.missions[ship]
|
||||||
|
params = mission.params()
|
||||||
|
if not nm in params:
|
||||||
|
print(f'{nm} is not a valid param')
|
||||||
|
return
|
||||||
|
param = params[nm]
|
||||||
|
try:
|
||||||
|
parsed_val = param.parse(val, self.store)
|
||||||
|
except ValueError as e:
|
||||||
|
raise MissionError(e)
|
||||||
|
return
|
||||||
|
ship.set_mission_state(nm, parsed_val)
|
||||||
|
|
||||||
|
def update_missions(self):
|
||||||
|
for s in self.store.all(Ship):
|
||||||
|
if s.mission is None:
|
||||||
|
if s in self.missions:
|
||||||
|
self.stop_mission(s)
|
||||||
|
elif s not in self.missions:
|
||||||
|
self.start_mission(s)
|
||||||
|
if s in self.missions:
|
||||||
|
m = self.missions[s]
|
||||||
|
m.next_step = max(s.cooldown, s.arrival)
|
||||||
|
|
||||||
|
def init_mission(self, s, mtyp):
|
||||||
|
if mtyp == 'none':
|
||||||
|
s.mission_state = {}
|
||||||
|
s.mission_status = None
|
||||||
|
s.mission = None
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
mclass = get_mission_class(mtyp)
|
||||||
|
except ValueError:
|
||||||
|
raise CentralCommandError('no such mission')
|
||||||
|
s.mission = mtyp
|
||||||
|
s.mission_status = 'init'
|
||||||
|
s.mission_state = {k: v.default for k,v in mclass.params().items()}
|
||||||
|
self.start_mission(s)
|
||||||
|
|
||||||
|
def start_mission(self, s):
|
||||||
|
mtype = s.mission
|
||||||
|
m = create_mission(mtype, s, self.store, self.api)
|
||||||
|
self.missions[s] = m
|
||||||
|
return m
|
||||||
|
|
||||||
|
def stop_mission(self, s):
|
||||||
|
if s in self.missions:
|
||||||
|
del self.missions[s]
|
||||||
@@ -41,7 +41,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=str(type(e))!='ApiErrorp')
|
logging.error(e, exc_info=type(e).__name__ not in ['ApiError','CommandError', 'CentralCommandError'])
|
||||||
|
|
||||||
def handle_empty(self):
|
def handle_empty(self):
|
||||||
pass
|
pass
|
||||||
@@ -90,5 +90,8 @@ class CommandLine:
|
|||||||
except EOFError:
|
except EOFError:
|
||||||
self.handle_eof()
|
self.handle_eof()
|
||||||
break
|
break
|
||||||
self.handle_cmd(c)
|
try:
|
||||||
|
self.handle_cmd(c)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(e, exc_info=True)
|
||||||
|
|
||||||
|
|||||||
@@ -2,90 +2,256 @@ 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 Analyzer
|
||||||
import argparse
|
import argparse
|
||||||
from nullptr.models.agent import Agent
|
from nullptr.models import *
|
||||||
from nullptr.models.system import System
|
|
||||||
from nullptr.models.waypoint import Waypoint
|
|
||||||
from nullptr.models.marketplace import Marketplace
|
|
||||||
from nullptr.models.jumpgate import Jumpgate
|
|
||||||
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.central_command import CentralCommand
|
||||||
|
|
||||||
|
class CommandError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
class Commander(CommandLine):
|
class Commander(CommandLine):
|
||||||
def __init__(self, store_dir='data', agent=None):
|
def __init__(self, store_file='data/store.npt'):
|
||||||
self.store_dir = store_dir
|
self.store = Store(store_file)
|
||||||
self.store = Store(store_dir)
|
self.agent = self.select_agent()
|
||||||
self.store.load()
|
|
||||||
self.agent = self.select_agent(agent)
|
|
||||||
self.api = Api(self.store, self.agent)
|
self.api = Api(self.store, self.agent)
|
||||||
self.atlas_builder = AtlasBuilder(self.store, self.api)
|
self.centcom = CentralCommand(self.store, self.api)
|
||||||
self.analyzer = Analyzer(self.store)
|
self.analyzer = Analyzer(self.store)
|
||||||
|
self.ship = None
|
||||||
|
|
||||||
self.stop_auto= False
|
self.stop_auto= False
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
|
def prompt(self):
|
||||||
|
if self.ship:
|
||||||
|
return f'{self.ship.symbol}> '
|
||||||
|
else:
|
||||||
|
return '> '
|
||||||
|
|
||||||
|
def has_ship(self):
|
||||||
|
if self.ship is not None:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print('set a ship')
|
||||||
|
|
||||||
def ask_obj(self, typ, prompt):
|
def ask_obj(self, typ, prompt):
|
||||||
obj = None
|
obj = None
|
||||||
while obj is None:
|
while obj is None:
|
||||||
symbol = input(prompt)
|
symbol = input(prompt).strip()
|
||||||
obj = self.store.get(typ, symbol.upper())
|
obj = self.store.get(typ, symbol.upper())
|
||||||
if obj is None:
|
if obj is None:
|
||||||
print('not found')
|
print('not found')
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
def select_agent(self, agent_str):
|
def select_agent(self):
|
||||||
if agent_str is not None:
|
agents = self.store.all(Agent)
|
||||||
return self.store.get(Agent, agent_str)
|
agent = next(agents, None)
|
||||||
|
if agent is None:
|
||||||
|
agent = self.agent_setup()
|
||||||
|
return agent
|
||||||
|
|
||||||
|
def agent_setup(self):
|
||||||
|
symbol = input('agent name: ')
|
||||||
|
agent = self.store.get(Agent, symbol, create=True)
|
||||||
|
self.agent = agent
|
||||||
|
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')
|
||||||
|
ship = self.store.get(Ship, symbol.upper() + '-2')
|
||||||
|
api.list_waypoints(ship.location.system)
|
||||||
|
self.store.flush()
|
||||||
|
return agent
|
||||||
|
|
||||||
|
def resolve(self, typ, arg):
|
||||||
|
arg = arg.upper()
|
||||||
|
matches = [c for c in self.store.all(typ) if c.symbol.startswith(arg)]
|
||||||
|
if len(matches) == 1:
|
||||||
|
return matches[0]
|
||||||
|
elif len(matches) > 1:
|
||||||
|
raise CommandError('multiple matches')
|
||||||
else:
|
else:
|
||||||
agents = self.store.all(Agent)
|
raise CommandError('not found')
|
||||||
agent = next(agents, None)
|
|
||||||
if agent is None:
|
|
||||||
symbol = input('agent name: ')
|
|
||||||
agent = self.store.get(Agent, symbol)
|
|
||||||
return agent
|
|
||||||
|
|
||||||
def after_cmd(self):
|
def after_cmd(self):
|
||||||
self.store.flush()
|
self.store.flush()
|
||||||
|
|
||||||
def do_info(self):
|
def do_info(self, arg=''):
|
||||||
pprint(self.api.info(), 100)
|
if arg.startswith('r'):
|
||||||
|
self.api.info()
|
||||||
|
|
||||||
|
pprint(self.agent, 100)
|
||||||
|
|
||||||
|
def do_auto(self):
|
||||||
|
self.centcom.run_interactive()
|
||||||
|
|
||||||
|
def print_mission(self):
|
||||||
|
print(f'mission: {self.ship.mission} ({self.ship.mission_status})')
|
||||||
|
pprint(self.ship.mission_state)
|
||||||
|
|
||||||
|
def do_mission(self, arg=''):
|
||||||
|
if not self.has_ship(): return
|
||||||
|
if arg:
|
||||||
|
self.centcom.init_mission(self.ship, arg)
|
||||||
|
self.print_mission()
|
||||||
|
|
||||||
|
def do_mreset(self):
|
||||||
|
if not self.has_ship(): return
|
||||||
|
self.ship.mission_state = {}
|
||||||
|
|
||||||
|
def do_mset(self, nm, val):
|
||||||
|
if not self.has_ship(): return
|
||||||
|
self.centcom.set_mission_param(self.ship, nm, val)
|
||||||
|
|
||||||
|
def active_contract(self):
|
||||||
|
for c in self.store.all('Contract'):
|
||||||
|
if c.accepted and not c.fulfilled: return c
|
||||||
|
raise CommandError('no active contract')
|
||||||
|
|
||||||
|
def do_cmine(self):
|
||||||
|
if not self.has_ship(): return
|
||||||
|
site = self.ship.location
|
||||||
|
contract = self.active_contract()
|
||||||
|
delivery = contract.unfinished_delivery()
|
||||||
|
if delivery is None:
|
||||||
|
raise CommandError('no delivery')
|
||||||
|
resource = delivery['trade_symbol']
|
||||||
|
destination = delivery['destination']
|
||||||
|
self.centcom.init_mission(self.ship, 'mine')
|
||||||
|
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):
|
||||||
|
if not self.has_ship(): return
|
||||||
|
if len(self.ship.cargo) > 0:
|
||||||
|
raise CommandError('please dump cargo first')
|
||||||
|
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', 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()
|
||||||
|
|
||||||
|
def do_travel(self, dest):
|
||||||
|
dest = self.resolve('Waypoint', dest)
|
||||||
|
self.centcom.init_mission(self.ship, 'travel')
|
||||||
|
self.centcom.set_mission_param(self.ship, 'dest', dest)
|
||||||
|
self.print_mission()
|
||||||
|
|
||||||
def do_register(self, faction):
|
def do_register(self, faction):
|
||||||
self.api.register(faction.upper())
|
self.api.register(faction.upper())
|
||||||
|
pprint(self.api.agent)
|
||||||
def do_universe(self, page=1):
|
|
||||||
self.atlas_builder.run(page)
|
|
||||||
|
|
||||||
def do_systems(self, page=1):
|
def do_systems(self, page=1):
|
||||||
r = self.api.list_systems(int(page))
|
r = self.api.list_systems(int(page))
|
||||||
pprint(self.api.last_meta)
|
pprint(self.api.last_meta)
|
||||||
|
|
||||||
def do_waypoints(self, system_str):
|
def do_stats(self):
|
||||||
system = self.store.get(System, system_str.upper())
|
total = 0
|
||||||
|
for t in self.store.data:
|
||||||
|
num = len(self.store.data[t])
|
||||||
|
nam = t.__name__
|
||||||
|
total += num
|
||||||
|
print(f'{num:5d} {nam}')
|
||||||
|
print(f'{total:5d} total')
|
||||||
|
|
||||||
|
def do_defrag(self):
|
||||||
|
self.store.defrag()
|
||||||
|
|
||||||
|
def do_system(self, system_str):
|
||||||
|
system = self.store.get(System, system_str)
|
||||||
r = self.api.list_waypoints(system)
|
r = self.api.list_waypoints(system)
|
||||||
pprint(r)
|
pprint(r)
|
||||||
|
|
||||||
|
def do_waypoints(self, system_str=''):
|
||||||
|
if system_str == '':
|
||||||
|
if not self.has_ship(): return
|
||||||
|
system = self.ship.location.system
|
||||||
|
else:
|
||||||
|
system = self.store.get(System, system_str)
|
||||||
|
print(f'=== waypoints in {system}')
|
||||||
|
r = self.store.all_members(system, 'Waypoint')
|
||||||
|
for w in r:
|
||||||
|
traits = []
|
||||||
|
if '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):
|
def do_marketplace(self, waypoint_str):
|
||||||
waypoint = self.store.get(Waypoint, waypoint_str.upper())
|
waypoint = self.store.get(Waypoint, waypoint_str.upper())
|
||||||
r = self.api.marketplace(waypoint)
|
r = self.api.marketplace(waypoint)
|
||||||
|
|
||||||
def do_jumps(self, waypoint_str):
|
def do_jumps(self, waypoint_str=None):
|
||||||
waypoint = self.store.get(Waypoint, waypoint_str.upper())
|
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)
|
r = self.api.jumps(waypoint)
|
||||||
pprint(r)
|
pprint(r)
|
||||||
|
|
||||||
def do_query(self):
|
def do_query(self, resource):
|
||||||
location = self.ask_obj(System, 'Where are you? ')
|
if not self.has_ship(): return
|
||||||
resource = input('what resource?').upper()
|
location = self.ship.location
|
||||||
sellbuy = self.ask_multichoice(['sell','buy'], 'do you want to sell or buy?')
|
resource = resource.upper()
|
||||||
print('Found markets:')
|
print('Found markets:')
|
||||||
for m in self.analyzer.find_markets(resource, sellbuy):
|
for typ, m, d, plen in self.analyzer.find_closest_markets(resource, 'buy,exchange',location):
|
||||||
system = self.store.get(System, m.system())
|
price = '?'
|
||||||
p = self.analyzer.find_path(location, system)
|
if resource in m.prices:
|
||||||
if p is None: continue
|
price = m.prices[resource]['buy']
|
||||||
print(m, f'{len(p)-1} hops')
|
print(m, typ[0], f'{plen-1:3} hops {price}')
|
||||||
|
|
||||||
def do_path(self):
|
def do_path(self):
|
||||||
orig = self.ask_obj(System, 'from: ')
|
orig = self.ask_obj(System, 'from: ')
|
||||||
@@ -94,3 +260,154 @@ class Commander(CommandLine):
|
|||||||
# dest = self.store.get(System, 'X1-DA90')
|
# dest = self.store.get(System, 'X1-DA90')
|
||||||
path = self.analyzer.find_path(orig, dest)
|
path = self.analyzer.find_path(orig, dest)
|
||||||
pprint(path)
|
pprint(path)
|
||||||
|
|
||||||
|
def do_ships(self, arg=''):
|
||||||
|
if arg.startswith('r'):
|
||||||
|
r = self.api.list_ships()
|
||||||
|
else:
|
||||||
|
r = list(self.store.all('Ship'))
|
||||||
|
pprint(r)
|
||||||
|
|
||||||
|
def do_contracts(self, arg=''):
|
||||||
|
if arg.startswith('r'):
|
||||||
|
r = self.api.list_contracts()
|
||||||
|
else:
|
||||||
|
r = list(self.store.all('Contract'))
|
||||||
|
pprint(r)
|
||||||
|
|
||||||
|
def do_deliver(self):
|
||||||
|
if not self.has_ship(): return
|
||||||
|
site = self.ship.location
|
||||||
|
contract = self.active_contract()
|
||||||
|
delivery = contract.unfinished_delivery()
|
||||||
|
if delivery is None:
|
||||||
|
raise CommandError('no delivery')
|
||||||
|
resource = delivery['trade_symbol']
|
||||||
|
self.api.deliver(self.ship, resource, contract)
|
||||||
|
pprint(contract)
|
||||||
|
|
||||||
|
def do_fulfill(self):
|
||||||
|
contract = self.active_contract()
|
||||||
|
self.api.fulfill(contract)
|
||||||
|
|
||||||
|
def do_ship(self, arg=''):
|
||||||
|
if arg != '':
|
||||||
|
symbol = f'{self.agent.symbol}-{arg}'
|
||||||
|
ship = self.store.get('Ship', symbol)
|
||||||
|
if ship is None:
|
||||||
|
print('not found')
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
self.ship = ship
|
||||||
|
pprint(self.ship)
|
||||||
|
|
||||||
|
def do_pp(self):
|
||||||
|
pprint(self.api.last_result)
|
||||||
|
|
||||||
|
def do_go(self, arg):
|
||||||
|
if not self.has_ship(): return
|
||||||
|
system = self.ship.location.system
|
||||||
|
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)
|
||||||
|
|||||||
23
nullptr/missions/__init__.py
Normal file
23
nullptr/missions/__init__.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
from nullptr.missions.survey import SurveyMission
|
||||||
|
from nullptr.missions.mine import MiningMission
|
||||||
|
from nullptr.missions.haul import HaulMission
|
||||||
|
from nullptr.missions.travel import TravelMission
|
||||||
|
from nullptr.missions.probe import ProbeMission
|
||||||
|
|
||||||
|
def get_mission_class( mtype):
|
||||||
|
types = {
|
||||||
|
'survey': SurveyMission,
|
||||||
|
'mine': MiningMission,
|
||||||
|
'haul': HaulMission,
|
||||||
|
'travel': TravelMission,
|
||||||
|
'probe': ProbeMission
|
||||||
|
}
|
||||||
|
if mtype not in types:
|
||||||
|
raise ValueError(f'invalid mission type {mtype}')
|
||||||
|
return types[mtype]
|
||||||
|
|
||||||
|
def create_mission(mtype, ship, store, api):
|
||||||
|
typ = get_mission_class(mtype)
|
||||||
|
m = typ(ship, store, api)
|
||||||
|
return m
|
||||||
|
|
||||||
255
nullptr/missions/base.py
Normal file
255
nullptr/missions/base.py
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
from nullptr.store import Store
|
||||||
|
from nullptr.models.base import Base
|
||||||
|
from nullptr.models.waypoint import Waypoint
|
||||||
|
from nullptr.models.contract import Contract
|
||||||
|
from nullptr.models.system import System
|
||||||
|
from nullptr.models.survey import Survey
|
||||||
|
from nullptr.models.ship import Ship
|
||||||
|
from nullptr.analyzer import Analyzer
|
||||||
|
from time import time
|
||||||
|
from functools import partial
|
||||||
|
import logging
|
||||||
|
from nullptr.util import *
|
||||||
|
|
||||||
|
class MissionError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class MissionParam:
|
||||||
|
def __init__(self, cls, required=True, default=None):
|
||||||
|
self.cls = cls
|
||||||
|
self.required = required
|
||||||
|
self.default = default
|
||||||
|
|
||||||
|
def parse(self, val, store):
|
||||||
|
if self.cls == str:
|
||||||
|
return str(val)
|
||||||
|
elif self.cls == int:
|
||||||
|
return int(val)
|
||||||
|
elif self.cls == list:
|
||||||
|
if type(val) == str:
|
||||||
|
return [i.strip() for i in val.split(',')]
|
||||||
|
return val
|
||||||
|
elif issubclass(self.cls, Base):
|
||||||
|
if type(val) == str:
|
||||||
|
data = store.get(self.cls, val)
|
||||||
|
else:
|
||||||
|
data = val
|
||||||
|
if data is None:
|
||||||
|
raise ValueError('object not found')
|
||||||
|
return data.symbol
|
||||||
|
else:
|
||||||
|
raise ValueError('unknown param typr')
|
||||||
|
|
||||||
|
class Mission:
|
||||||
|
@classmethod
|
||||||
|
def params(cls):
|
||||||
|
return {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, ship, store, api):
|
||||||
|
self.ship = ship
|
||||||
|
self.store = store
|
||||||
|
self.api = api
|
||||||
|
self.next_step = 0
|
||||||
|
self.analyzer = Analyzer(self.store)
|
||||||
|
|
||||||
|
def sts(self, nm, v):
|
||||||
|
if issubclass(type(v), Base):
|
||||||
|
v = v.symbol
|
||||||
|
self.ship.set_mission_state(nm, v)
|
||||||
|
|
||||||
|
def rst(self, typ, nm):
|
||||||
|
symbol = self.st(nm)
|
||||||
|
return self.store.get(typ, symbol)
|
||||||
|
|
||||||
|
def st(self, nm):
|
||||||
|
if not nm in self.ship.mission_state:
|
||||||
|
return None
|
||||||
|
return self.ship.mission_state[nm]
|
||||||
|
|
||||||
|
def status(self, nw=None):
|
||||||
|
if nw is None:
|
||||||
|
return self.ship.mission_status
|
||||||
|
else:
|
||||||
|
self.ship.mission_status = nw
|
||||||
|
|
||||||
|
def start_state(self):
|
||||||
|
return 'done'
|
||||||
|
|
||||||
|
def error(self, msg):
|
||||||
|
self.status('error')
|
||||||
|
print(msg)
|
||||||
|
|
||||||
|
def init_state(self):
|
||||||
|
for name, param in self.params().items():
|
||||||
|
if param.required and param.default is None:
|
||||||
|
if not name in self.ship.mission_state:
|
||||||
|
return self.error(f'Param {name} not set')
|
||||||
|
self.status(self.start_state())
|
||||||
|
|
||||||
|
def steps(self):
|
||||||
|
return {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
def step_done(self):
|
||||||
|
logging.info(f'mission finished for {self.ship}')
|
||||||
|
|
||||||
|
def is_waiting(self):
|
||||||
|
return self.next_step > time()
|
||||||
|
|
||||||
|
def is_finished(self):
|
||||||
|
return self.status() in ['done','error']
|
||||||
|
|
||||||
|
def is_ready(self):
|
||||||
|
return not self.is_waiting() and not self.is_finished()
|
||||||
|
|
||||||
|
def step(self):
|
||||||
|
steps = self.steps()
|
||||||
|
if self.status() == 'init':
|
||||||
|
self.init_state()
|
||||||
|
status = self.status()
|
||||||
|
if not status in steps:
|
||||||
|
logging.warning(f"Invalid mission status {status}")
|
||||||
|
self.status('error')
|
||||||
|
return
|
||||||
|
handler, next_step = steps[status]
|
||||||
|
try:
|
||||||
|
result = handler()
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(e, exc_info=True)
|
||||||
|
self.status('error')
|
||||||
|
return
|
||||||
|
if type(next_step) == str:
|
||||||
|
self.status(next_step)
|
||||||
|
elif type(next_step) == dict:
|
||||||
|
if result not in next_step:
|
||||||
|
logging.warning(f'Invalid step result {result}')
|
||||||
|
self.status('error')
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
self.status(next_step[result])
|
||||||
|
print(f'{self.ship} {status} -> {self.status()}')
|
||||||
|
|
||||||
|
class BaseMission(Mission):
|
||||||
|
def step_go_dest(self):
|
||||||
|
destination = self.rst(Waypoint, 'destination')
|
||||||
|
if self.ship.location() == destination:
|
||||||
|
return
|
||||||
|
self.api.navigate(self.ship, destination)
|
||||||
|
self.next_step = self.ship.arrival
|
||||||
|
|
||||||
|
def step_go_site(self):
|
||||||
|
site = self.rst(Waypoint,'site')
|
||||||
|
if self.ship.location() == site:
|
||||||
|
return
|
||||||
|
self.api.navigate(self.ship, site)
|
||||||
|
self.next_step = self.ship.arrival
|
||||||
|
|
||||||
|
def step_unload(self):
|
||||||
|
contract = self.rst(Contract, 'contract')
|
||||||
|
delivery = self.st('delivery')
|
||||||
|
if delivery == 'sell':
|
||||||
|
return self.step_sell(False)
|
||||||
|
typs = self.ship.deliverable_cargo(contract)
|
||||||
|
if len(typs) == 0:
|
||||||
|
return 'done'
|
||||||
|
self.api.deliver(self.ship, typs[0], contract)
|
||||||
|
if len(typs) == 1:
|
||||||
|
return 'done'
|
||||||
|
else:
|
||||||
|
return 'more'
|
||||||
|
|
||||||
|
def step_sell(self, except_resource=True):
|
||||||
|
target = self.st('resource')
|
||||||
|
market = self.store.get('Marketplace', self.ship.location.symbol)
|
||||||
|
sellables = market.sellable_items(self.ship.cargo.keys())
|
||||||
|
if target in sellables and except_resource:
|
||||||
|
sellables.remove(target)
|
||||||
|
if len(sellables) == 0:
|
||||||
|
return 'done'
|
||||||
|
self.api.sell(self.ship, sellables[0])
|
||||||
|
if len(sellables) == 1:
|
||||||
|
return 'done'
|
||||||
|
else:
|
||||||
|
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):
|
||||||
|
traject = self.st('traject')
|
||||||
|
if traject is None or traject == []:
|
||||||
|
return 'done'
|
||||||
|
dest = traject[-1]
|
||||||
|
loc = self.ship.location
|
||||||
|
if dest == loc:
|
||||||
|
self.sts('traject', None)
|
||||||
|
return 'done'
|
||||||
|
hop = traject.pop(0)
|
||||||
|
if type(hop) == Waypoint:
|
||||||
|
self.api.navigate(self.ship, hop)
|
||||||
|
self.next_step = self.ship.arrival
|
||||||
|
else:
|
||||||
|
self.api.jump(self.ship, hop)
|
||||||
|
self.next_step = self.ship.cooldown
|
||||||
|
if traject == []:
|
||||||
|
traject= None
|
||||||
|
self.sts('traject', traject)
|
||||||
|
return 'more'
|
||||||
|
|
||||||
|
def step_calculate_traject(self, dest):
|
||||||
|
if type(dest) == str:
|
||||||
|
dest = self.store.get(Waypoint, dest)
|
||||||
|
loc = self.ship.location
|
||||||
|
loc_sys = loc.system
|
||||||
|
loc_jg = self.analyzer.get_jumpgate(loc_sys)
|
||||||
|
loc_jg_wp = self.store.get(Waypoint, loc_jg.symbol)
|
||||||
|
dest_sys = dest.system
|
||||||
|
dest_jg = self.analyzer.get_jumpgate(dest_sys)
|
||||||
|
if dest_sys == loc_sys:
|
||||||
|
result = [dest]
|
||||||
|
self.sts('traject', result)
|
||||||
|
return
|
||||||
|
path = self.analyzer.find_path(loc_sys, dest_sys)
|
||||||
|
result = []
|
||||||
|
if loc.symbol != loc_jg.symbol:
|
||||||
|
result.append(loc_jg_wp)
|
||||||
|
result += [s for s in path[1:]]
|
||||||
|
if dest_jg.symbol != dest.symbol:
|
||||||
|
result.append(dest)
|
||||||
|
self.sts('traject', result)
|
||||||
|
print(result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def step_dock(self):
|
||||||
|
self.api.dock(self.ship)
|
||||||
|
|
||||||
|
def step_refuel(self):
|
||||||
|
if self.ship.fuel_capacity == 0:
|
||||||
|
return
|
||||||
|
if self.ship.fuel_current / self.ship.fuel_capacity < 0.5:
|
||||||
|
try:
|
||||||
|
self.api.refuel(self.ship)
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def step_orbit(self):
|
||||||
|
self.api.orbit(self.ship)
|
||||||
|
|
||||||
|
def travel_steps(self, nm, destination, next_step):
|
||||||
|
destination = self.st(destination)
|
||||||
|
calc = partial(self.step_calculate_traject, destination)
|
||||||
|
return {
|
||||||
|
f'travel-{nm}': (self.step_orbit, f'calc-trav-{nm}'),
|
||||||
|
f'calc-trav-{nm}': (calc, f'go-{nm}'),
|
||||||
|
f'go-{nm}': (self.step_travel, {
|
||||||
|
'done': f'dock-{nm}',
|
||||||
|
'more': f'go-{nm}'
|
||||||
|
}),
|
||||||
|
f'dock-{nm}': (self.step_dock, f'refuel-{nm}'),
|
||||||
|
f'refuel-{nm}': (self.step_refuel, next_step)
|
||||||
|
}
|
||||||
25
nullptr/missions/haul.py
Normal file
25
nullptr/missions/haul.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
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
|
||||||
|
class HaulMission(BaseMission):
|
||||||
|
def start_state(self):
|
||||||
|
return 'travel-to'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def params(cls):
|
||||||
|
return {
|
||||||
|
'site': MissionParam(Waypoint, True),
|
||||||
|
'resource': MissionParam(str, True),
|
||||||
|
'dest': MissionParam(Waypoint, True),
|
||||||
|
'delivery': MissionParam(str, True, 'deliver'),
|
||||||
|
'contract': MissionParam(Contract, False)
|
||||||
|
}
|
||||||
|
|
||||||
|
def steps(self):
|
||||||
|
return {
|
||||||
|
**self.travel_steps('to', 'site', 'load'),
|
||||||
|
'load': (self.step_load, 'travel-back'),
|
||||||
|
**self.travel_steps('back', 'dest', 'unload'),
|
||||||
|
'unload': (self.step_unload, 'travel-to'),
|
||||||
|
}
|
||||||
80
nullptr/missions/mine.py
Normal file
80
nullptr/missions/mine.py
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
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.util import *
|
||||||
|
|
||||||
|
class MiningMission(BaseMission):
|
||||||
|
@classmethod
|
||||||
|
def params(cls):
|
||||||
|
return {
|
||||||
|
'site': MissionParam(Waypoint, True),
|
||||||
|
'resource': MissionParam(str, True),
|
||||||
|
'dest': MissionParam(Waypoint, True),
|
||||||
|
'delivery': MissionParam(str, True, 'deliver'),
|
||||||
|
'contract': MissionParam(Contract, False)
|
||||||
|
}
|
||||||
|
|
||||||
|
def start_state(self):
|
||||||
|
return 'travel-to'
|
||||||
|
|
||||||
|
def steps(self):
|
||||||
|
return {
|
||||||
|
**self.travel_steps('to', 'site', 'orbit1'),
|
||||||
|
'orbit1': (self.step_orbit, 'extract'),
|
||||||
|
'extract': (self.step_extract, {
|
||||||
|
'done': 'dock',
|
||||||
|
'more': 'extract'
|
||||||
|
}),
|
||||||
|
'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, {
|
||||||
|
'done': 'travel-to',
|
||||||
|
'more': 'unload'
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_survey(self):
|
||||||
|
resource = self.st('resource')
|
||||||
|
site = self.rst(Waypoint,'site')
|
||||||
|
# todo optimize
|
||||||
|
for s in self.store.all(Survey):
|
||||||
|
if resource in s.deposits and site.symbol == s.waypoint():
|
||||||
|
return s
|
||||||
|
return None
|
||||||
|
|
||||||
|
def step_extract(self):
|
||||||
|
survey = self.get_survey()
|
||||||
|
print('using survey:', str(survey))
|
||||||
|
result = self.api.extract(self.ship, survey)
|
||||||
|
symbol = sg(result,'extraction.yield.symbol')
|
||||||
|
units = sg(result,'extraction.yield.units')
|
||||||
|
print('extracted:', units, symbol)
|
||||||
|
self.next_step = self.ship.cooldown
|
||||||
|
if self.ship.cargo_units < self.ship.cargo_capacity:
|
||||||
|
return 'more'
|
||||||
|
else:
|
||||||
|
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'
|
||||||
|
|
||||||
33
nullptr/missions/probe.py
Normal file
33
nullptr/missions/probe.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
from nullptr.missions.base import BaseMission, MissionParam
|
||||||
|
from nullptr.models.waypoint import Waypoint
|
||||||
|
|
||||||
|
class ProbeMission(BaseMission):
|
||||||
|
def start_state(self):
|
||||||
|
return 'next-hop'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def params(cls):
|
||||||
|
return {
|
||||||
|
'hops': MissionParam(list, True),
|
||||||
|
'next-hop': MissionParam(int, True, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
def steps(self):
|
||||||
|
return {
|
||||||
|
'next-hop': (self.step_next_hop, 'travel-to'),
|
||||||
|
**self.travel_steps('to', 'site', 'market'),
|
||||||
|
'market': (self.step_market, 'next-hop'),
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
def step_market(self):
|
||||||
|
loc = self.ship.location()
|
||||||
|
self.api.marketplace(loc)
|
||||||
|
|
||||||
|
def step_next_hop(self):
|
||||||
|
hops = self.st('hops')
|
||||||
|
next_hop = self.st('next-hop')
|
||||||
|
hop = hops[next_hop]
|
||||||
|
self.sts('site', hop)
|
||||||
|
self.sts('next-hop', (next_hop+1) % len(hops))
|
||||||
|
|
||||||
15
nullptr/missions/survey.py
Normal file
15
nullptr/missions/survey.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
from nullptr.missions.base import BaseMission, MissionParam
|
||||||
|
|
||||||
|
class SurveyMission(BaseMission):
|
||||||
|
def start_state(self):
|
||||||
|
return 'survey'
|
||||||
|
|
||||||
|
def steps(self):
|
||||||
|
return {
|
||||||
|
'survey': (self.step_survey, 'survey')
|
||||||
|
}
|
||||||
|
|
||||||
|
def step_survey(self):
|
||||||
|
result = self.api.survey(self.ship)
|
||||||
|
#pprint(result, 2)
|
||||||
|
self.next_step = self.ship.cooldown
|
||||||
16
nullptr/missions/travel.py
Normal file
16
nullptr/missions/travel.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
from nullptr.missions.base import BaseMission, MissionParam
|
||||||
|
from nullptr.models.waypoint import Waypoint
|
||||||
|
|
||||||
|
class TravelMission(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', 'done')
|
||||||
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
from nullptr.models.base import Base
|
||||||
|
from nullptr.models.waypoint import Waypoint
|
||||||
|
from nullptr.models.sector import Sector
|
||||||
|
from nullptr.models.system import System
|
||||||
|
from nullptr.models.agent import Agent
|
||||||
|
from nullptr.models.marketplace import Marketplace
|
||||||
|
from nullptr.models.jumpgate import Jumpgate
|
||||||
|
from nullptr.models.ship import Ship
|
||||||
|
from nullptr.models.contract import Contract
|
||||||
|
from nullptr.models.survey import Survey
|
||||||
|
from nullptr.models.atlas import Atlas
|
||||||
|
|
||||||
|
__all__ = [ 'Waypoint', 'Sector', 'Ship', 'Survey', 'System', 'Agent', 'Marketplace', 'Jumpgate', 'Contract', 'Base', 'Atlas' ]
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
from .base import Base
|
from .base import Base
|
||||||
|
|
||||||
class Agent(Base):
|
class Agent(Base):
|
||||||
token: str = None
|
def define(self):
|
||||||
credits: int = 0
|
self.token: str = None
|
||||||
|
self.credits: int = 0
|
||||||
|
|
||||||
def update(self, d):
|
def update(self, d):
|
||||||
self.seta('credits', d)
|
self.seta('credits', d)
|
||||||
|
|
||||||
def path(self):
|
|
||||||
return f'{self.symbol}.{self.ext()}'
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def ext(self):
|
def ext(self):
|
||||||
return 'agt'
|
return 'agt'
|
||||||
|
|||||||
11
nullptr/models/atlas.py
Normal file
11
nullptr/models/atlas.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
from .base import Base
|
||||||
|
|
||||||
|
class Atlas(Base):
|
||||||
|
@classmethod
|
||||||
|
def ext(self):
|
||||||
|
return 'atl'
|
||||||
|
|
||||||
|
def define(self):
|
||||||
|
self.total_pages = 0
|
||||||
|
self.seen_pages = 0
|
||||||
|
|
||||||
@@ -1,57 +1,96 @@
|
|||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from dataclasses import dataclass
|
|
||||||
from nullptr.util import sg
|
from nullptr.util import sg
|
||||||
|
|
||||||
@dataclass
|
class Reference:
|
||||||
|
def __init__(self, typ, symbol, store):
|
||||||
|
self.typ = typ
|
||||||
|
self.symbol = symbol
|
||||||
|
self.store = store
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(cls, obj):
|
||||||
|
o = cls(type(obj), obj.symbol, obj.store)
|
||||||
|
return o
|
||||||
|
|
||||||
|
def resolve(self):
|
||||||
|
return self.store.get(self.typ, self.symbol)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'*REF*{self.symbol}.{self.typ.ext()}'
|
||||||
|
|
||||||
class Base:
|
class Base:
|
||||||
symbol: str
|
identifier = 'symbol'
|
||||||
store: object
|
|
||||||
|
|
||||||
def __init__(self, symbol, store):
|
def __init__(self, symbol, store):
|
||||||
|
self.disable_dirty = True
|
||||||
|
self.file_offset = None
|
||||||
self.store = store
|
self.store = store
|
||||||
self.symbol = symbol
|
self.symbol = symbol
|
||||||
|
self.define()
|
||||||
|
self.disable_dirty = False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def ext(cls):
|
||||||
|
raise NotImplementedError('no ext')
|
||||||
|
|
||||||
|
def define(self):
|
||||||
|
pass
|
||||||
|
|
||||||
def __hash__(self):
|
def __hash__(self):
|
||||||
return hash((str(type(self)), self.symbol))
|
return hash((str(type(self)), self.symbol))
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
return self.symbol == other.symbol and type(self) == type(other)
|
return type(self) == type(other) and self.symbol == other.symbol
|
||||||
|
|
||||||
def seta(self, attr, d, name=None):
|
def get_system(self):
|
||||||
|
parts = self.symbol.split('-')
|
||||||
|
system_str = f'{parts[0]}-{parts[1]}'
|
||||||
|
system = self.store.get('System', system_str, create=True)
|
||||||
|
return system
|
||||||
|
|
||||||
|
def seta(self, attr, d, name=None, interp=None):
|
||||||
if name is None:
|
if name is None:
|
||||||
name = attr
|
name = attr
|
||||||
val = sg(d, name)
|
val = sg(d, name)
|
||||||
if val is not None:
|
if val is not None:
|
||||||
|
if interp is not None:
|
||||||
|
val = interp(val)
|
||||||
setattr(self, attr, val)
|
setattr(self, attr, val)
|
||||||
|
|
||||||
def setlst(self, attr, d, name, member):
|
def setlst(self, attr, d, name, member, interp=None):
|
||||||
val = sg(d, name)
|
val = sg(d, name)
|
||||||
if val is not None:
|
if val is not None:
|
||||||
lst = [sg(x, member) for x in val]
|
lst = []
|
||||||
|
for x in val:
|
||||||
|
val = sg(x, member)
|
||||||
|
if interp is not None:
|
||||||
|
val = interp(val)
|
||||||
|
lst.append(val)
|
||||||
setattr(self, attr, lst)
|
setattr(self, attr, lst)
|
||||||
|
|
||||||
def __setattr__(self, name, value):
|
def __setattr__(self, name, value):
|
||||||
if name not in ['symbol','store','__dict__']:
|
if name not in ['symbol','store','disable_dirty', 'file_offset'] and not self.disable_dirty:
|
||||||
self.store.dirty(self)
|
self.store.dirty(self)
|
||||||
|
if issubclass(type(value), Base):
|
||||||
|
value = Reference.create(value)
|
||||||
super().__setattr__(name, value)
|
super().__setattr__(name, value)
|
||||||
|
|
||||||
|
def __getattribute__(self, nm):
|
||||||
|
val = super().__getattribute__(nm)
|
||||||
|
if type(val) == Reference:
|
||||||
|
val = val.resolve()
|
||||||
|
return val
|
||||||
|
|
||||||
def update(self, d):
|
def update(self, d):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def dict(self):
|
def is_expired(self):
|
||||||
r = {}
|
return False
|
||||||
for k,v in self.__dict__.items():
|
|
||||||
if k in ['store']:
|
|
||||||
continue
|
|
||||||
r[k] = deepcopy(v)
|
|
||||||
return r
|
|
||||||
|
|
||||||
def path(self):
|
def load(self, d):
|
||||||
raise NotImplementedError('path')
|
self.disable_dirty = True
|
||||||
|
self.__dict__.update(d)
|
||||||
@classmethod
|
self.disable_dirty = False
|
||||||
def ext(self):
|
|
||||||
raise NotImplementedError('extension')
|
|
||||||
|
|
||||||
def type(self):
|
def type(self):
|
||||||
return self.__class__.__name__
|
return self.__class__.__name__
|
||||||
@@ -59,6 +98,9 @@ class Base:
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.f()
|
return self.f()
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return self.f()
|
||||||
|
|
||||||
def f(self, detail=1):
|
def f(self, detail=1):
|
||||||
r = self.symbol
|
r = self.symbol
|
||||||
if detail > 1:
|
if detail > 1:
|
||||||
|
|||||||
69
nullptr/models/contract.py
Normal file
69
nullptr/models/contract.py
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
|
||||||
|
from time import time
|
||||||
|
from nullptr.util import *
|
||||||
|
from .base import Base
|
||||||
|
|
||||||
|
class Contract(Base):
|
||||||
|
identifier = 'id'
|
||||||
|
def define(self):
|
||||||
|
self.type: str = ''
|
||||||
|
self.deliveries: list = []
|
||||||
|
self.accepted: bool = False
|
||||||
|
self.fulfilled: bool = False
|
||||||
|
self.expires: int = 0
|
||||||
|
self.expires_str: str = ''
|
||||||
|
self.pay: int = 0
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def ext(cls):
|
||||||
|
return 'cnt'
|
||||||
|
|
||||||
|
def is_expired(self):
|
||||||
|
return time() > self.expires
|
||||||
|
|
||||||
|
def api_dict(self):
|
||||||
|
return {
|
||||||
|
'id': self.symbol.lower(),
|
||||||
|
'expiration': self.expires_str,
|
||||||
|
}
|
||||||
|
|
||||||
|
def is_done(self):
|
||||||
|
for d in self.deliveries:
|
||||||
|
if d['units_fulfilled'] > d['units_requires']:
|
||||||
|
return False
|
||||||
|
return False
|
||||||
|
|
||||||
|
def unfinished_delivery(self):
|
||||||
|
for d in self.deliveries:
|
||||||
|
if d['units_required'] > d['units_fulfilled']:
|
||||||
|
return d
|
||||||
|
return None
|
||||||
|
|
||||||
|
def update(self, d):
|
||||||
|
self.seta('expires',d, 'terms.deadline',parse_timestamp)
|
||||||
|
self.seta('expires_str', d,'terms.deadline')
|
||||||
|
self.seta('accepted', d, 'accepted')
|
||||||
|
self.seta('fulfilled', d, 'fulfilled')
|
||||||
|
self.seta('type', d, 'type')
|
||||||
|
self.pay = mg(d, 'terms.payment.onAccepted') + mg(d, 'terms.payment.onFulfilled')
|
||||||
|
deliveries = must_get(d, 'terms.deliver')
|
||||||
|
self.deliveries = []
|
||||||
|
for e in deliveries:
|
||||||
|
delivery = {}
|
||||||
|
delivery['trade_symbol'] = must_get(e, 'tradeSymbol')
|
||||||
|
delivery['units_fulfilled'] = must_get(e, 'unitsFulfilled')
|
||||||
|
delivery['units_required'] = must_get(e, 'unitsRequired')
|
||||||
|
delivery['destination'] = must_get(e, 'destinationSymbol')
|
||||||
|
self.deliveries.append(delivery)
|
||||||
|
|
||||||
|
|
||||||
|
def f(self, detail=1):
|
||||||
|
hours = int(max(0, self.expires - time()) / 3600)
|
||||||
|
accepted = 'A' if self.accepted else '-'
|
||||||
|
fulfilled = 'F' if self.fulfilled else '-'
|
||||||
|
result = f'{self.symbol} {hours}h {accepted}{fulfilled}'
|
||||||
|
if detail > 1:
|
||||||
|
result += '\n'
|
||||||
|
for d in self.deliveries:
|
||||||
|
result += f"({d['units_fulfilled']} / {d['units_required']}) {d['trade_symbol']} to {d['destination']}"
|
||||||
|
return result
|
||||||
@@ -1,13 +1,17 @@
|
|||||||
from .system_member import SystemMember
|
from .base import Base
|
||||||
from typing import List
|
from .system import System
|
||||||
|
from dataclasses import field
|
||||||
|
|
||||||
class Jumpgate(SystemMember):
|
class Jumpgate(Base):
|
||||||
range: int
|
def define(self):
|
||||||
faction: str
|
self.range: int = 0
|
||||||
systems:List[str] = []
|
self.faction: str = ''
|
||||||
|
self.systems: list = []
|
||||||
|
self.system = self.get_system()
|
||||||
|
|
||||||
def update(self, d):
|
def update(self, d):
|
||||||
self.setlst('systems', d, 'connectedSystems', 'symbol')
|
getter = self.store.getter(System, create=True)
|
||||||
|
self.setlst('systems', d, 'connectedSystems', 'symbol', interp=getter)
|
||||||
self.seta('faction', d, 'factionSymbol')
|
self.seta('faction', d, 'factionSymbol')
|
||||||
self.seta('range', d, 'jumpRange')
|
self.seta('range', d, 'jumpRange')
|
||||||
|
|
||||||
@@ -15,6 +19,9 @@ class Jumpgate(SystemMember):
|
|||||||
def ext(self):
|
def ext(self):
|
||||||
return 'jmp'
|
return 'jmp'
|
||||||
|
|
||||||
def path(self):
|
def f(self, detail=1):
|
||||||
sector, system, _ = self.symbol.split('-')
|
r = self.symbol
|
||||||
return f'atlas/{sector}/{system[0:1]}/{system}/{self.symbol}.{self.ext()}'
|
if detail > 1:
|
||||||
|
r += '\n'
|
||||||
|
r += '\n'.join([s.symbol for s in self.systems])
|
||||||
|
return r
|
||||||
|
|||||||
@@ -1,21 +1,62 @@
|
|||||||
|
|
||||||
from .system_member import SystemMember
|
from .base import Base
|
||||||
from typing import List
|
from time import time
|
||||||
|
from nullptr.util import *
|
||||||
|
from dataclasses import field
|
||||||
|
from nullptr.models import Waypoint
|
||||||
|
|
||||||
class Marketplace(SystemMember):
|
class Marketplace(Base):
|
||||||
imports:List[str] = []
|
def define(self):
|
||||||
exports:List[str] = []
|
self.imports:list = []
|
||||||
exchange:List[str] = []
|
self.exports:list = []
|
||||||
|
self.exchange:list = []
|
||||||
|
self.prices:dict = {}
|
||||||
|
self.last_prices:int = 0
|
||||||
|
self.set_waypoint()
|
||||||
|
self.system = self.get_system()
|
||||||
|
|
||||||
|
def set_waypoint(self):
|
||||||
|
waypoint = self.store.get(Waypoint, self.symbol, create=True)
|
||||||
|
self.waypoint = waypoint
|
||||||
|
|
||||||
def update(self, d):
|
def update(self, d):
|
||||||
self.setlst('imports', d, 'imports', 'symbol')
|
self.setlst('imports', d, 'imports', 'symbol')
|
||||||
self.setlst('exports', d, 'exports', 'symbol')
|
self.setlst('exports', d, 'exports', 'symbol')
|
||||||
self.setlst('exchange', d, 'exchange', 'symbol')
|
self.setlst('exchange', d, 'exchange', 'symbol')
|
||||||
|
if 'tradeGoods' in d:
|
||||||
|
self.last_prices = time()
|
||||||
|
prices = {}
|
||||||
|
for g in mg(d, 'tradeGoods'):
|
||||||
|
price = {}
|
||||||
|
symbol= mg(g, 'symbol')
|
||||||
|
price['symbol'] = symbol
|
||||||
|
price['buy'] = mg(g, 'purchasePrice')
|
||||||
|
price['sell'] = mg(g, 'sellPrice')
|
||||||
|
price['volume'] = mg(g, 'tradeVolume')
|
||||||
|
prices[symbol] = price
|
||||||
|
self.prices = prices
|
||||||
|
|
||||||
|
def sellable_items(self, resources):
|
||||||
|
return [r for r in resources if r in self.prices]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def ext(self):
|
def ext(self):
|
||||||
return 'mkt'
|
return 'mkt'
|
||||||
|
|
||||||
def path(self):
|
def rtype(self, r):
|
||||||
sector, system, _ = self.symbol.split('-')
|
if r in self.imports:
|
||||||
return f'atlas/{sector}/{system[0:1]}/{system}/{self.symbol}.{self.ext()}'
|
return 'I'
|
||||||
|
if r in self.exports:
|
||||||
|
return 'E'
|
||||||
|
if r in self.exchange:
|
||||||
|
return 'X'
|
||||||
|
return '?'
|
||||||
|
|
||||||
|
def f(self, detail=1):
|
||||||
|
r = self.symbol
|
||||||
|
if detail > 1:
|
||||||
|
r += '\n'
|
||||||
|
for p in self.prices.values():
|
||||||
|
t = self.rtype(p['symbol'])
|
||||||
|
r += f'{t} {p["symbol"]:25s} {p["sell"]:5d} {p["buy"]:5d}\n'
|
||||||
|
return r
|
||||||
|
|||||||
98
nullptr/models/ship.py
Normal file
98
nullptr/models/ship.py
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
from .base import Base
|
||||||
|
from time import time
|
||||||
|
from nullptr.util import *
|
||||||
|
from nullptr.models import Waypoint
|
||||||
|
|
||||||
|
class Ship(Base):
|
||||||
|
def define(self):
|
||||||
|
self.cargo:dict = {}
|
||||||
|
self.mission_state:dict = {}
|
||||||
|
self.status:str = ''
|
||||||
|
self.cargo_capacity:int = 0
|
||||||
|
self.cargo_units:int = 0
|
||||||
|
self.location = None
|
||||||
|
self.cooldown:int = 0
|
||||||
|
self.arrival:int = 0
|
||||||
|
self.fuel_current:int = 0
|
||||||
|
self.fuel_capacity:int = 0
|
||||||
|
self.mission:str = None
|
||||||
|
self.mission_status:str = 'init'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def ext(self):
|
||||||
|
return 'shp'
|
||||||
|
|
||||||
|
def update(self, d):
|
||||||
|
self.seta('status', d, 'nav.status')
|
||||||
|
getter = self.store.getter(Waypoint, create=True)
|
||||||
|
self.seta('location', d, 'nav.waypointSymbol', interp=getter)
|
||||||
|
self.seta('cargo_capacity', d, 'cargo.capacity')
|
||||||
|
self.seta('cargo_units', d, 'cargo.units')
|
||||||
|
self.seta('fuel_capacity', d, 'fuel.capacity')
|
||||||
|
self.seta('fuel_current', d,'fuel.current')
|
||||||
|
cargo = sg(d, 'cargo.inventory')
|
||||||
|
if cargo is not None:
|
||||||
|
self.load_cargo(cargo)
|
||||||
|
self.seta('cooldown', d, 'cooldown.expiration', parse_timestamp)
|
||||||
|
self.seta('arrival', d, 'nav.route.arrival', parse_timestamp)
|
||||||
|
|
||||||
|
def tick(self):
|
||||||
|
if self.status == 'IN_TRANSIT' and self.arrival < time():
|
||||||
|
self.status = 'IN_ORBIT'
|
||||||
|
|
||||||
|
def is_cooldown(self):
|
||||||
|
return self.cooldown > time()
|
||||||
|
|
||||||
|
def is_travelling(self):
|
||||||
|
return self.status == 'IN_TRANSIT'
|
||||||
|
|
||||||
|
def set_mission_state(self, nm, val):
|
||||||
|
self.mission_state[nm] = val
|
||||||
|
self.store.dirty(self)
|
||||||
|
|
||||||
|
def get_cargo(self, typ):
|
||||||
|
if typ not in self.cargo:
|
||||||
|
return 0
|
||||||
|
return self.cargo[typ]
|
||||||
|
|
||||||
|
def load_cargo(self, cargo):
|
||||||
|
result = {}
|
||||||
|
for i in cargo:
|
||||||
|
symbol = must_get(i, 'symbol')
|
||||||
|
units = must_get(i, 'units')
|
||||||
|
result[symbol] = units
|
||||||
|
self.cargo = result
|
||||||
|
|
||||||
|
def deliverable_cargo(self, contract):
|
||||||
|
result = []
|
||||||
|
for d in contract.deliveries:
|
||||||
|
if self.get_cargo(d['trade_symbol']) > 0:
|
||||||
|
result.append(d['trade_symbol'])
|
||||||
|
return result
|
||||||
|
|
||||||
|
def nondeliverable_cargo(self, contract):
|
||||||
|
cargo = self.cargo.keys()
|
||||||
|
deliveries = [d['trade_symbol'] for d in contract.deliveries]
|
||||||
|
garbage = [c for c in cargo if c not in deliveries]
|
||||||
|
return garbage
|
||||||
|
|
||||||
|
def update_timers(self):
|
||||||
|
if self.status == 'IN_TRANSIT' and self.arrival < time():
|
||||||
|
self.status = 'IN_ORBIT'
|
||||||
|
|
||||||
|
|
||||||
|
def f(self, detail=1):
|
||||||
|
self.update_timers()
|
||||||
|
arrival = int(self.arrival - time())
|
||||||
|
cooldown = int(self.cooldown - time())
|
||||||
|
r = self.symbol
|
||||||
|
if detail > 1:
|
||||||
|
r += ' ' + self.status
|
||||||
|
r += f' [{self.fuel_current}/{self.fuel_capacity}]'
|
||||||
|
r += ' ' + str(self.location)
|
||||||
|
if self.is_travelling():
|
||||||
|
r += f' [A: {arrival}]'
|
||||||
|
if self.is_cooldown():
|
||||||
|
r += f' [C: {cooldown}]'
|
||||||
|
return r
|
||||||
|
|
||||||
50
nullptr/models/survey.py
Normal file
50
nullptr/models/survey.py
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
from time import time
|
||||||
|
from nullptr.util import *
|
||||||
|
from .base import Base
|
||||||
|
|
||||||
|
size_names = ['SMALL','MODERATE','LARGE']
|
||||||
|
|
||||||
|
class Survey(Base):
|
||||||
|
identifier = 'signature'
|
||||||
|
def define(self):
|
||||||
|
self.type: str = ''
|
||||||
|
self.deposits: list[str] = []
|
||||||
|
self.size: int = 0
|
||||||
|
self.expires: int = 0
|
||||||
|
self.expires_str: str = ''
|
||||||
|
self.exhausted: bool = False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def ext(cls):
|
||||||
|
return 'svy'
|
||||||
|
|
||||||
|
def is_expired(self):
|
||||||
|
return time() > self.expires or self.exhausted
|
||||||
|
|
||||||
|
def waypoint(self):
|
||||||
|
p = self.symbol.split('-')
|
||||||
|
return '-'.join(p[:3])
|
||||||
|
|
||||||
|
def api_dict(self):
|
||||||
|
return {
|
||||||
|
'signature': self.symbol,
|
||||||
|
'symbol': self.waypoint(),
|
||||||
|
'deposits': [{'symbol': d} for d in self.deposits],
|
||||||
|
'expiration': self.expires_str,
|
||||||
|
'size': size_names[self.size]
|
||||||
|
}
|
||||||
|
|
||||||
|
def update(self, d):
|
||||||
|
sz = must_get(d, 'size')
|
||||||
|
self.size = size_names.index(sz)
|
||||||
|
self.deposits = [d['symbol'] for d in must_get(d, 'deposits')]
|
||||||
|
self.seta('expires',d, 'expiration',parse_timestamp)
|
||||||
|
self.seta('expires_str',d, 'expiration')
|
||||||
|
|
||||||
|
def f(self, detail=1):
|
||||||
|
result = self.symbol
|
||||||
|
if detail > 1:
|
||||||
|
result += ' ' + ','.join(self.deposits)
|
||||||
|
minutes = max(self.expires - time(), 0) //60
|
||||||
|
result += ' ' + str(int(minutes)) + 'm'
|
||||||
|
return result
|
||||||
@@ -3,9 +3,12 @@ from .base import Base
|
|||||||
from math import sqrt
|
from math import sqrt
|
||||||
|
|
||||||
class System(Base):
|
class System(Base):
|
||||||
x:int = 0
|
def define(self):
|
||||||
y:int = 0
|
self.x:int = 0
|
||||||
type:str = 'unknown'
|
self.y:int = 0
|
||||||
|
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)
|
||||||
@@ -16,9 +19,8 @@ class System(Base):
|
|||||||
def ext(self):
|
def ext(self):
|
||||||
return 'stm'
|
return 'stm'
|
||||||
|
|
||||||
def path(self):
|
|
||||||
sector, symbol = self.symbol.split('-')
|
|
||||||
return f'atlas/{sector}/{symbol[0:1]}/{self.symbol}.{self.ext()}'
|
|
||||||
|
|
||||||
def distance(self, other):
|
def distance(self, other):
|
||||||
return int(sqrt((self.x - other.x) ** 2 + (self.y - other.y) ** 2))
|
return int(sqrt((self.x - other.x) ** 2 + (self.y - other.y) ** 2))
|
||||||
|
|
||||||
|
def sector(self):
|
||||||
|
return self.symbol.split('-')[0]
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
from .base import Base
|
|
||||||
|
|
||||||
class SystemMember(Base):
|
|
||||||
@classmethod
|
|
||||||
def ext(cls):
|
|
||||||
return 'obj'
|
|
||||||
|
|
||||||
def system(self):
|
|
||||||
p = self.symbol.split('-')
|
|
||||||
return f'{p[0]}-{p[1]}'
|
|
||||||
@@ -1,13 +1,17 @@
|
|||||||
from .system_member import SystemMember
|
from .base import Base, Reference
|
||||||
|
from nullptr.models.system import System
|
||||||
from nullptr.util import *
|
from nullptr.util import *
|
||||||
from typing import List
|
from time import time
|
||||||
|
|
||||||
class Waypoint(SystemMember):
|
class Waypoint(Base):
|
||||||
x:int = 0
|
def define(self):
|
||||||
y:int = 0
|
self.x:int = 0
|
||||||
type:str = 'unknown'
|
self.y:int = 0
|
||||||
traits:List[str]=[]
|
self.type:str = 'unknown'
|
||||||
faction:str = ''
|
self.traits:list = []
|
||||||
|
self.faction:str = ''
|
||||||
|
self.system = self.get_system()
|
||||||
|
self.uncharted = True
|
||||||
|
|
||||||
def update(self, d):
|
def update(self, d):
|
||||||
self.seta('x', d)
|
self.seta('x', d)
|
||||||
@@ -15,11 +19,9 @@ class Waypoint(SystemMember):
|
|||||||
self.seta('type', d)
|
self.seta('type', d)
|
||||||
self.seta('faction', d, 'faction.symbol')
|
self.seta('faction', d, 'faction.symbol')
|
||||||
self.setlst('traits', d, 'traits', 'symbol')
|
self.setlst('traits', d, 'traits', 'symbol')
|
||||||
|
self.uncharted = 'UNCHARTED' in self.traits
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def ext(self):
|
def ext(self):
|
||||||
return 'way'
|
return 'way'
|
||||||
|
|
||||||
def path(self):
|
|
||||||
sector, system, _ = self.symbol.split('-')
|
|
||||||
return f'atlas/{sector}/{system[0:1]}/{system}/{self.symbol}.{self.ext()}'
|
|
||||||
|
|||||||
263
nullptr/store.py
263
nullptr/store.py
@@ -1,83 +1,195 @@
|
|||||||
from nullptr.models.base import Base
|
from nullptr.models import *
|
||||||
from nullptr.models.waypoint import Waypoint
|
|
||||||
from nullptr.models.sector import Sector
|
|
||||||
from nullptr.models.system import System
|
|
||||||
from nullptr.models.agent import Agent
|
|
||||||
from nullptr.models.marketplace import Marketplace
|
|
||||||
from nullptr.models.system_member import SystemMember
|
|
||||||
from nullptr.models.jumpgate import Jumpgate
|
|
||||||
from os.path import isfile, dirname, isdir
|
from os.path import isfile, dirname, isdir
|
||||||
import os
|
import os
|
||||||
from os.path import basename
|
from os.path import basename
|
||||||
import json
|
import json
|
||||||
from .util import *
|
from .util import *
|
||||||
from time import time
|
from time import time
|
||||||
|
import pickle
|
||||||
|
from struct import unpack, pack
|
||||||
|
from functools import partial
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
class StorePickler(pickle.Pickler):
|
||||||
|
def persistent_id(self, obj):
|
||||||
|
return "STORE" if type(obj) == Store else None
|
||||||
|
|
||||||
|
class StoreUnpickler(pickle.Unpickler):
|
||||||
|
def __init__(self, stream, store):
|
||||||
|
self.store = store
|
||||||
|
super().__init__(stream)
|
||||||
|
|
||||||
|
def persistent_load(self, pers_id):
|
||||||
|
if pers_id == "STORE":
|
||||||
|
return self.store
|
||||||
|
raise pickle.UnpicklingError("I don know the persid!")
|
||||||
|
|
||||||
|
|
||||||
|
class ChunkHeader:
|
||||||
|
def __init__(self):
|
||||||
|
self.offset = 0
|
||||||
|
self.in_use = True
|
||||||
|
self.size = 0
|
||||||
|
self.used = 0
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def parse(cls, fil):
|
||||||
|
offset = fil.tell()
|
||||||
|
d = fil.read(16)
|
||||||
|
if len(d) < 16:
|
||||||
|
return None
|
||||||
|
o = cls()
|
||||||
|
o.offset = offset
|
||||||
|
d, o.used = unpack('<QQ', d)
|
||||||
|
o.size = d & 0x7fffffffffffffff
|
||||||
|
o.in_use = d & 0x8000000000000000 != 0
|
||||||
|
# print(o)
|
||||||
|
return o
|
||||||
|
|
||||||
|
def write(self, f):
|
||||||
|
d = self.size
|
||||||
|
if self.in_use:
|
||||||
|
d |= 1 << 63
|
||||||
|
d = pack('<QQ', d, self.used)
|
||||||
|
f.write(d)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'chunk {self.in_use} {self.size} {self.used}'
|
||||||
|
|
||||||
class Store:
|
class Store:
|
||||||
def __init__(self, data_dir):
|
def __init__(self, data_file):
|
||||||
self.init_models()
|
self.init_models()
|
||||||
self.data_dir = data_dir
|
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 = {}
|
||||||
self.dirty_objects = set()
|
self.dirty_objects = set()
|
||||||
|
self.cleanup_interval = 600
|
||||||
|
self.last_cleanup = 0
|
||||||
|
self.slack = 0.1
|
||||||
|
self.slack_min = 64
|
||||||
|
self.slack_max = 1024
|
||||||
|
self.load()
|
||||||
|
|
||||||
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}
|
||||||
|
self.model_names = {c.__name__: c for c in self.models}
|
||||||
|
|
||||||
def dirty(self, obj):
|
def dirty(self, obj):
|
||||||
self.dirty_objects.add(obj)
|
self.dirty_objects.add(obj)
|
||||||
|
|
||||||
def path(self, obj):
|
def dump_object(self, obj):
|
||||||
return os.path.join(self.data_dir, obj.path())
|
buf = BytesIO()
|
||||||
|
p = StorePickler(buf)
|
||||||
|
p.dump(obj)
|
||||||
|
return buf.getvalue()
|
||||||
|
|
||||||
def load_file(self, path):
|
def load_object(self, data, offset):
|
||||||
if not isfile(path):
|
buf = BytesIO(data)
|
||||||
return None
|
p = StoreUnpickler(buf, self)
|
||||||
fn = basename(path)
|
obj = p.load()
|
||||||
ext = fn.split('.')[-1]
|
obj.file_offset = offset
|
||||||
symbol = fn.split('.')[0]
|
obj.disable_dirty = False
|
||||||
if ext not in self.extensions:
|
self.hold(obj)
|
||||||
return None
|
|
||||||
with open(path) as f:
|
|
||||||
data = json.load(f)
|
|
||||||
|
|
||||||
typ = self.extensions[ext]
|
|
||||||
obj = self.create(typ, symbol)
|
|
||||||
data['store'] = self
|
|
||||||
obj.__dict__ = data
|
|
||||||
return obj
|
|
||||||
|
|
||||||
def load(self):
|
def load(self):
|
||||||
cnt = 0
|
cnt = 0
|
||||||
start_time = time()
|
start_time = time()
|
||||||
for fil in list_files(self.data_dir, True):
|
total = 0
|
||||||
self.load_file(fil)
|
free = 0
|
||||||
|
self.fil.seek(0)
|
||||||
|
offset = 0
|
||||||
|
while (hdr := ChunkHeader.parse(self.fil)):
|
||||||
|
# print(hdr)
|
||||||
|
total += hdr.size
|
||||||
|
if not hdr.in_use:
|
||||||
|
self.fil.seek(hdr.size, 1)
|
||||||
|
free += hdr.size
|
||||||
|
continue
|
||||||
|
data = self.fil.read(hdr.used)
|
||||||
|
self.load_object(data, offset)
|
||||||
|
self.fil.seek(hdr.size - hdr.used, 1)
|
||||||
|
offset = self.fil.tell()
|
||||||
cnt += 1
|
cnt += 1
|
||||||
|
|
||||||
dur = time() - start_time
|
dur = time() - start_time
|
||||||
print(f'loaded {cnt} objects in {dur:.2f} seconds')
|
print(f'Loaded {cnt} objects in {dur:.2f} seconds')
|
||||||
|
print(f'Fragmented space: {free} / {total} bytes')
|
||||||
|
|
||||||
|
def allocate_chunk(self, sz):
|
||||||
|
used = sz
|
||||||
|
slack = sz * self.slack
|
||||||
|
slack = min(slack, self.slack_max)
|
||||||
|
slack = max(slack, self.slack_min)
|
||||||
|
sz += int(slack)
|
||||||
|
self.fil.seek(0, 2)
|
||||||
|
offset = self.fil.tell()
|
||||||
|
h = ChunkHeader()
|
||||||
|
h.size = sz
|
||||||
|
h.used = used
|
||||||
|
h.offset = self.fil.tell()
|
||||||
|
h.write(self.fil)
|
||||||
|
return offset, h
|
||||||
|
|
||||||
|
def purge(self, obj):
|
||||||
|
if obj.file_offset is None:
|
||||||
|
return
|
||||||
|
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)
|
||||||
|
obj.file_offset = None
|
||||||
|
|
||||||
def store(self, obj):
|
def store(self, obj):
|
||||||
path = self.path(obj)
|
data = self.dump_object(obj)
|
||||||
path_dir = dirname(path)
|
osize = len(data)
|
||||||
data = obj.dict()
|
# is there an existing chunk for this obj?
|
||||||
if not isdir(path_dir):
|
if obj.file_offset is not None:
|
||||||
os.makedirs(path_dir, exist_ok=True)
|
# read chunk hdr
|
||||||
with open(path, 'w') as f:
|
self.fil.seek(obj.file_offset)
|
||||||
json.dump(data, f, indent=2)
|
hdr = ChunkHeader.parse(self.fil)
|
||||||
|
csize = hdr.size
|
||||||
|
# if the chunk is too small
|
||||||
|
if csize < osize:
|
||||||
|
# free the chunk
|
||||||
|
hdr.in_use = False
|
||||||
|
# force a new chunk
|
||||||
|
obj.file_offset = None
|
||||||
|
else:
|
||||||
|
# if it is big enough, update the used field
|
||||||
|
hdr.used = osize
|
||||||
|
self.fil.seek(hdr.offset)
|
||||||
|
hdr.write(self.fil)
|
||||||
|
|
||||||
def create(self, typ, symbol):
|
if obj.file_offset is None:
|
||||||
obj = typ(symbol, self)
|
obj.file_offset, hdr = self.allocate_chunk(osize)
|
||||||
|
# print(type(obj).__name__, hdr)
|
||||||
|
self.fil.write(data)
|
||||||
|
slack = b'\x00' * (hdr.size - hdr.used)
|
||||||
|
self.fil.write(slack)
|
||||||
|
|
||||||
|
def hold(self, obj):
|
||||||
|
typ = type(obj)
|
||||||
|
symbol = obj.symbol
|
||||||
|
obj.store = self
|
||||||
self.data[typ][symbol] = obj
|
self.data[typ][symbol] = obj
|
||||||
if issubclass(typ, SystemMember):
|
if hasattr(obj, 'system') and obj.system != None:
|
||||||
system_str = obj.system()
|
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()
|
||||||
self.system_members[system_str].add(obj)
|
self.system_members[system_str].add(obj)
|
||||||
|
|
||||||
|
def create(self, typ, symbol):
|
||||||
|
obj = typ(symbol, self)
|
||||||
|
self.hold(obj)
|
||||||
|
self.dirty(obj)
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
def get(self, typ, symbol, create=False):
|
def get(self, typ, symbol, create=False):
|
||||||
|
if type(typ) == str and typ in self.model_names:
|
||||||
|
typ = self.model_names[typ]
|
||||||
|
symbol = symbol.upper()
|
||||||
if typ not in self.data:
|
if typ not in self.data:
|
||||||
return None
|
return None
|
||||||
if symbol not in self.data[typ]:
|
if symbol not in self.data[typ]:
|
||||||
@@ -87,33 +199,92 @@ class Store:
|
|||||||
return None
|
return None
|
||||||
return self.data[typ][symbol]
|
return self.data[typ][symbol]
|
||||||
|
|
||||||
def update(self, typ, symbol, data):
|
def getter(self, typ, create=False):
|
||||||
|
if type(typ) == str and typ in self.model_names:
|
||||||
|
typ = self.model_names[typ]
|
||||||
|
return partial(self.get, typ, create=create)
|
||||||
|
|
||||||
|
def update(self, typ, data, symbol=None):
|
||||||
|
if type(typ) == str and typ in self.model_names:
|
||||||
|
typ = self.model_names[typ]
|
||||||
|
if symbol is None:
|
||||||
|
symbol = mg(data, typ.identifier)
|
||||||
obj = self.get(typ, symbol, True)
|
obj = self.get(typ, symbol, True)
|
||||||
obj.update(data)
|
obj.update(data)
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
def update_list(self, typ, lst):
|
def update_list(self, typ, lst):
|
||||||
return [self.update(typ, mg(d, 'symbol'), d) for d in lst]
|
return [self.update(typ, d) for d in lst]
|
||||||
|
|
||||||
def all(self, typ):
|
def all(self, typ):
|
||||||
|
if type(typ) == str and typ in self.model_names:
|
||||||
|
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):
|
||||||
|
if type(typ) == str and typ in self.model_names:
|
||||||
|
typ = self.model_names[typ]
|
||||||
|
|
||||||
if type(system) == System:
|
if type(system) == System:
|
||||||
system = system.symbol
|
system = system.symbol
|
||||||
|
|
||||||
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):
|
||||||
|
if time() < self.last_cleanup + self.cleanup_interval:
|
||||||
|
return
|
||||||
|
start_time = time()
|
||||||
|
expired = list()
|
||||||
|
for t in self.data:
|
||||||
|
for o in self.all(t):
|
||||||
|
if o.is_expired():
|
||||||
|
expired.append(o)
|
||||||
|
for o in expired:
|
||||||
|
self.purge(obj)
|
||||||
|
|
||||||
|
del self.data[type(o)][o.symbol]
|
||||||
|
dur = time() - start_time
|
||||||
|
# print(f'cleaned {len(expired)} in {dur:.03f} seconds')
|
||||||
|
|
||||||
def flush(self):
|
def flush(self):
|
||||||
|
self.cleanup()
|
||||||
it = 0
|
it = 0
|
||||||
start_time = time()
|
start_time = time()
|
||||||
for obj in self.dirty_objects:
|
for obj in self.dirty_objects:
|
||||||
it += 1
|
it += 1
|
||||||
self.store(obj)
|
if obj.is_expired():
|
||||||
|
self.purge(obj)
|
||||||
|
else:
|
||||||
|
self.store(obj)
|
||||||
|
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}')
|
# print(f'flush done {it} items {dur:.2f}')
|
||||||
|
|
||||||
|
def defrag(self):
|
||||||
|
nm = self.fil.name
|
||||||
|
self.fil.close()
|
||||||
|
os.rename(nm, nm + '.bak')
|
||||||
|
self.fil = open(nm, 'ab+')
|
||||||
|
for t in self.data:
|
||||||
|
for o in self.all(t):
|
||||||
|
o.file_offset = None
|
||||||
|
self.store(o)
|
||||||
|
|||||||
@@ -1,20 +1,15 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from math import ceil
|
from math import ceil
|
||||||
import os
|
import os
|
||||||
from os.path import isfile
|
from os.path import isfile, dirname
|
||||||
|
|
||||||
def list_files(path, recursive=False):
|
def open_file(fn):
|
||||||
if recursive:
|
d = dirname(fn)
|
||||||
for p, dirnames, fils in os.walk(path):
|
os.makedirs(d, exist_ok=True)
|
||||||
for f in fils:
|
if isfile(fn):
|
||||||
fil = os.path.join(p, f)
|
return open(fn, 'rb+')
|
||||||
yield fil
|
|
||||||
else:
|
else:
|
||||||
for f in os.listdir(path):
|
return open(fn, 'ab+')
|
||||||
fil = os.path.join(path, f)
|
|
||||||
if not isfile(fil):
|
|
||||||
continue
|
|
||||||
yield fil
|
|
||||||
|
|
||||||
def must_get(d, k):
|
def must_get(d, k):
|
||||||
if type(k) == str:
|
if type(k) == str:
|
||||||
@@ -58,7 +53,7 @@ def pretty(d, ident=0, detail=2):
|
|||||||
return d.f(detail)
|
return d.f(detail)
|
||||||
r = ''
|
r = ''
|
||||||
idt = ' ' * ident
|
idt = ' ' * ident
|
||||||
if type(d) == list:
|
if type(d) in [list, set]:
|
||||||
r += 'lst'
|
r += 'lst'
|
||||||
for i in d:
|
for i in d:
|
||||||
r += '\n' + idt + pretty(i, ident + 1, detail)
|
r += '\n' + idt + pretty(i, ident + 1, detail)
|
||||||
|
|||||||
50
store.md
Normal file
50
store.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# The store format
|
||||||
|
This project uses a custom database format just because we can.
|
||||||
|
|
||||||
|
The script reads the entire store on startup. Each object can br altered in memory. A 'dirty' status is set when an object is changed. periodically the script will flush the store, writing all changes back to disk.
|
||||||
|
|
||||||
|
The store disk format is optimized for two things:
|
||||||
|
* Loading the entire contents in memory
|
||||||
|
* Writing changed objects back to disk
|
||||||
|
|
||||||
|
## Objects
|
||||||
|
First lets discuss what we are storing.
|
||||||
|
|
||||||
|
Objects are identified by type and symbol. A symbol is an uppercase alphanumeric string that optionally contains '-' chars. the type is a lowecase alphanumeric string of length 3.
|
||||||
|
|
||||||
|
For each type, the store has a class defined that is used to load objects of that type.
|
||||||
|
|
||||||
|
An object identifier is its symbol and type joined with a '.' character.
|
||||||
|
|
||||||
|
Some examples of object identifiers:
|
||||||
|
|
||||||
|
* X1-J84.sys
|
||||||
|
* X1-J84-0828772.way
|
||||||
|
* CAPT-J-1.shp
|
||||||
|
|
||||||
|
A waypoint is always part of a system. This is also visible because the waypoint symbol is prefixed by the system symbol. However, this relation is not enforced or used by the store. The symbol is an opaque string.
|
||||||
|
|
||||||
|
An object has attributes. Values of attributes can be strings, ints, floats, bools, lists, dicts and references. lists and dicts can also only contain the values listed. References are pointers to other objects in the store.
|
||||||
|
|
||||||
|
## Indices
|
||||||
|
An index is a dict with a string as key and a list of objects as value. The dict is built when loading the store. when the index is iterated, each object is re-checked and removed if necessary.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
* store.load(fil) loads all objects
|
||||||
|
* 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.purge(obj)
|
||||||
|
* store.clean() removes all expired objects
|
||||||
|
* store.flush() writes all dirty objects to disk
|
||||||
|
* store.defrag() consolidates the store file to minimize storage and loading time
|
||||||
|
|
||||||
|
type may be a class or a string containing the name of a class. The type should be a subclass of models.base.Base
|
||||||
|
|
||||||
|
# file format
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
Reference in New Issue
Block a user