tcp-proxy.py 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919
  1. #!/usr/bin/env python3
  2. import sys
  3. import re
  4. from twisted.internet import defer
  5. from twisted.internet import protocol
  6. from twisted.internet import reactor
  7. from twisted.internet import task
  8. from twisted.python import log
  9. from twisted.python.logfile import DailyLogFile
  10. import pendulum
  11. from subprocess import check_output
  12. from colorama import Fore, Back, Style
  13. from itertools import cycle
  14. from deprecated import deprecated
  15. from pprint import pformat
  16. # This isn't the best configuration, but it's simple
  17. # and works. Mostly.
  18. try:
  19. from config_dev import *
  20. except ModuleNotFoundError:
  21. from config import *
  22. # Extract the version information from git.
  23. # The match gives us only tags starting with v[0-9]* Using anything else trips up on double digits.
  24. version = check_output(
  25. [
  26. "git",
  27. "describe",
  28. "--abbrev=8",
  29. "--long",
  30. "--tags",
  31. "--dirty",
  32. "--always",
  33. "--match",
  34. "v[0-9]*",
  35. ],
  36. universal_newlines=True,
  37. ).strip()
  38. def merge(color_string):
  39. """ Given a string of colorama ANSI, merge them if you can. """
  40. return color_string.replace("m\x1b[", ";")
  41. # https://en.wikipedia.org/wiki/ANSI_escape_code
  42. # Cleans all ANSI
  43. cleaner = re.compile(r"\x1b\[[0-9;]*[A-Zmh]")
  44. # Looks for ANSI (that should be considered to be a newline)
  45. # This needs to see what is send when something enters / leaves
  46. # the player's current sector. (That doesn't work/isn't
  47. # detected. NNY!) It is "\x1b[K" Erase in Line!
  48. makeNL = re.compile(r"\x1b\[[0-9;]*[JK]")
  49. def treatAsNL(line):
  50. """ Replace any ANSI codes that would be better understood as newlines. """
  51. global makeNL
  52. return makeNL.sub("\n", line)
  53. def cleanANSI(line):
  54. """ Remove all ANSI codes. """
  55. global cleaner
  56. return cleaner.sub("", line)
  57. # return re.sub(r'\x1b\[([0-9,A-Z]{1,2}(;[0-9]{1,2})?(;[0-9]{3})?)?[m|K]?', '', line)
  58. PORT_CLASSES = { 1: 'BBS', 2: 'BSB', 3: 'SBB', 4:'SSB', 5:'SBS', 6:'BSS', 7:'SSS', 8:'BBB'}
  59. CLASSES_PORT = { v: k for k,v in PORT_CLASSES.items() }
  60. from observer import Observer
  61. class PlayerInput(object):
  62. def __init__(self, game):
  63. # I think game gives us access to everything we need
  64. self.game = game
  65. self.observer = self.game.observer
  66. self.save = None
  67. self.defer = None
  68. self.game_queue = game.game_queue
  69. # default colors, and useful consts
  70. self.c = merge(Style.BRIGHT + Fore.WHITE + Back.BLUE)
  71. self.r = Style.RESET_ALL
  72. self.nl = "\n\r"
  73. self.bsb = "\b \b"
  74. self.keepalive = None
  75. def color(self, c):
  76. self.c = c
  77. def awake(self):
  78. log.msg("PlayerInput.awake()")
  79. self.game.queue_player.put(" ")
  80. def prompt(self, prompt, limit, default=''):
  81. log.msg("PlayerInput({0}, {1}, {2}".format(prompt, limit, default))
  82. self.prompt = prompt
  83. self.limit = limit
  84. self.default = default
  85. self.input = ''
  86. assert(self.save is None)
  87. assert(self.keepalive is None)
  88. # Note: This clears out the server "keep alive"
  89. self.save = self.observer.save()
  90. self.observer.connect('player', self.input)
  91. self.keepalive = task.LoopingCall(self.awake)
  92. self.keepalive.start(30)
  93. # We need to "hide" the game output.
  94. # Otherwise it WITH mess up the user input display.
  95. self.to_player = self.game.to_player
  96. self.game.to_player = False
  97. # Display prompt
  98. self.queue_game.put(self.r + self.nl + self.c + prompt)
  99. # Set "Background of prompt"
  100. self.queue_game.put( " " * limit + "\b" * limit)
  101. assert(self.defer is None)
  102. d = defer.Deferred()
  103. self.defer = d
  104. return d
  105. def input(self, chunk):
  106. """ Data from player (in bytes) """
  107. chunk = chunk.decode('utf-8', 'ignore')
  108. for ch in chunk:
  109. if ch == "\b":
  110. if len(self.input) > 0:
  111. self.queue_game.put(self.bsb)
  112. self.input = self.input[0:-1]
  113. else:
  114. self.queue_game.put("\a")
  115. if ch == "'\r":
  116. self.queue_game.put(self.r + self.nl)
  117. assert(not self.save is None)
  118. self.observer.load(self.save)
  119. self.save = None
  120. self.keepalive.stop()
  121. self.keepalive = None
  122. line = self.input
  123. self.input = ''
  124. assert(not self.defer is None)
  125. reactor.callLater(0, self.defer.callback, line)
  126. self.defer = None
  127. if ch.isprintable():
  128. if len(self.input) + 1 <= self.limit:
  129. self.input += c
  130. self.queue_game.put(ch)
  131. else:
  132. self.queue_game.put("\a")
  133. def output(self, line):
  134. """ A default display of what they just input. """
  135. log.msg("PlayerInput.output({0})".format(line))
  136. self.game.queue_game.put(self.r + self.nl + "[{0}]".format(line) + self.nl)
  137. return line
  138. class ProxyMenu(object):
  139. def __init__(self, game):
  140. self.nl = "\n\r"
  141. self.c = merge(Style.BRIGHT + Fore.YELLOW + Back.BLUE)
  142. self.r = Style.RESET_ALL
  143. self.c1 = merge(Style.BRIGHT + Fore.BLUE)
  144. self.c2 = merge(Style.NORMAL + Fore.BLUE)
  145. self.game = game
  146. self.queue_game = game.queue_game
  147. self.observer = game.observer
  148. # Yes, at this point we would activate
  149. self.prompt = game.buffer
  150. self.save = self.observer.save()
  151. self.observer.connect("player", self.player)
  152. # If we want it, it's here.
  153. self.defer = None
  154. self.keepalive = task.LoopingCall(self.awake)
  155. self.keepalive.start(30)
  156. self.menu()
  157. def __del__(self):
  158. log.msg("ProxyMenu {0} RIP".format(self))
  159. def whenDone(self):
  160. self.defer = defer.Deferred()
  161. # Call this to chain something after we exit.
  162. return self.defer
  163. def menu(self):
  164. self.queue_game.put(self.nl + self.c + "TradeWars Proxy active." + self.r + self.nl)
  165. self.queue_game.put(" " + self.c1 + "D" + self.c2 + " - " + self.c1 + "Diagnostics" + self.nl)
  166. self.queue_game.put(
  167. " " + self.c1 + "T" + self.c2 + " - " + self.c1 + "Display current Time" + self.nl
  168. )
  169. self.queue_game.put(" " + self.c1 + "P" + self.c2 + " - " + self.c1 + "Port CIM Report" + self.nl)
  170. self.queue_game.put(" " + self.c1 + "S" + self.c2 + " - " + self.c1 + "Scripts" + self.nl)
  171. self.queue_game.put(" " + self.c1 + "X" + self.c2 + " - " + self.c1 + "eXit" + self.nl)
  172. self.queue_game.put(" " + self.c + "-=>" + self.r + " ")
  173. def awake(self):
  174. log.msg("ProxyMenu.awake()")
  175. self.game.queue_player.put(" ")
  176. def player(self, chunk):
  177. """ Data from player (in bytes). """
  178. chunk = chunk.decode("utf-8", 'ignore')
  179. key = chunk.upper()
  180. log.msg("ProxyMenu.player({0})".format(key))
  181. # Weird / long running task / something odd happening here?
  182. self.keepalive.stop()
  183. if key == "T":
  184. self.queue_game.put(self.c + key + self.r + self.nl)
  185. # perform T option
  186. now = pendulum.now()
  187. self.queue_game.put(self.nl + self.c1 + "Current time " + now.to_datetime_string() + self.nl)
  188. elif key == 'X':
  189. self.queue_game.put(self.c + key + self.r + self.nl)
  190. self.observer.load(self.save)
  191. self.save = None
  192. # It isn't running (NOW), so don't try to stop it.
  193. # self.keepalive.stop()
  194. self.keepalive = None
  195. self.queue_game.put(self.prompt)
  196. self.prompt = None
  197. if self.defer:
  198. self.defer.callback()
  199. self.defer = None
  200. return
  201. self.keepalive.start(30, True)
  202. self.menu()
  203. class MCP(object):
  204. def __init__(self, game):
  205. self.game = game
  206. self.queue_game = None
  207. # we don't have this .. yet!
  208. self.prompt = None
  209. self.observer = None
  210. self.keepalive = None
  211. # Port Data
  212. self.portdata = None
  213. self.portcycle = None
  214. def finishSetup(self):
  215. # if self.queue_game is None:
  216. self.queue_game = self.game.queue_game
  217. # if self.observer is None:
  218. self.observer = self.game.observer
  219. self.observer.connect("hotkey", self.activate)
  220. self.observer.connect("notyet", self.notyet)
  221. self.observer.connect('close', self.close)
  222. def close(self, _):
  223. if self.keepalive:
  224. if self.keepalive.running:
  225. self.keepalive.stop()
  226. def notyet(self, _):
  227. """ No, not yet! """
  228. nl = "\n\r"
  229. r = Style.RESET_ALL
  230. log.msg("NNY!")
  231. prompt = self.game.buffer
  232. self.queue_game.put(r + nl + Style.BRIGHT + "Proxy:" + Style.RESET_ALL + " I can't activate at this time." + nl)
  233. self.queue_game.put(prompt)
  234. def stayAwake(self):
  235. """ Send a space to the game to keep it alive/don't timeout. """
  236. log.msg("Gameserver, stay awake.")
  237. self.game.queue_player.put(" ")
  238. def startAwake(self):
  239. """ Start the task that keeps the game server alive.
  240. There is currently a bug in there somewhere, which causes it to
  241. duplicate:
  242. 2019-11-25 21:19:03-0500 [-] Gameserver, stay awake.
  243. 2019-11-25 21:19:14-0500 [-] Gameserver, stay awake.
  244. 2019-11-25 21:19:24-0500 [-] Gameserver, stay awake.
  245. 2019-11-25 21:19:27-0500 [-] Gameserver, stay awake.
  246. 2019-11-25 21:19:31-0500 [-] Gameserver, stay awake.
  247. 2019-11-25 21:19:33-0500 [-] Gameserver, stay awake.
  248. 2019-11-25 21:19:44-0500 [-] Gameserver, stay awake.
  249. 2019-11-25 21:19:54-0500 [-] Gameserver, stay awake.
  250. 2019-11-25 21:19:57-0500 [-] Gameserver, stay awake.
  251. 2019-11-25 21:20:01-0500 [-] Gameserver, stay awake.
  252. 2019-11-25 21:20:03-0500 [-] Gameserver, stay awake.
  253. ^ These aren't 30 seconds apart.
  254. These are being sent, even when the MCP is not active!
  255. """
  256. self.keepalive = task.LoopingCall(self.stayAwake)
  257. self.keepalive.start(30)
  258. def activate(self, _):
  259. log.msg("MCP menu called.")
  260. # We want the raw one, not the ANSI cleaned getPrompt.
  261. prompt = self.game.buffer
  262. if not self.prompt is None:
  263. # silly, we're already active
  264. log.msg("I think we're already active. Ignoring request.")
  265. return
  266. # Or will the caller setup/restore the prompt?
  267. self.prompt = prompt
  268. # queue_game = to player
  269. self.displayMenu()
  270. self.observer.connect("player", self.fromPlayer)
  271. # TODO: Add background "keepalive" event so the game doesn't time out on us.
  272. self.startAwake()
  273. # self.keepalive = task.LoopingCall(self.stayAwake)
  274. # self.keepalive.start(30)
  275. def displayMenu(self):
  276. nl = "\n\r"
  277. c = merge(Style.BRIGHT + Fore.YELLOW + Back.BLUE)
  278. r = Style.RESET_ALL
  279. c1 = merge(Style.BRIGHT + Fore.BLUE)
  280. c2 = merge(Style.NORMAL + Fore.BLUE)
  281. self.queue_game.put(nl + c + "TradeWars Proxy active." + r + nl)
  282. self.queue_game.put(" " + c1 + "D" + c2 + " - " + c1 + "Diagnostics" + nl)
  283. self.queue_game.put(
  284. " " + c1 + "T" + c2 + " - " + c1 + "Display current Time" + nl
  285. )
  286. self.queue_game.put(" " + c1 + "P" + c2 + " - " + c1 + "Port CIM Report" + nl)
  287. self.queue_game.put(" " + c1 + "S" + c2 + " - " + c1 + "Scripts" + nl)
  288. self.queue_game.put(" " + c1 + "X" + c2 + " - " + c1 + "eXit" + nl)
  289. self.queue_game.put(" " + c + "-=>" + r + " ")
  290. def fromPlayer(self, chunk):
  291. """ Data from player (in bytes). """
  292. chunk = chunk.decode("utf-8", "ignore")
  293. nl = "\n\r"
  294. c = merge(Style.BRIGHT + Fore.YELLOW + Back.BLUE)
  295. r = Style.RESET_ALL
  296. c1 = merge(Style.BRIGHT + Fore.BLUE)
  297. c2 = merge(Style.NORMAL + Fore.BLUE)
  298. key = chunk.upper()
  299. if key == "T":
  300. self.queue_game.put(c + key + r + nl)
  301. now = pendulum.now()
  302. log.msg("Time")
  303. self.queue_game.put(
  304. nl + c1 + "It is currently " + now.to_datetime_string() + "." + nl
  305. )
  306. self.displayMenu()
  307. elif key == "P":
  308. log.msg("Port")
  309. self.queue_game.put(c + key + r + nl)
  310. self.portReport()
  311. # self.queue_game.put(nl + c + "NO, NOT YET!" + r + nl)
  312. # self.displayMenu()
  313. elif key == "S":
  314. log.msg("Scripts")
  315. self.queue_game.put(c + key + r + nl)
  316. self.scripts()
  317. elif key == 'D':
  318. self.queue_game.put(nl + "Diagnostics" + nl + "portdata:" + nl)
  319. line = pformat(self.portdata).replace("\n", "\n\r")
  320. self.queue_game.put(line + nl)
  321. self.displayMenu()
  322. elif key == "X":
  323. log.msg('"Quit, return to "normal". (Whatever that means!)')
  324. self.queue_game.put(c + key + r + nl)
  325. self.observer.disconnect("player", self.fromPlayer)
  326. self.queue_game.put(nl + c1 + "Returning to game" + c2 + "..." + r + nl)
  327. self.queue_game.put(self.prompt)
  328. self.prompt = None
  329. self.keepalive.stop()
  330. self.keepalive = None
  331. self.game.to_player = True
  332. else:
  333. if key.isprintable():
  334. self.queue_game.put(r + nl)
  335. self.queue_game.put("Excuse me? I don't understand '" + key + "'." + nl)
  336. self.displayMenu()
  337. def portReport(self):
  338. """ Activate CIM and request Port Report """
  339. self.game.to_player = False
  340. self.portdata = None
  341. self.observer.connect('prompt', self.portPrompt)
  342. self.observer.connect('game-line', self.portParse)
  343. self.game.queue_player.put("^")
  344. def portPrompt(self, prompt):
  345. if prompt == ': ':
  346. log.msg("CIM Prompt")
  347. if self.portdata is None:
  348. log.msg("R - Port Report")
  349. self.portdata = dict()
  350. self.game.queue_player.put("R")
  351. self.portcycle = cycle(['/', '-', '\\', '|'])
  352. self.queue_game.put(' ')
  353. else:
  354. log.msg("Q - Quit")
  355. self.game.queue_player.put("Q")
  356. self.portcycle = None
  357. def portBS(self, info):
  358. if info[0] == '-':
  359. bs = 'B'
  360. else:
  361. bs = 'S'
  362. return (bs, int(info[1:].strip()))
  363. def portParse(self, line):
  364. if line == '':
  365. return
  366. if line == ': ':
  367. return
  368. log.msg("parse line:", line)
  369. if line.startswith('Command [TL='):
  370. return
  371. if line == ': ENDINTERROG':
  372. log.msg("CIM Done")
  373. log.msg(pformat(self.portdata))
  374. self.queue_game.put("\b \b" + "\n\r")
  375. self.observer.disconnect('prompt', self.portPrompt)
  376. self.observer.disconnect('game-line', self.portParse)
  377. self.game.to_player = True
  378. # self.keepalive.start(30)
  379. self.startAwake()
  380. self.displayMenu()
  381. return
  382. # Give some sort of feedback to the user.
  383. if self.portcycle:
  384. if len(self.portdata) % 10 == 0:
  385. self.queue_game.put("\b" + next(self.portcycle))
  386. # Ok, we need to parse this line
  387. # 436 2870 100% - 1520 100% - 2820 100%
  388. # 2 1950 100% - 1050 100% 2780 100%
  389. # 5 2800 100% - 2330 100% - 1230 100%
  390. # 8 2890 100% 1530 100% - 2310 100%
  391. # 9 - 2160 100% 2730 100% - 2120 100%
  392. # 324 - 2800 100% 2650 100% - 2490 100%
  393. # 492 990 100% 900 100% 1660 100%
  394. # 890 1920 100% - 2140 100% 1480 100%
  395. # 1229 - 2870 100% - 1266 90% 728 68%
  396. # 1643 - 3000 100% - 3000 100% - 3000 100%
  397. # 1683 - 1021 97% 1460 100% - 2620 100%
  398. # 1898 - 1600 100% - 1940 100% - 1860 100%
  399. # 2186 1220 100% - 900 100% - 1840 100%
  400. # 2194 2030 100% - 1460 100% - 1080 100%
  401. # 2577 2810 100% - 1550 100% - 2350 100%
  402. # 2629 2570 100% - 2270 100% - 1430 100%
  403. # 3659 - 1720 100% 1240 100% - 2760 100%
  404. # 3978 - 920 100% 2560 100% - 2590 100%
  405. # 4302 348 25% - 2530 100% - 316 23%
  406. # 4516 - 1231 60% - 1839 75% 7 0%
  407. work = line.replace('%', '')
  408. parts = re.split(r"(?<=\d)\s", work)
  409. if len(parts) == 8:
  410. port = int(parts[0].strip())
  411. data = dict()
  412. data['fuel'] = dict( )
  413. data['fuel']['sale'], data['fuel']['units'] = self.portBS(parts[1])
  414. data['fuel']['pct'] = int(parts[2].strip())
  415. data['org'] = dict( )
  416. data['org']['sale'], data['org']['units'] = self.portBS(parts[3])
  417. data['org']['pct'] = int(parts[4].strip())
  418. data['equ'] = dict( )
  419. data['equ']['sale'], data['equ']['units'] = self.portBS(parts[5])
  420. data['equ']['pct'] = int(parts[6].strip())
  421. # Store what this port is buying/selling
  422. data['port'] = data['fuel']['sale'] + data['org']['sale'] + data['equ']['sale']
  423. # Convert BBS/SBB to Class number 1-8
  424. data['class'] = CLASSES_PORT[data['port']]
  425. self.portdata[port] = data
  426. else:
  427. self.queue_game.put("?")
  428. log.msg("Line in question is: [{0}].".format(line))
  429. log.msg(repr(parts))
  430. def scripts(self):
  431. self.script = dict()
  432. self.observer.disconnect("player", self.fromPlayer)
  433. self.observer.connect("player", self.scriptFromPlayer)
  434. self.observer.connect('game-line', self.scriptLine)
  435. nl = "\n\r"
  436. c = merge(Style.BRIGHT + Fore.WHITE + Back.BLUE)
  437. r = Style.RESET_ALL
  438. c1 = merge(Style.BRIGHT + Fore.CYAN)
  439. c2 = merge(Style.NORMAL + Fore.CYAN)
  440. self.queue_game.put(nl + c + "TradeWars Proxy Script(s)" + r + nl)
  441. self.queue_game.put(" " + c1 + "P" + c2 + " - " + c1 + "Port Trading Pair" + nl)
  442. self.queue_game.put(" " + c + "-=>" + r + " ")
  443. def unscript(self):
  444. self.observer.connect("player", self.fromPlayer)
  445. self.observer.disconnect("player", self.scriptFromPlayer)
  446. self.observer.disconnect('game-line', self.scriptLine)
  447. self.displayMenu()
  448. def scriptLine(self, line):
  449. pass
  450. def scriptFromPlayer(self, chunk):
  451. """ Data from player (in bytes). """
  452. chunk = chunk.decode("utf-8", "ignore")
  453. nl = "\n\r"
  454. c = merge(Style.BRIGHT + Fore.WHITE + Back.BLUE)
  455. r = Style.RESET_ALL
  456. key = chunk.upper()
  457. if key == 'Q':
  458. self.queue_game.put(c + key + r + nl)
  459. self.observer.connect("player", self.fromPlayer)
  460. self.observer.disconnect("player", self.scriptFromPlayer)
  461. self.observer.disconnect('game-line', self.scriptLine)
  462. self.observer.connect('game-line', self.portParse)
  463. self.displayMenu()
  464. elif key == 'P':
  465. self.queue_game.put(c + key + r + nl)
  466. d = self.playerInput("Enter sector to trade to: ", 6)
  467. d.addCallback(self.save_sector)
  468. def save_sector(self, sector):
  469. log.msg("save_sector {0}".format(sector))
  470. if sector.strip() == '':
  471. self.queue_game.put("Script Aborted.")
  472. self.unscript()
  473. return
  474. s = int(sector.strip())
  475. self.script['sector'] = s
  476. d = self.playerInput("Enter times to execute script: ", 6)
  477. d.addCallback(self.save_loop)
  478. def save_loop(self, loop):
  479. log.msg("save_loop {0}".format(loop))
  480. if loop.strip() == '':
  481. self.queue_game.put("Script Aborted.")
  482. self.unscript()
  483. return
  484. l = int(loop.strip())
  485. self.script['loop'] = l
  486. d = self.playerInput("Enter markup/markdown percentage: ", 3)
  487. d.addCallback(self.save_mark)
  488. def save_mark(self, mark):
  489. log.msg("save_mark {0}".format(mark))
  490. if mark.strip() == "":
  491. self.script['mark'] = 5
  492. else:
  493. self.script['mark'] = int(mark.strip())
  494. # Ok, we have the values we need to run the Port trade script
  495. self.queue_game.put(pformat(self.script).replace("\n", "\n\r"))
  496. self.unscript()
  497. def playerInput(self, prompt, limit):
  498. """ Given a prompt and limit, this handles user input.
  499. This displays the prompt, and sets up the proper
  500. observers, while preserving the "current" ones.
  501. This returns a deferred, so you can chain the results
  502. of this.
  503. """
  504. log.msg("playerInput({0}, {1}".format(prompt, limit))
  505. nl = "\n\r"
  506. c = merge(Style.BRIGHT + Fore.WHITE + Back.BLUE)
  507. r = Style.RESET_ALL
  508. self.queue_game.put(r + nl + c + prompt)
  509. # This should set the background and show the size of the entry area.
  510. self.queue_game.put(" " * limit + "\b" * limit)
  511. d = defer.Deferred()
  512. self.player_input = d
  513. self.input_limit = limit
  514. self.input_input = ''
  515. self.save = self.observer.save()
  516. self.observer.connect('player', self.niceInput)
  517. # input_funcs = { 'player': [self.niceInput] }
  518. # self.observer.set_funcs(input_funcs)
  519. return d
  520. def niceInput(self, chunk):
  521. """ Data from player (in bytes). """
  522. chunk = chunk.decode("utf-8", "ignore")
  523. # log.msg("niceInput:", repr(chunk))
  524. r = Style.RESET_ALL
  525. for c in chunk:
  526. if c == '\b':
  527. # Backspace
  528. if len(self.input_input) > 0:
  529. self.queue_game.put("\b \b")
  530. self.input_input = self.input_input[0:-1]
  531. else:
  532. # Can't
  533. self.queue_game.put("\a")
  534. if c == "\r":
  535. # Ok, completed!
  536. self.queue_game.put(r + "\n\r")
  537. self.observer.load(self.save)
  538. self.save = None
  539. line = self.input_input
  540. log.msg("finishing niceInput {0}".format(line))
  541. # self.queue_game.put("[{0}]\n\r".format(line))
  542. self.input_input = ''
  543. # Ok, maybe this isn't the way to do this ...
  544. # self.player_input.callback(line)
  545. reactor.callLater(0, self.player_input.callback, line)
  546. self.player_input = None
  547. if c.isprintable():
  548. if len(self.input_input) + 1 <= self.input_limit:
  549. self.input_input += c
  550. self.queue_game.put(c)
  551. else:
  552. # Limit reached
  553. self.queue_game.put("\a")
  554. class Game(protocol.Protocol):
  555. def __init__(self):
  556. self.buffer = ""
  557. self.game = None
  558. self.usergame = (None, None)
  559. self.to_player = True
  560. self.mcp = MCP(self)
  561. def connectionMade(self):
  562. log.msg("Connected to Game Server")
  563. self.queue_player = self.factory.queue_player
  564. self.queue_game = self.factory.queue_game
  565. self.observer = self.factory.observer
  566. self.factory.game = self
  567. self.setPlayerReceived()
  568. self.observer.connect("user-game", self.show_game)
  569. self.mcp.finishSetup()
  570. def show_game(self, game):
  571. self.usergame = game
  572. log.msg("## User-Game:", game)
  573. def setPlayerReceived(self):
  574. """ Get deferred from client queue, callback clientDataReceived. """
  575. self.queue_player.get().addCallback(self.playerDataReceived)
  576. def playerDataReceived(self, chunk):
  577. if chunk is False:
  578. self.queue_player = None
  579. log.msg("Player: disconnected, close connection to game")
  580. # I don't believe I need this if I'm using protocol.Factory
  581. self.factory.continueTrying = False
  582. self.transport.loseConnection()
  583. else:
  584. # Pass received data to the server
  585. if type(chunk) == str:
  586. self.transport.write(chunk.encode())
  587. else:
  588. self.transport.write(chunk)
  589. self.setPlayerReceived()
  590. def lineReceived(self, line):
  591. """ line received from the game. """
  592. if LOG_LINES:
  593. log.msg(">> [{0}]".format(line))
  594. if "TWGS v2.20b" in line and "www.eisonline.com" in line:
  595. self.queue_game.put(
  596. "TWGS Proxy build "
  597. + version
  598. + " is active. \x1b[1;34m~\x1b[0m to activate.\n\r\n\r",
  599. )
  600. if "TradeWars Game Server" in line and "Copyright (C) EIS" in line:
  601. # We are not in a game
  602. if not self.game is None:
  603. # We were in a game.
  604. self.game = None
  605. self.observer.emit("user-game", (self.factory.player.user, self.game))
  606. if "Selection (? for menu): " in line:
  607. game = line[-1]
  608. if game >= "A" and game < "Q":
  609. self.game = game
  610. log.msg("Game: {0}".format(self.game))
  611. self.observer.emit("user-game", (self.factory.player.user, self.game))
  612. self.observer.emit("game-line", line)
  613. def getPrompt(self):
  614. """ Return the current prompt, stripped of ANSI. """
  615. return cleanANSI(self.buffer)
  616. def dataReceived(self, chunk):
  617. """ Data received from the Game.
  618. Remove backspaces.
  619. Treat some ANSI codes as NewLine.
  620. Remove ANSI.
  621. Break into lines.
  622. Trim out carriage returns.
  623. Call lineReceived().
  624. "Optionally" pass data to player.
  625. FUTURE: trigger on prompt. [cleanANSI(buffer)]
  626. """
  627. # Sequence error:
  628. # If I don't put the chunk(I received) to the player.
  629. # anything I display -- lineReceive() put() ... would
  630. # be out of order. (I'd be responding -- before it
  631. # was displayed to the user.)
  632. if self.to_player:
  633. self.queue_game.put(chunk)
  634. self.buffer += chunk.decode("utf-8", "ignore")
  635. # Process any backspaces
  636. while "\b" in self.buffer:
  637. part = self.buffer.partition("\b")
  638. self.buffer = part[0][:-1] + part[2]
  639. # Treat some ANSI codes as a newline
  640. self.buffer = treatAsNL(self.buffer)
  641. # Break into lines
  642. while "\n" in self.buffer:
  643. part = self.buffer.partition("\n")
  644. line = part[0].replace("\r", "")
  645. # Clean ANSI codes from line
  646. line = cleanANSI(line)
  647. self.lineReceived(line)
  648. self.buffer = part[2]
  649. self.observer.emit("prompt", self.getPrompt())
  650. def connectionLost(self, why):
  651. log.msg("Game connectionLost because: %s" % why)
  652. self.observer.emit('close', why)
  653. self.queue_game.put(False)
  654. self.transport.loseConnection()
  655. class GlueFactory(protocol.ClientFactory):
  656. # class GlueFactory(protocol.Factory):
  657. maxDelay = 10
  658. protocol = Game
  659. def __init__(self, player):
  660. self.player = player
  661. self.queue_player = player.queue_player
  662. self.queue_game = player.queue_game
  663. self.observer = player.observer
  664. self.game = None
  665. def closeIt(self):
  666. log.msg("closeIt")
  667. self.queue_game.put(False)
  668. def getUser(self, user):
  669. log.msg("getUser( %s )" % user)
  670. self.twgs.logUser(user)
  671. # This was needed when I replaced ClientFactory with Factory.
  672. # def clientConnectionLost(self, connector, why):
  673. # log.msg("clientconnectionlost: %s" % why)
  674. # self.queue_client.put(False)
  675. def clientConnectionFailed(self, connector, why):
  676. log.msg("connection to game failed: %s" % why)
  677. self.queue_game.put(b"Sorry! I'm Unable to connect to the game server.\r\n")
  678. # syncterm gets cranky/locks up if we close this here.
  679. # (Because it is still sending rlogin information?)
  680. reactor.callLater(2, self.closeIt)
  681. class Player(protocol.Protocol):
  682. def __init__(self):
  683. self.buffer = ""
  684. self.user = None
  685. self.observer = Observer()
  686. self.game = None
  687. self.glue = None
  688. def connectionMade(self):
  689. """ connected, setup queues.
  690. queue_player is data from player.
  691. queue_game is data to player. (possibly from game)
  692. """
  693. self.queue_player = defer.DeferredQueue()
  694. self.queue_game = defer.DeferredQueue()
  695. self.setGameReceived()
  696. # Connect GlueFactory to this Player object.
  697. factory = GlueFactory(self)
  698. self.glue = factory
  699. # Make connection to the game server
  700. reactor.connectTCP(HOST, PORT, factory, 5)
  701. def setGameReceived(self):
  702. """ Get deferred from client queue, callback clientDataReceived. """
  703. self.queue_game.get().addCallback(self.gameDataReceived)
  704. def gameDataReceived(self, chunk):
  705. """ Data received from the game. """
  706. # If we have received game data, it has to be connected.
  707. if self.game is None:
  708. self.game = self.glue.game
  709. if chunk is False:
  710. self.transport.loseConnection()
  711. else:
  712. if type(chunk) == bytes:
  713. self.transport.write(chunk)
  714. elif type(chunk) == str:
  715. self.transport.write(chunk.encode())
  716. else:
  717. log.err("gameDataReceived: type (%s) given!", type(chunk))
  718. self.transport.write(chunk)
  719. self.setGameReceived()
  720. def dataReceived(self, chunk):
  721. if self.user is None:
  722. self.buffer += chunk.decode("utf-8", "ignore")
  723. parts = self.buffer.split("\x00")
  724. if len(parts) >= 5:
  725. # rlogin we have the username
  726. self.user = parts[1]
  727. log.msg("User: {0}".format(self.user))
  728. zpos = self.buffer.rindex("\x00")
  729. self.buffer = self.buffer[zpos + 1 :]
  730. # but I don't need the buffer anymore, so:
  731. self.buffer = ""
  732. # Pass user value on to whatever needs it.
  733. self.observer.emit("user", self.user)
  734. # Unfortunately, the ones interested in this don't exist yet.
  735. if not self.observer.emit("player", chunk):
  736. # Was not dispatched. Send to game.
  737. self.queue_player.put(chunk)
  738. else:
  739. # There's an observer. Don't continue.
  740. return
  741. if chunk == b"~":
  742. if self.game:
  743. prompt = self.game.getPrompt()
  744. if re.match(r"Command \[TL=.* \(\?=Help\)\? :", prompt):
  745. self.observer.emit("hotkey", prompt)
  746. else:
  747. self.observer.emit("notyet", prompt)
  748. # Selection (? for menu): (the game server menu)
  749. # Enter your choice: (game menu)
  750. # Command [TL=00:00:00]:[1800] (?=Help)? : <- YES!
  751. # Computer command [TL=00:00:00]:[613] (?=Help)?
  752. if chunk == b"|":
  753. # how can I tell if this is active or not?
  754. # I no longer see a 'player' observer. GOOD!
  755. # log.msg(pformat(self.observer.dispatch))
  756. # prompt = self.game.getPrompt()
  757. # if re.match(r"Command \[TL=.* \(\?=Help\)\? :", prompt):
  758. menu = ProxyMenu(self.game)
  759. def connectionLost(self, why):
  760. log.msg("lost connection %s" % why)
  761. self.observer.emit('close', why)
  762. self.queue_player.put(False)
  763. def connectionFailed(self, why):
  764. log.msg("connectionFailed: %s" % why)
  765. if __name__ == "__main__":
  766. if LOGFILE:
  767. log.startLogging(DailyLogFile("proxy.log", "."))
  768. else:
  769. log.startLogging(sys.stdout)
  770. log.msg("This is version: %s" % version)
  771. factory = protocol.Factory()
  772. factory.protocol = Player
  773. reactor.listenTCP(LISTEN_PORT, factory, interface=LISTEN_ON)
  774. reactor.run()