123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565 |
- #!/usr/bin/env python3
- import sys
- import re
- from twisted.internet import defer
- from twisted.internet import protocol
- from twisted.internet import reactor
- from twisted.internet import task
- from twisted.internet.task import coiterate
- from twisted.python import log
- from twisted.python.logfile import DailyLogFile
- import pendulum
- from subprocess import check_output
- import os
- from colorama import Fore, Back, Style
- from pprint import pformat
- import yaml
- def config_load(filename):
- global config
- with open(filename, "r") as fp:
- config = yaml.safe_load(fp)
- if os.path.exists("config_dev.yaml"):
- config_load("config_dev.yaml")
- else:
- config_load("config.yaml")
- # Extract the version information from git.
- # The match gives us only tags starting with v[0-9]* Using anything else trips up on double digits.
- version = check_output(
- [
- "git",
- "describe",
- "--abbrev=8",
- "--long",
- "--tags",
- "--dirty",
- "--always",
- "--match",
- "v[0-9]*",
- ],
- universal_newlines=True,
- ).strip()
- def merge(color_string):
- """ Given a string of colorama ANSI, merge them if you can. """
- return color_string.replace("m\x1b[", ";")
- # https://en.wikipedia.org/wiki/ANSI_escape_code
- # Cleans all ANSI
- cleaner = re.compile(r"\x1b\[[0-9;]*[A-Zmh]")
- # Looks for ANSI (that should be considered to be a newline)
- # This needs to see what is send when something enters / leaves
- # the player's current sector. (That doesn't work/isn't
- # detected. NNY!) It is "\x1b[K" Erase in Line!
- makeNL = re.compile(r"\x1b\[[0-9;]*[JK]")
- def treatAsNL(line):
- """ Replace any ANSI codes that would be better understood as newlines. """
- global makeNL
- return makeNL.sub("\n", line)
- def cleanANSI(line):
- """ Remove all ANSI codes. """
- global cleaner
- return cleaner.sub("", line)
- # return re.sub(r'\x1b\[([0-9,A-Z]{1,2}(;[0-9]{1,2})?(;[0-9]{3})?)?[m|K]?', '', line)
- from observer import Observer
- from flexible import PlayerInput, ProxyMenu
- from galaxy import GameData
- from flexible import PORT_CLASSES, CLASSES_PORT
- class Game(protocol.Protocol):
- def __init__(self):
- self.buffer = ""
- self.game = None
- self.usergame = (None, None)
- self.gamedata = None
- self.to_player = True
- self.linestate = ""
- def connectionMade(self):
- log.msg("Connected to Game Server")
- self.queue_player = self.factory.queue_player
- self.queue_game = self.factory.queue_game
- self.observer = self.factory.observer
- self.factory.game = self
- self.setPlayerReceived()
- self.observer.connect("user-game", self.show_game)
- def show_game(self, game):
- self.usergame = game
- log.msg("## User-Game:", game)
- if game[1] is None:
- if self.gamedata is not None:
- # start the save
- coiterate(self.gamedata.save())
- self.gamedata = None
- if hasattr(self, "portdata"):
- log.msg("Clearing out old portdata.")
- self.portdata = {}
- if hasattr(self, "warpdata"):
- log.msg("Clearing out old warpdata.")
- self.warpdata = {}
- else:
- # Load the game data (if any)
- self.gamedata = GameData(game)
- coiterate(self.gamedata.load())
- def setPlayerReceived(self):
- """ Get deferred from client queue, callback clientDataReceived. """
- self.queue_player.get().addCallback(self.playerDataReceived)
- def playerDataReceived(self, chunk):
- if chunk is False:
- self.queue_player = None
- log.msg("Player: disconnected, close connection to game")
- # I don't believe I need this if I'm using protocol.Factory
- self.factory.continueTrying = False
- self.transport.loseConnection()
- else:
- # Pass received data to the server
- if type(chunk) == str:
- self.transport.write(chunk.encode())
- log.msg(">> [{0}]".format(chunk))
- else:
- self.transport.write(chunk)
- log.msg(">> [{0}]".format(chunk.decode("utf-8", "ignore")))
- self.setPlayerReceived()
- def warpline(self, line):
- log.msg("warp:", line)
- # 1 > 3 > 5 > 77 > 999
- last_sector = self.lastwarp
- line = line.replace("(", "").replace(")", "").replace(">", "").strip()
- for s in line.split():
- # Ok, this should be all of the warps.
- sector = int(s)
- if last_sector > 0:
- self.gamedata.warp_to(last_sector, sector)
- last_sector = sector
- self.lastwarp = sector
- def cimline(self, line):
- log.msg(self.linestate, ":", line)
- if line[-1] == "%":
- self.linestate = "portcim"
- if self.linestate == "warpcim":
- # warps
- work = line.strip()
- if work != "":
- parts = re.split(r"(?<=\d)\s", work)
- parts = [int(x) for x in parts]
- sector = parts.pop(0)
- self.gamedata.warp_to(sector, *parts)
- elif self.linestate == "portcim":
- # ports
- work = line.replace("%", "")
- parts = re.parts = re.split(r"(?<=\d)\s", work)
- if len(parts) == 8:
- sector = int(parts[0].strip())
- data = dict()
- def portBS(info):
- if info[0] == "-":
- bs = "B"
- else:
- bs = "S"
- return (bs, int(info[1:].strip()))
- data["fuel"] = dict()
- data["fuel"]["sale"], data["fuel"]["units"] = portBS(parts[1])
- data["fuel"]["pct"] = int(parts[2].strip())
- data["org"] = dict()
- data["org"]["sale"], data["org"]["units"] = portBS(parts[3])
- data["org"]["pct"] = int(parts[4].strip())
- data["equ"] = dict()
- data["equ"]["sale"], data["equ"]["units"] = portBS(parts[5])
- data["equ"]["pct"] = int(parts[6].strip())
- # Store what this port is buying/selling
- data["port"] = (
- data["fuel"]["sale"] + data["org"]["sale"] + data["equ"]["sale"]
- )
- # Convert BBS/SBB to Class number 1-8
- data["class"] = CLASSES_PORT[data["port"]]
- self.gamedata.set_port(sector, data)
- else:
- self.linestate = "cim"
- def sectorline(self, line):
- log.msg("sector:", self.current_sector, ':', line)
- if line.startswith('Beacon : '):
- pass # get beacon text
- elif line.startswith('Ports : '):
- # Ports : Ballista, Class 1 (BBS)
- self.sector_state = 'port'
- if '<=-DANGER-=>' in line:
- # Port is destroyed
- if sector in self.gamedate.ports:
- del self.gamedata.ports[sector]
- elif '(StarDock)' not in line:
- _, _, class_port = line.partition(', Class ')
- c, port = class_port.split(' ')
- c = int(c)
- port = port.replace('(', '').replace(')', '')
- data = { 'port': port, 'class': c }
- self.gamedata.set_port(self.current_sector, data)
- elif line.startswith('Planets : '):
- # Planets : (O) Flipper
- self.sector_state = 'planet'
- elif line.startswith('Traders : '):
- self.sector_state = 'trader'
- elif line.startswith('Ships : '):
- self.sector_state = 'ship'
- elif line.startswith('Fighters: '):
- self.sector_state = 'fighter'
- elif line.startswith('NavHaz : '):
- pass
- elif line.startswith('Mines : '):
- self.sector_state = 'mine'
- elif line.startswith(' '):
- # continues
- if self.sector_state == 'mines':
- pass
- if self.sector_state == 'planet':
- pass
- if self.sector_state == 'trader':
- pass
- if self.sector_state == 'ship':
- pass
- elif len(line) > 8 and line[8] == ':':
- self.sector_state = 'normal'
- elif line.startswith('Warps to Sector(s) :'):
- # Warps to Sector(s) : 5468
- _, _, work = line.partition(':')
- work = work.strip().replace('(', '').replace(')', '').replace(' - ', ' ')
- parts = [ int(x) for x in work.split(' ')]
- log.msg("Sectorline warps", parts)
- self.gamedata.warp_to(self.current_sector, *parts)
- self.sector_state = 'normal'
- self.linestate = ''
- def lineReceived(self, line):
- """ line received from the game. """
- if "log_lines" in config and config["log_lines"]:
- log.msg("<< [{0}]".format(line))
- if "TradeWars Game Server" in line and "Copyright (C) EIS" in line:
- # We are not in a game
- if not self.game is None:
- # We were in a game.
- self.game = None
- self.observer.emit("user-game", (self.factory.player.user, self.game))
- elif "Selection (? for menu): " in line:
- game = line[-1]
- if game >= "A" and game < "Q":
- self.game = game
- log.msg("Game: {0}".format(self.game))
- self.observer.emit("user-game", (self.factory.player.user, self.game))
- # Process.pas parse line
- if line.startswith("The shortest path (") or line.startswith(" TO > "):
- self.linestate = "warpline"
- self.lastwarp = 0
- elif self.linestate == "warpline":
- if line == "":
- self.linestate = ""
- else:
- self.warpline(line)
- elif self.linestate == "portcim" or self.linestate == "warpcim":
- if line == ": ENDINTERROG":
- self.linestate = ""
- elif line == ": ":
- self.linestate = "cim"
- elif line == "":
- self.linestate = ""
- else:
- if len(line) > 2:
- self.cimline(line)
- elif self.linestate == "cim":
- if line == ": ENDINTERROG" or line == "":
- self.linestate = ""
- elif len(line) > 2:
- if line.rstrip()[-1] == "%":
- self.linestate = "portcim"
- else:
- self.linestate = "warpcim"
- self.cimline(line)
- elif line.startswith(": "):
- self.linestate = "cim"
- elif line.startswith("Sector : "):
- # Sector : 2565 in uncharted space.
- self.linestate = "sector"
- work = line.strip()
- parts = re.split(r"\s+", work)
- self.current_sector = int(parts[2])
- elif self.linestate == "sector":
- self.sectorline(line)
- self.observer.emit("game-line", line)
- def getPrompt(self):
- """ Return the current prompt, stripped of ANSI. """
- return cleanANSI(self.buffer)
- def dataReceived(self, chunk):
- """ Data received from the Game.
-
- Remove backspaces.
- Treat some ANSI codes as NewLine.
- Remove ANSI.
- Break into lines.
- Trim out carriage returns.
- Call lineReceived().
-
- "Optionally" pass data to player.
- FUTURE: trigger on prompt. [cleanANSI(buffer)]
- """
- # Store the text into the buffer before we inject into it.
- self.buffer += chunk.decode("utf-8", "ignore")
- # log.msg("data: [{0}]".format(repr(chunk)))
- if b"TWGS v2.20b" in chunk and b"www.eisonline.com" in chunk:
- # Ok, we have a possible target.
- target = b"www.eisonline.com\n\r"
- pos = chunk.find(target)
- if pos != -1:
- # Found it! Inject!
- message = (
- "TWGS Proxy build " + version + ". ~ to activate in game.\n\r"
- )
- chunk = (
- chunk[0 : pos + len(target)]
- + message.encode()
- + chunk[pos + len(target) :]
- )
- # Sequence error:
- # If I don't put the chunk(I received) to the player.
- # anything I display -- lineReceive() put() ... would
- # be out of order. (I'd be responding -- before it
- # was displayed to the user.)
- if self.to_player:
- self.queue_game.put(chunk)
- # self.buffer += chunk.decode("utf-8", "ignore")
- #
- # Begin processing the buffer
- #
- # Process any backspaces
- while "\b" in self.buffer:
- part = self.buffer.partition("\b")
- self.buffer = part[0][:-1] + part[2]
- # Treat some ANSI codes as a newline
- self.buffer = treatAsNL(self.buffer)
- # Break into lines
- while "\n" in self.buffer:
- part = self.buffer.partition("\n")
- line = part[0].replace("\r", "")
- # Clean ANSI codes from line
- line = cleanANSI(line)
- self.lineReceived(line)
- self.buffer = part[2]
- self.observer.emit("prompt", self.getPrompt())
- def connectionLost(self, why):
- log.msg("Game connectionLost because: %s" % why)
- self.observer.emit("close", why)
- self.queue_game.put(False)
- self.transport.loseConnection()
- class GlueFactory(protocol.ClientFactory):
- # class GlueFactory(protocol.Factory):
- maxDelay = 10
- protocol = Game
- def __init__(self, player):
- self.player = player
- self.queue_player = player.queue_player
- self.queue_game = player.queue_game
- self.observer = player.observer
- self.game = None
- def closeIt(self):
- log.msg("closeIt")
- self.queue_game.put(False)
- def getUser(self, user):
- log.msg("getUser( %s )" % user)
- self.twgs.logUser(user)
- # This was needed when I replaced ClientFactory with Factory.
- # def clientConnectionLost(self, connector, why):
- # log.msg("clientconnectionlost: %s" % why)
- # self.queue_client.put(False)
- def clientConnectionFailed(self, connector, why):
- log.msg("connection to game failed: %s" % why)
- self.queue_game.put(b"Sorry! I'm Unable to connect to the game server.\r\n")
- # syncterm gets cranky/locks up if we close this here.
- # (Because it is still sending rlogin information?)
- reactor.callLater(2, self.closeIt)
- class Player(protocol.Protocol):
- def __init__(self):
- self.buffer = ""
- self.user = None
- self.observer = Observer()
- self.game = None
- self.glue = None
- def connectionMade(self):
- """ connected, setup queues.
-
- queue_player is data from player.
- queue_game is data to player. (possibly from game)
- """
- self.queue_player = defer.DeferredQueue()
- self.queue_game = defer.DeferredQueue()
- self.setGameReceived()
- # Connect GlueFactory to this Player object.
- factory = GlueFactory(self)
- self.glue = factory
- # Make connection to the game server
- reactor.connectTCP(config["host"], config["port"], factory, 5)
- def setGameReceived(self):
- """ Get deferred from client queue, callback clientDataReceived. """
- self.queue_game.get().addCallback(self.gameDataReceived)
- def gameDataReceived(self, chunk):
- """ Data received from the game. """
- # If we have received game data, it has to be connected.
- if self.game is None:
- self.game = self.glue.game
- if chunk is False:
- self.transport.loseConnection()
- else:
- if type(chunk) == bytes:
- self.transport.write(chunk)
- elif type(chunk) == str:
- self.transport.write(chunk.encode())
- else:
- log.err("gameDataReceived: type (%s) given!".format(type(chunk)))
- self.transport.write(chunk)
- self.setGameReceived()
- def dataReceived(self, chunk):
- if self.user is None:
- self.buffer += chunk.decode("utf-8", "ignore")
- parts = self.buffer.split("\x00")
- if len(parts) >= 5:
- # rlogin we have the username
- self.user = parts[1]
- log.msg("User: {0}".format(self.user))
- zpos = self.buffer.rindex("\x00")
- self.buffer = self.buffer[zpos + 1 :]
- # but I don't need the buffer anymore, so:
- self.buffer = ""
- # Pass user value on to whatever needs it.
- self.observer.emit("user", self.user)
- # Unfortunately, the ones interested in this don't exist yet.
- if not self.observer.emit("player", chunk):
- # Was not dispatched. Send to game.
- self.queue_player.put(chunk)
- else:
- # There's an observer. Don't continue.
- return
- if chunk == b"~":
- prompt = self.game.getPrompt()
- # Selection (? for menu): (the game server menu)
- # Enter your choice: (game menu)
- # Command [TL=00:00:00]:[1800] (?=Help)? : <- YES!
- # Computer command [TL=00:00:00]:[613] (?=Help)?
- # (and others I've yet to see...)
- if re.match(r"Command \[TL=.* \(\?=Help\)\? :", prompt):
- menu = ProxyMenu(self.game)
- else:
- nl = "\n\r"
- r = Style.RESET_ALL
- log.msg("NNY!")
- prompt = self.game.buffer
- self.queue_game.put(
- r
- + nl
- + Style.BRIGHT
- + "Proxy:"
- + Style.RESET_ALL
- + " I can't activate at this time."
- + nl
- )
- self.queue_game.put(prompt)
- self.queue_player.put("\a")
- # self.observer.emit("notyet", prompt)
- def connectionLost(self, why):
- log.msg("lost connection %s" % why)
- self.observer.emit("close", why)
- self.queue_player.put(False)
- def connectionFailed(self, why):
- log.msg("connectionFailed: %s" % why)
- if __name__ == "__main__":
- if "logfile" in config and config["logfile"]:
- log.startLogging(DailyLogFile("proxy.log", "."))
- else:
- log.startLogging(sys.stdout)
- log.msg("This is version: %s" % version)
- factory = protocol.Factory()
- factory.protocol = Player
- reactor.listenTCP(config["listen_port"], factory, interface=config["listen_on"])
- reactor.run()
- else:
- # I can't seem to get twistd -y tcp-proxy.py
- # to work. Missing imports?
- application = service.Application("TradeWarsGameServer-Proxy")
- factory = protocol.Factory()
- factory.protocol = Player
- internet.TCPServer(config["listen_port"], factory).setServiceParent(application)
|