mcp.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  1. from twisted.internet import reactor
  2. from twisted.internet import task
  3. from twisted.internet import defer
  4. from colorama import Fore, Back, Style
  5. from twisted.python import log
  6. """ This entire object, will likely be replaced with something
  7. smaller, simpler, and much more reliable.
  8. """
  9. class MCP(object):
  10. def __init__(self, game):
  11. self.game = game
  12. self.queue_game = None
  13. # we don't have this .. yet!
  14. self.prompt = None
  15. self.observer = None
  16. self.keepalive = None
  17. # Port Data
  18. self.portdata = None
  19. self.portcycle = None
  20. def finishSetup(self):
  21. # if self.queue_game is None:
  22. self.queue_game = self.game.queue_game
  23. # if self.observer is None:
  24. self.observer = self.game.observer
  25. self.observer.connect("hotkey", self.activate)
  26. self.observer.connect("notyet", self.notyet)
  27. self.observer.connect("close", self.close)
  28. def close(self, _):
  29. if self.keepalive:
  30. if self.keepalive.running:
  31. self.keepalive.stop()
  32. def notyet(self, _):
  33. """ No, not yet! """
  34. nl = "\n\r"
  35. r = Style.RESET_ALL
  36. log.msg("NNY!")
  37. prompt = self.game.buffer
  38. self.queue_game.put(
  39. r
  40. + nl
  41. + Style.BRIGHT
  42. + "Proxy:"
  43. + Style.RESET_ALL
  44. + " I can't activate at this time."
  45. + nl
  46. )
  47. self.queue_game.put(prompt)
  48. def stayAwake(self):
  49. """ Send a space to the game to keep it alive/don't timeout. """
  50. log.msg("Gameserver, stay awake.")
  51. self.game.queue_player.put(" ")
  52. def startAwake(self):
  53. """ Start the task that keeps the game server alive.
  54. There is currently a bug in there somewhere, which causes it to
  55. duplicate:
  56. 2019-11-25 21:19:03-0500 [-] Gameserver, stay awake.
  57. 2019-11-25 21:19:14-0500 [-] Gameserver, stay awake.
  58. 2019-11-25 21:19:24-0500 [-] Gameserver, stay awake.
  59. 2019-11-25 21:19:27-0500 [-] Gameserver, stay awake.
  60. 2019-11-25 21:19:31-0500 [-] Gameserver, stay awake.
  61. 2019-11-25 21:19:33-0500 [-] Gameserver, stay awake.
  62. 2019-11-25 21:19:44-0500 [-] Gameserver, stay awake.
  63. 2019-11-25 21:19:54-0500 [-] Gameserver, stay awake.
  64. 2019-11-25 21:19:57-0500 [-] Gameserver, stay awake.
  65. 2019-11-25 21:20:01-0500 [-] Gameserver, stay awake.
  66. 2019-11-25 21:20:03-0500 [-] Gameserver, stay awake.
  67. ^ These aren't 30 seconds apart.
  68. These are being sent, even when the MCP is not active!
  69. """
  70. self.keepalive = task.LoopingCall(self.stayAwake)
  71. self.keepalive.start(30)
  72. def activate(self, _):
  73. log.msg("MCP menu called.")
  74. # We want the raw one, not the ANSI cleaned getPrompt.
  75. prompt = self.game.buffer
  76. if not self.prompt is None:
  77. # silly, we're already active
  78. log.msg("I think we're already active. Ignoring request.")
  79. return
  80. # Or will the caller setup/restore the prompt?
  81. self.prompt = prompt
  82. # queue_game = to player
  83. self.displayMenu()
  84. self.observer.connect("player", self.fromPlayer)
  85. # TODO: Add background "keepalive" event so the game doesn't time out on us.
  86. self.startAwake()
  87. # self.keepalive = task.LoopingCall(self.stayAwake)
  88. # self.keepalive.start(30)
  89. def displayMenu(self):
  90. nl = "\n\r"
  91. c = merge(Style.BRIGHT + Fore.YELLOW + Back.BLUE)
  92. r = Style.RESET_ALL
  93. c1 = merge(Style.BRIGHT + Fore.BLUE)
  94. c2 = merge(Style.NORMAL + Fore.BLUE)
  95. self.queue_game.put(nl + c + "TradeWars Proxy active." + r + nl)
  96. self.queue_game.put(" " + c1 + "D" + c2 + " - " + c1 + "Diagnostics" + nl)
  97. self.queue_game.put(
  98. " " + c1 + "T" + c2 + " - " + c1 + "Display current Time" + nl
  99. )
  100. self.queue_game.put(" " + c1 + "P" + c2 + " - " + c1 + "Port CIM Report" + nl)
  101. self.queue_game.put(" " + c1 + "S" + c2 + " - " + c1 + "Scripts" + nl)
  102. self.queue_game.put(" " + c1 + "X" + c2 + " - " + c1 + "eXit" + nl)
  103. self.queue_game.put(" " + c + "-=>" + r + " ")
  104. def fromPlayer(self, chunk):
  105. """ Data from player (in bytes). """
  106. chunk = chunk.decode("utf-8", "ignore")
  107. nl = "\n\r"
  108. c = merge(Style.BRIGHT + Fore.YELLOW + Back.BLUE)
  109. r = Style.RESET_ALL
  110. c1 = merge(Style.BRIGHT + Fore.BLUE)
  111. c2 = merge(Style.NORMAL + Fore.BLUE)
  112. key = chunk.upper()
  113. if key == "T":
  114. self.queue_game.put(c + key + r + nl)
  115. now = pendulum.now()
  116. log.msg("Time")
  117. self.queue_game.put(
  118. nl + c1 + "It is currently " + now.to_datetime_string() + "." + nl
  119. )
  120. self.displayMenu()
  121. elif key == "P":
  122. log.msg("Port")
  123. self.queue_game.put(c + key + r + nl)
  124. self.portReport()
  125. # self.queue_game.put(nl + c + "NO, NOT YET!" + r + nl)
  126. # self.displayMenu()
  127. elif key == "S":
  128. log.msg("Scripts")
  129. self.queue_game.put(c + key + r + nl)
  130. self.scripts()
  131. elif key == "D":
  132. self.queue_game.put(nl + "Diagnostics" + nl + "portdata:" + nl)
  133. line = pformat(self.portdata).replace("\n", "\n\r")
  134. self.queue_game.put(line + nl)
  135. self.displayMenu()
  136. elif key == "X":
  137. log.msg('"Quit, return to "normal". (Whatever that means!)')
  138. self.queue_game.put(c + key + r + nl)
  139. self.observer.disconnect("player", self.fromPlayer)
  140. self.queue_game.put(nl + c1 + "Returning to game" + c2 + "..." + r + nl)
  141. self.queue_game.put(self.prompt)
  142. self.prompt = None
  143. self.keepalive.stop()
  144. self.keepalive = None
  145. self.game.to_player = True
  146. else:
  147. if key.isprintable():
  148. self.queue_game.put(r + nl)
  149. self.queue_game.put("Excuse me? I don't understand '" + key + "'." + nl)
  150. self.displayMenu()
  151. def portReport(self):
  152. """ Activate CIM and request Port Report """
  153. self.game.to_player = False
  154. self.portdata = None
  155. self.observer.connect("prompt", self.portPrompt)
  156. self.observer.connect("game-line", self.portParse)
  157. self.game.queue_player.put("^")
  158. def portPrompt(self, prompt):
  159. if prompt == ": ":
  160. log.msg("CIM Prompt")
  161. if self.portdata is None:
  162. log.msg("R - Port Report")
  163. self.portdata = dict()
  164. self.game.queue_player.put("R")
  165. self.portcycle = cycle(["/", "-", "\\", "|"])
  166. self.queue_game.put(" ")
  167. else:
  168. log.msg("Q - Quit")
  169. self.game.queue_player.put("Q")
  170. self.portcycle = None
  171. def portBS(self, info):
  172. if info[0] == "-":
  173. bs = "B"
  174. else:
  175. bs = "S"
  176. return (bs, int(info[1:].strip()))
  177. def portParse(self, line):
  178. if line == "":
  179. return
  180. if line == ": ":
  181. return
  182. log.msg("parse line:", line)
  183. if line.startswith("Command [TL="):
  184. return
  185. if line == ": ENDINTERROG":
  186. log.msg("CIM Done")
  187. log.msg(pformat(self.portdata))
  188. self.queue_game.put("\b \b" + "\n\r")
  189. self.observer.disconnect("prompt", self.portPrompt)
  190. self.observer.disconnect("game-line", self.portParse)
  191. self.game.to_player = True
  192. # self.keepalive.start(30)
  193. self.startAwake()
  194. self.displayMenu()
  195. return
  196. # Give some sort of feedback to the user.
  197. if self.portcycle:
  198. if len(self.portdata) % 10 == 0:
  199. self.queue_game.put("\b" + next(self.portcycle))
  200. # Ok, we need to parse this line
  201. # 436 2870 100% - 1520 100% - 2820 100%
  202. # 2 1950 100% - 1050 100% 2780 100%
  203. # 5 2800 100% - 2330 100% - 1230 100%
  204. # 8 2890 100% 1530 100% - 2310 100%
  205. # 9 - 2160 100% 2730 100% - 2120 100%
  206. # 324 - 2800 100% 2650 100% - 2490 100%
  207. # 492 990 100% 900 100% 1660 100%
  208. # 890 1920 100% - 2140 100% 1480 100%
  209. # 1229 - 2870 100% - 1266 90% 728 68%
  210. # 1643 - 3000 100% - 3000 100% - 3000 100%
  211. # 1683 - 1021 97% 1460 100% - 2620 100%
  212. # 1898 - 1600 100% - 1940 100% - 1860 100%
  213. # 2186 1220 100% - 900 100% - 1840 100%
  214. # 2194 2030 100% - 1460 100% - 1080 100%
  215. # 2577 2810 100% - 1550 100% - 2350 100%
  216. # 2629 2570 100% - 2270 100% - 1430 100%
  217. # 3659 - 1720 100% 1240 100% - 2760 100%
  218. # 3978 - 920 100% 2560 100% - 2590 100%
  219. # 4302 348 25% - 2530 100% - 316 23%
  220. # 4516 - 1231 60% - 1839 75% 7 0%
  221. work = line.replace("%", "")
  222. parts = re.split(r"(?<=\d)\s", work)
  223. if len(parts) == 8:
  224. port = int(parts[0].strip())
  225. data = dict()
  226. data["fuel"] = dict()
  227. data["fuel"]["sale"], data["fuel"]["units"] = self.portBS(parts[1])
  228. data["fuel"]["pct"] = int(parts[2].strip())
  229. data["org"] = dict()
  230. data["org"]["sale"], data["org"]["units"] = self.portBS(parts[3])
  231. data["org"]["pct"] = int(parts[4].strip())
  232. data["equ"] = dict()
  233. data["equ"]["sale"], data["equ"]["units"] = self.portBS(parts[5])
  234. data["equ"]["pct"] = int(parts[6].strip())
  235. # Store what this port is buying/selling
  236. data["port"] = (
  237. data["fuel"]["sale"] + data["org"]["sale"] + data["equ"]["sale"]
  238. )
  239. # Convert BBS/SBB to Class number 1-8
  240. data["class"] = CLASSES_PORT[data["port"]]
  241. self.portdata[port] = data
  242. else:
  243. self.queue_game.put("?")
  244. log.msg("Line in question is: [{0}].".format(line))
  245. log.msg(repr(parts))
  246. def scripts(self):
  247. self.script = dict()
  248. self.observer.disconnect("player", self.fromPlayer)
  249. self.observer.connect("player", self.scriptFromPlayer)
  250. self.observer.connect("game-line", self.scriptLine)
  251. nl = "\n\r"
  252. c = merge(Style.BRIGHT + Fore.WHITE + Back.BLUE)
  253. r = Style.RESET_ALL
  254. c1 = merge(Style.BRIGHT + Fore.CYAN)
  255. c2 = merge(Style.NORMAL + Fore.CYAN)
  256. self.queue_game.put(nl + c + "TradeWars Proxy Script(s)" + r + nl)
  257. self.queue_game.put(" " + c1 + "P" + c2 + " - " + c1 + "Port Trading Pair" + nl)
  258. self.queue_game.put(" " + c + "-=>" + r + " ")
  259. def unscript(self):
  260. self.observer.connect("player", self.fromPlayer)
  261. self.observer.disconnect("player", self.scriptFromPlayer)
  262. self.observer.disconnect("game-line", self.scriptLine)
  263. self.displayMenu()
  264. def scriptLine(self, line):
  265. pass
  266. def scriptFromPlayer(self, chunk):
  267. """ Data from player (in bytes). """
  268. chunk = chunk.decode("utf-8", "ignore")
  269. nl = "\n\r"
  270. c = merge(Style.BRIGHT + Fore.WHITE + Back.BLUE)
  271. r = Style.RESET_ALL
  272. key = chunk.upper()
  273. if key == "Q":
  274. self.queue_game.put(c + key + r + nl)
  275. self.observer.connect("player", self.fromPlayer)
  276. self.observer.disconnect("player", self.scriptFromPlayer)
  277. self.observer.disconnect("game-line", self.scriptLine)
  278. self.observer.connect("game-line", self.portParse)
  279. self.displayMenu()
  280. elif key == "P":
  281. self.queue_game.put(c + key + r + nl)
  282. d = self.playerInput("Enter sector to trade to: ", 6)
  283. d.addCallback(self.save_sector)
  284. def save_sector(self, sector):
  285. log.msg("save_sector {0}".format(sector))
  286. if sector.strip() == "":
  287. self.queue_game.put("Script Aborted.")
  288. self.unscript()
  289. return
  290. s = int(sector.strip())
  291. self.script["sector"] = s
  292. d = self.playerInput("Enter times to execute script: ", 6)
  293. d.addCallback(self.save_loop)
  294. def save_loop(self, loop):
  295. log.msg("save_loop {0}".format(loop))
  296. if loop.strip() == "":
  297. self.queue_game.put("Script Aborted.")
  298. self.unscript()
  299. return
  300. l = int(loop.strip())
  301. self.script["loop"] = l
  302. d = self.playerInput("Enter markup/markdown percentage: ", 3)
  303. d.addCallback(self.save_mark)
  304. def save_mark(self, mark):
  305. log.msg("save_mark {0}".format(mark))
  306. if mark.strip() == "":
  307. self.script["mark"] = 5
  308. else:
  309. self.script["mark"] = int(mark.strip())
  310. # Ok, we have the values we need to run the Port trade script
  311. self.queue_game.put(pformat(self.script).replace("\n", "\n\r"))
  312. self.unscript()
  313. def playerInput(self, prompt, limit):
  314. """ Given a prompt and limit, this handles user input.
  315. This displays the prompt, and sets up the proper
  316. observers, while preserving the "current" ones.
  317. This returns a deferred, so you can chain the results
  318. of this.
  319. """
  320. log.msg("playerInput({0}, {1}".format(prompt, limit))
  321. nl = "\n\r"
  322. c = merge(Style.BRIGHT + Fore.WHITE + Back.BLUE)
  323. r = Style.RESET_ALL
  324. self.queue_game.put(r + nl + c + prompt)
  325. # This should set the background and show the size of the entry area.
  326. self.queue_game.put(" " * limit + "\b" * limit)
  327. d = defer.Deferred()
  328. self.player_input = d
  329. self.input_limit = limit
  330. self.input_input = ""
  331. self.save = self.observer.save()
  332. self.observer.connect("player", self.niceInput)
  333. # input_funcs = { 'player': [self.niceInput] }
  334. # self.observer.set_funcs(input_funcs)
  335. return d
  336. def niceInput(self, chunk):
  337. """ Data from player (in bytes). """
  338. chunk = chunk.decode("utf-8", "ignore")
  339. # log.msg("niceInput:", repr(chunk))
  340. r = Style.RESET_ALL
  341. for c in chunk:
  342. if c == "\b":
  343. # Backspace
  344. if len(self.input_input) > 0:
  345. self.queue_game.put("\b \b")
  346. self.input_input = self.input_input[0:-1]
  347. else:
  348. # Can't
  349. self.queue_game.put("\a")
  350. if c == "\r":
  351. # Ok, completed!
  352. self.queue_game.put(r + "\n\r")
  353. self.observer.load(self.save)
  354. self.save = None
  355. line = self.input_input
  356. log.msg("finishing niceInput {0}".format(line))
  357. # self.queue_game.put("[{0}]\n\r".format(line))
  358. self.input_input = ""
  359. # Ok, maybe this isn't the way to do this ...
  360. # self.player_input.callback(line)
  361. reactor.callLater(0, self.player_input.callback, line)
  362. self.player_input = None
  363. if c.isprintable():
  364. if len(self.input_input) + 1 <= self.input_limit:
  365. self.input_input += c
  366. self.queue_game.put(c)
  367. else:
  368. # Limit reached
  369. self.queue_game.put("\a")