A corporation manager and dashboard for EVE Online
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

universe.py 11 KiB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. # -*- coding: utf-8 -*-
  2. import gzip
  3. from threading import Lock
  4. import yaml
  5. __all__ = ["Universe"]
  6. def _cache(func):
  7. """Wrap a no-argument method to cache its return value in the object."""
  8. def inner(self):
  9. key = func.__name__
  10. if key in self._cache:
  11. return self._cache[key]
  12. value = func(self)
  13. self._cache[key] = value
  14. return value
  15. return inner
  16. class _UniqueObject:
  17. """Base class for uniquely ID'd objects in the universe."""
  18. def __init__(self, universe, id_, data):
  19. self._universe = universe
  20. self._id = id_
  21. self._data = data
  22. self._cache = {}
  23. @property
  24. def id(self):
  25. """The object's unique ID, as an integer."""
  26. return self._id
  27. class _SolarSystem(_UniqueObject):
  28. """Represents a solar system."""
  29. @property
  30. def name(self):
  31. """The solar system's name, as a string."""
  32. return self._data["name"]
  33. @property
  34. @_cache
  35. def constellation(self):
  36. """The solar system's constellation, as a _Constellation object."""
  37. return self._universe.constellation(self._data["constellation"])
  38. @property
  39. @_cache
  40. def region(self):
  41. """The solar system's region, as a _Region object."""
  42. return self._universe.region(self._data["region"])
  43. @property
  44. def security(self):
  45. """The solar system's security status, as a float."""
  46. return self._data["security"]
  47. @property
  48. def coords(self):
  49. """The solar system's coordinates, as a 3-tuple of floats (x, y, z)."""
  50. return tuple(self._data["coords"])
  51. @property
  52. @_cache
  53. def gates(self):
  54. """The solar system's adjacent systems, via stargate.
  55. A list of _SolarSystem objects.
  56. """
  57. return [self._universe.system(sid) for sid in self._data["gates"]]
  58. @property
  59. @_cache
  60. def faction(self):
  61. """The solar system's faction, as a _Faction object, or None."""
  62. if "faction" in self._data:
  63. return self._universe.faction(self._data["faction"])
  64. return self.constellation.faction
  65. @property
  66. def is_nullsec(self):
  67. """Whether the solar system is in nullsec."""
  68. return self.security <= 0.0
  69. @property
  70. def is_lowsec(self):
  71. """Whether the solar system is in nullsec."""
  72. return self.security > 0.0 and self.security < 0.45
  73. @property
  74. def is_highsec(self):
  75. """Whether the solar system is in nullsec."""
  76. return self.security >= 0.45
  77. @property
  78. def is_whspace(self):
  79. """Whether the solar system is in wormhole space."""
  80. return self.region.is_whspace
  81. class _Constellation(_UniqueObject):
  82. """Represents a constellation."""
  83. @property
  84. def name(self):
  85. """The constellation's name, as a string."""
  86. return self._data["name"]
  87. @property
  88. @_cache
  89. def region(self):
  90. """The constellation's region, as a _Region object."""
  91. return self._universe.region(self._data["region"])
  92. @property
  93. @_cache
  94. def faction(self):
  95. """The constellation's faction, as a _Faction object, or None."""
  96. if "faction" in self._data:
  97. return self._universe.faction(self._data["faction"])
  98. return self.region.faction
  99. @property
  100. def is_whspace(self):
  101. """Whether the constellation is in wormhole space."""
  102. return self.region.is_whspace
  103. class _Region(_UniqueObject):
  104. """Represents a region."""
  105. @property
  106. def name(self):
  107. """The region's name, as a string."""
  108. return self._data["name"]
  109. @property
  110. @_cache
  111. def faction(self):
  112. """The region's faction, as a _Faction object, or None."""
  113. if "faction" in self._data:
  114. return self._universe.faction(self._data["faction"])
  115. return None
  116. @property
  117. def is_whspace(self):
  118. """Whether the region is in wormhole space."""
  119. return self._id >= 11000000
  120. class _Faction(_UniqueObject):
  121. """Represents a faction."""
  122. @property
  123. def name(self):
  124. """The faction's name, as a string."""
  125. return self._data["name"]
  126. @property
  127. @_cache
  128. def territory(self):
  129. """The faction's controlled territory.
  130. A list of _SolarSystems.
  131. """
  132. return [system for system in self._universe.systems()
  133. if system.faction and system.faction.id == self.id]
  134. class _Type(_UniqueObject):
  135. """Represents any type, including ships and materials."""
  136. @property
  137. def name(self):
  138. """The item's name, as a string."""
  139. return self._data["name"]
  140. @property
  141. def group_id(self):
  142. """The item's group ID, as an integer."""
  143. return self._data["group_id"]
  144. @property
  145. def market_group_id(self):
  146. """The item's market group ID, as an integer, or None."""
  147. return self._data.get("market_group_id")
  148. class _Killable(_UniqueObject):
  149. """Represents a killable object, like a ship, structure, or fighter."""
  150. def __init__(self, universe, kid, cat, data):
  151. super().__init__(universe, kid, data)
  152. self._cat = cat
  153. @property
  154. def name(self):
  155. """The killable object's name, as a string."""
  156. return self._data["name"]
  157. @property
  158. def group(self):
  159. """The killable object's group, as a string."""
  160. return self._data["group"]
  161. @property
  162. def is_ship(self):
  163. """Whether the killable object is a ship."""
  164. return self._cat == "ships"
  165. @property
  166. def is_structure(self):
  167. """Whether the killable object is a structure."""
  168. return self._cat == "structures"
  169. @property
  170. def is_fighter(self):
  171. """Whether the killable object is a fighter."""
  172. return self._cat == "fighters"
  173. class _DummySolarSystem(_SolarSystem):
  174. """Represents an unknown or invalid solar system."""
  175. def __init__(self, universe):
  176. super().__init__(universe, -1, {
  177. "name": "Unknown",
  178. "constellation": -1,
  179. "region": -1,
  180. "security": 0.0,
  181. "coords": (0.0, 0.0, 0.0),
  182. "gates": []
  183. })
  184. class _DummyConstellation(_Constellation):
  185. """Represents an unknown or invalid constellation."""
  186. def __init__(self, universe):
  187. super().__init__(universe, -1, {
  188. "name": "Unknown",
  189. "region": -1
  190. })
  191. class _DummyRegion(_Region):
  192. """Represents an unknown or invalid region."""
  193. def __init__(self, universe):
  194. super().__init__(universe, -1, {
  195. "name": "Unknown"
  196. })
  197. class _DummyFaction(_Faction):
  198. """Represents an unknown or invalid faction."""
  199. def __init__(self, universe):
  200. super().__init__(universe, -1, {
  201. "name": "Unknown"
  202. })
  203. class _DummyType(_Type):
  204. """Represents an unknown or invalid type."""
  205. def __init__(self, universe):
  206. super().__init__(universe, -1, {
  207. "name": "Unknown",
  208. "group_id": -1,
  209. "market_group_id": -1
  210. })
  211. class _DummyKillable(_Killable):
  212. """Represents an unknown or invalid killable object."""
  213. def __init__(self, universe):
  214. super().__init__(universe, -1, None, {
  215. "name": "Unknown",
  216. "group": "Unknown"
  217. })
  218. class Universe:
  219. """EVE API module for static universe data."""
  220. def __init__(self, datadir):
  221. self._dir = datadir
  222. self._lock = Lock()
  223. self._loaded = False
  224. self._systems = {}
  225. self._constellations = {}
  226. self._regions = {}
  227. self._factions = {}
  228. self._types = {}
  229. self._killable_idx = {}
  230. self._killable_tab = {}
  231. @staticmethod
  232. def _load_yaml(path):
  233. """Load in and return a YAML file with the given path."""
  234. with gzip.open(str(path), "rb") as fp:
  235. return yaml.load(fp, Loader=yaml.CLoader)
  236. def _load(self):
  237. """Load in universe data. This can be called multiple times safely."""
  238. if self._loaded:
  239. return
  240. with self._lock:
  241. if self._loaded:
  242. return
  243. galaxy = self._load_yaml(self._dir / "galaxy.yml.gz")
  244. self._systems = galaxy["systems"]
  245. self._constellations = galaxy["constellations"]
  246. self._regions = galaxy["regions"]
  247. del galaxy
  248. entities = self._load_yaml(self._dir / "entities.yml.gz")
  249. self._factions = entities["factions"]
  250. del entities
  251. self._types = self._load_yaml(self._dir / "types.yml.gz")
  252. killables = self._load_yaml(self._dir / "killables.yml.gz")
  253. self._killable_idx = {kid: cat for cat, kids in killables.items()
  254. for kid in kids}
  255. self._killable_tab = killables
  256. del killables
  257. self._loaded = True
  258. def system(self, sid):
  259. """Return a _SolarSystem with the given ID.
  260. If the ID is invalid, return a dummy unknown object with ID -1.
  261. """
  262. self._load()
  263. if sid not in self._systems:
  264. return _DummySolarSystem(self)
  265. return _SolarSystem(self, sid, self._systems[sid])
  266. def systems(self):
  267. """Return an iterator over all _SolarSystems."""
  268. self._load()
  269. for sid in self._systems:
  270. yield self.system(sid)
  271. def constellation(self, cid):
  272. """Return a _Constellation with the given ID.
  273. If the ID is invalid, return a dummy unknown object with ID -1.
  274. """
  275. self._load()
  276. if cid not in self._constellations:
  277. return _DummyConstellation(self)
  278. return _Constellation(self, cid, self._constellations[cid])
  279. def constellations(self):
  280. """Return an iterator over all _Constellations."""
  281. self._load()
  282. for cid in self._constellations:
  283. yield self.constellation(cid)
  284. def region(self, rid):
  285. """Return a _Region with the given ID.
  286. If the ID is invalid, return a dummy unknown object with ID -1.
  287. """
  288. self._load()
  289. if rid not in self._regions:
  290. return _DummyRegion(self)
  291. return _Region(self, rid, self._regions[rid])
  292. def regions(self):
  293. """Return an iterator over all _Regions."""
  294. self._load()
  295. for rid in self._regions:
  296. yield self.region(rid)
  297. def faction(self, fid):
  298. """Return a _Faction with the given ID.
  299. If the ID is invalid, return a dummy unknown object with ID -1.
  300. """
  301. self._load()
  302. if fid not in self._factions:
  303. return _DummyFaction(self)
  304. return _Faction(self, fid, self._factions[fid])
  305. def factions(self):
  306. """Return an iterator over all _Factions."""
  307. self._load()
  308. for fid in self._factions:
  309. yield self.faction(fid)
  310. def type(self, tid):
  311. """Return a _Type with the given ID.
  312. If the ID is invalid, return a dummy unknown object with ID -1.
  313. """
  314. self._load()
  315. if tid not in self._types:
  316. return _DummyKillable(self)
  317. return _Type(self, tid, self._types[tid])
  318. def killable(self, kid):
  319. """Return a _Killable with the given ID.
  320. If the ID is invalid, return a dummy unknown object with ID -1.
  321. """
  322. self._load()
  323. if kid not in self._killable_idx:
  324. return _DummyKillable(self)
  325. cat = self._killable_idx[kid]
  326. return _Killable(self, kid, cat, self._killable_tab[cat][kid])