Ver código fonte

Merge branch 'master' of ssh://bbs.red-green.com:8022/RedGreen/twgs-proxy
Yay I think we recover from our merge mess.

david 5 anos atrás
pai
commit
944ca08414
2 arquivos alterados com 280 adições e 66 exclusões
  1. 198 7
      galaxy.py
  2. 82 59
      tcp-proxy.py

+ 198 - 7
galaxy.py

@@ -2,14 +2,38 @@ import jsonlines
 import os
 from twisted.python import log
 from pprint import pprint
+from colorama import Fore, Back, Style
+
+
+def merge(color_string):
+    """ Given a string of colorama ANSI, merge them if you can. """
+    return color_string.replace("m\x1b[", ";")
+
+
+PORT_CLASSES = {
+    1: "BBS",
+    2: "BSB",
+    3: "SBB",
+    4: "SSB",
+    5: "SBS",
+    6: "BSS",
+    7: "SSS",
+    8: "BBB",
+}
+CLASSES_PORT = {v: k for k, v in PORT_CLASSES.items()}
 
 
 class GameData(object):
-    def __init__(self, usergame):
+    def __init__(self, usergame: tuple):
         # Construct the GameData storage object
         self.usergame = usergame
         self.warps = {}
         self.ports = {}
+        # 10 = 300 bytes
+        self.warp_groups = 10
+        # 3 = 560 bytes
+        # 5 = 930 bytes
+        self.port_groups = 3
 
     def storage_filename(self):
         """ return filename
@@ -34,20 +58,38 @@ class GameData(object):
 
         with jsonlines.open(filename, mode="w", sort_keys=True) as writer:
             # for warp, sectors in self.warps.items():
+            w = {"warp": {}}
+
             for warp in sorted(self.warps.keys()):
                 sectors = self.warps[warp]
                 # log.msg("save:", warp, sectors)
                 sects = sorted(list(sectors))  # make a list
-                w = {"warp": {warp: sects}}
+                w["warp"][warp] = sects
+                if len(w["warp"]) >= self.warp_groups:
+                    writer.write(w)
+                    w = {"warp": {}}
+                    yield
+
                 # log.msg(w)
+                # writer.write(w)
+                # yield
+
+            if len(w["warp"]) > 1:
                 writer.write(w)
-                yield
+
             # for sector, port in self.ports.items():
+            p = {"port": {}}
+
             for sector in sorted(self.ports.keys()):
                 port = self.ports[sector]
-                p = {"port": {sector: port}}
+                p["port"][sector] = port
+                if len(p["port"]) >= self.port_groups:
+                    writer.write(p)
+                    p = {"port": {}}
+                    yield
+            if len(p["port"]) > 1:
                 writer.write(p)
-                yield
+
         log.msg("Saved {0} {1}/{2}".format(filename, len(self.ports), len(self.warps)))
 
     def load(self):
@@ -71,7 +113,36 @@ class GameData(object):
                     yield
         log.msg("Loaded {0} {1}/{2}".format(filename, len(self.ports), len(self.warps)))
 
-    def warp_to(self, source, *dest):
+    def untwisted_load(self):
+        """ Load file without twisted deferred 
+        
+        This is for testing things out.
+        """
+        filename = self.storage_filename()
+
+        self.warps = {}
+        self.ports = {}
+        if os.path.exists(filename):
+            # Load it
+            with jsonlines.open(filename) as reader:
+                for obj in reader:
+                    if "warp" in obj:
+                        for s, w in obj["warp"].items():
+                            # log.msg(s, w)
+                            self.warps[int(s)] = set(w)
+                        # self.warps.update(obj["warp"])
+                    if "port" in obj:
+                        for s, p in obj["port"].items():
+                            self.ports[int(s)] = p
+                        # self.ports.update(obj["port"])
+        log.msg("Loaded {0} {1}/{2}".format(filename, len(self.ports), len(self.warps)))
+
+    def get_warps(self, sector: int):
+        if sector in self.warps:
+            return self.warps[sector]
+        return None
+
+    def warp_to(self, source: int, *dest):
         """ connect sector source to destination. 
         """
         log.msg("Warp {0} to {1}".format(source, dest))
@@ -82,8 +153,128 @@ class GameData(object):
             if d not in self.warps[source]:
                 self.warps[source].add(d)
 
-    def set_port(self, sector, data):
+    def set_port(self, sector: int, data: dict):
         log.msg("Port {0} : {1}".format(sector, data))
         if sector not in self.ports:
             self.ports[sector] = dict()
         self.ports[sector].update(data)
+
+        if "port" not in self.ports[sector]:
+            # incomplete port type - can we "complete" it?
+
+            if all(x in self.ports for x in ["fuel", "org", "equ"]):
+                # We have all of the port types, so:
+                port = "".join(
+                    [self.ports[sector][x]["sale"] for x in ["fuel", "org", "equ"]]
+                )
+                self.ports[sector]["port"] = port
+                self.ports[sector]["class"] = CLASSES_PORT[port]
+                log.msg("completed {0} : {1}".format(sector, self.ports[sector]))
+
+    @staticmethod
+    def port_burnt(port: dict):
+        """ Is this port burned out? """
+        if all(x in port for x in ["fuel", "org", "equ"]):
+            if all("pct" in port[x] for x in ["fuel", "org", "equ"]):
+                if (
+                    port["equ"]["pct"] <= 20
+                    or port["fuel"]["pct"] <= 20
+                    or port["org"]["pct"] <= 20
+                ):
+                    return True
+                return False
+        # Since we don't have any port information, hope for the best, assume it isn't burnt.
+        return False
+
+    @staticmethod
+    def flip(buy_sell):
+        # Invert B's and S's to determine if we can trade or not between ports.
+        return buy_sell.replace("S", "W").replace("B", "S").replace("W", "B")
+
+    @staticmethod
+    def port_trading(port1, port2):
+        # Given the port settings, can we trade between these?
+        if port1 == port2:
+            return False
+        p1 = [c for c in port1]
+        p2 = [c for c in port2]
+        rem = False
+        for i in range(3):
+            if p1[i] == p2[i]:
+                p1[i] = "X"
+                p2[i] = "X"
+                rem = True
+        if rem:
+            j1 = "".join(p1).replace("X", "")
+            j2 = "".join(p2).replace("X", "")
+            if j1 == "BS" and j2 == "SB":
+                return True
+            if j1 == "SB" and j2 == "BS":
+                return True
+
+        rport1 = GameData.flip(port1)
+        c = 0
+        match = []
+        for i in range(3):
+            if rport1[i] == port2[i]:
+                match.append(port2[i])
+                c += 1
+        if c > 1:
+            # Remove first match, flip it
+            f = GameData.flip(match.pop(0))
+            # Verify it is in there.
+            # so we're not matching SSS/BBB
+            if f in match:
+                return True
+            return False
+        return False
+
+    @staticmethod
+    def color_pct(pct: int):
+        if pct > 50:
+            # green
+            return "{0}{1:3}{2}".format(Fore.GREEN, pct, Style.RESET_ALL)
+        elif pct > 25:
+            return "{0}{1:3}{2}".format(
+                merge(Fore.YELLOW + Style.BRIGHT), pct, Style.RESET_ALL
+            )
+        else:
+            return "{0:3}".format(pct)
+
+    @staticmethod
+    def port_pct(port: dict):
+        # Make sure these exist in the port data given.
+        if all(x in port for x in ["fuel", "org", "equ"]):
+            return "{0},{1},{2}%".format(
+                GameData.color_pct(port["fuel"]["pct"]),
+                GameData.color_pct(port["org"]["pct"]),
+                GameData.color_pct(port["equ"]["pct"]),
+            )
+        else:
+            return "---,---,---%"
+
+    @staticmethod
+    def port_show_part(sector: int, sector_port: dict):
+        return "{0:5} ({1}) {2}".format(
+            sector, sector_port["port"], GameData.port_pct(sector_port)
+        )
+
+    def port_trade_show(self, sector: int, warp: int):
+        sector_port = self.ports[sector]
+        warp_port = self.ports[warp]
+        sector_pct = GameData.port_pct(sector_port)
+        warp_pct = GameData.port_pct(warp_port)
+        return "{0} \xae\xcd\xaf {1}".format(
+            GameData.port_show_part(sector, sector_port),
+            GameData.port_show_part(warp, warp_port),
+        )
+
+    @staticmethod
+    def port_show(sector: int, sector_port: dict, warp: int, warp_port: dict):
+        sector_pct = GameData.port_pct(sector_port)
+        warp_pct = GameData.port_pct(warp_port)
+        return "{0} -=- {1}".format(
+            GameData.port_show_part(sector, sector_port),
+            GameData.port_show_part(warp, warp_port),
+        )
+

+ 82 - 59
tcp-proxy.py

@@ -20,7 +20,7 @@ from pprint import pformat
 import yaml
 
 
-def config_load(filename):
+def config_load(filename: str):
     global config
     with open(filename, "r") as fp:
         config = yaml.safe_load(fp)
@@ -49,7 +49,7 @@ version = check_output(
 ).strip()
 
 
-def merge(color_string):
+def merge(color_string: str):
     """ Given a string of colorama ANSI, merge them if you can. """
     return color_string.replace("m\x1b[", ";")
 
@@ -66,13 +66,13 @@ cleaner = re.compile(r"\x1b\[[0-9;]*[A-Zmh]")
 makeNL = re.compile(r"\x1b\[[0-9;]*[JK]")
 
 
-def treatAsNL(line):
+def treatAsNL(line: str):
     """ Replace any ANSI codes that would be better understood as newlines. """
     global makeNL
     return makeNL.sub("\n", line)
 
 
-def cleanANSI(line):
+def cleanANSI(line: str):
     """ Remove all ANSI codes. """
     global cleaner
     return cleaner.sub("", line)
@@ -81,8 +81,7 @@ def cleanANSI(line):
 
 from observer import Observer
 from flexible import PlayerInput, ProxyMenu
-from galaxy import GameData
-from flexible import PORT_CLASSES, CLASSES_PORT
+from galaxy import GameData, PORT_CLASSES, CLASSES_PORT
 
 
 class Game(protocol.Protocol):
@@ -103,7 +102,7 @@ class Game(protocol.Protocol):
         self.setPlayerReceived()
         self.observer.connect("user-game", self.show_game)
 
-    def show_game(self, game):
+    def show_game(self, game: tuple):
         self.usergame = game
         log.msg("## User-Game:", game)
         if game[1] is None:
@@ -111,12 +110,6 @@ class Game(protocol.Protocol):
                 # 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)
@@ -136,14 +129,14 @@ class Game(protocol.Protocol):
         else:
             # Pass received data to the server
             if type(chunk) == str:
-                self.transport.write(chunk.encode())
+                self.transport.write(chunk.encode('latin-1'))
                 log.msg(">> [{0}]".format(chunk))
             else:
                 self.transport.write(chunk)
-                log.msg(">> [{0}]".format(chunk.decode("utf-8", "ignore")))
+                log.msg(">> [{0}]".format(chunk.decode("latin-1", "ignore")))
             self.setPlayerReceived()
 
-    def warpline(self, line):
+    def warpline(self, line: str):
         log.msg("warp:", line)
         # 1 > 3 > 5 > 77 > 999
         last_sector = self.lastwarp
@@ -156,8 +149,8 @@ class Game(protocol.Protocol):
             last_sector = sector
             self.lastwarp = sector
 
-    def cimline(self, line):
-        log.msg(self.linestate, ":", line)
+    def cimline(self, line: str):
+        # log.msg(self.linestate, ":", line)
         if line[-1] == "%":
             self.linestate = "portcim"
 
@@ -206,7 +199,7 @@ class Game(protocol.Protocol):
             else:
                 self.linestate = "cim"
 
-    def sectorline(self, line):
+    def sectorline(self, line: str):
         log.msg("sector:", self.current_sector, ':', line)
         if line.startswith('Beacon  : '):
             pass # get beacon text
@@ -260,7 +253,21 @@ class Game(protocol.Protocol):
             self.sector_state = 'normal'
             self.linestate = ''
 
-    def lineReceived(self, line):
+    def portline(self, line: str):
+        # Map these items to which keys
+        log.msg("portline({0}): {1}".format(self.current_sector, line))
+        mapto = { 'Fuel': 'fuel', 'Organics': 'org', 'Equipment': 'equ'}
+
+        if '%' in line:
+            # Fuel Ore   Buying    2890    100%       0
+            work = line.replace('Fuel Ore', 'Fuel').replace('%', '')
+            parts = re.split(r"\s+", work)
+            data = { mapto[parts[0]] : { 'sale': parts[1][0], 'units': parts[2], 'pct': int(parts[3]) } }
+            # log.msg("Setting {0} to {1}".format(self.current_sector, data))
+            self.gamedata.set_port(self.current_sector, data)
+            # log.msg("NOW: {0}".format(self.gamedata.ports[self.current_sector]))
+
+    def lineReceived(self, line: str):
         """ line received from the game. """
         if "log_lines" in config and config["log_lines"]:
             log.msg("<< [{0}]".format(line))
@@ -279,9 +286,18 @@ class Game(protocol.Protocol):
                 self.observer.emit("user-game", (self.factory.player.user, self.game))
 
         # Process.pas parse line
+        if line.startswith("Command [TL=]"):
+            # Ok, get the current sector from this
+            _, _, sector = line.partition("]:[")
+            sector, _, _ = sector.partition("]")
+            self.current_sector = int(sector)
+            log.msg("current sector: {0}".format(self.current_sector))
+
         if line.startswith("The shortest path (") or line.startswith("  TO > "):
             self.linestate = "warpline"
             self.lastwarp = 0
+        elif line.startswith(" Items     Status  Trading % of max OnBoard"):
+            self.linestate = "port"
         elif self.linestate == "warpline":
             if line == "":
                 self.linestate = ""
@@ -316,6 +332,11 @@ class Game(protocol.Protocol):
             self.current_sector = int(parts[2])
         elif self.linestate == "sector":
             self.sectorline(line)
+        elif self.linestate == 'port':
+            if line == '':
+                self.linestate = ''
+            else:
+                self.portline(line)
 
         self.observer.emit("game-line", line)
 
@@ -339,7 +360,7 @@ class Game(protocol.Protocol):
 
         # Store the text into the buffer before we inject into it.
 
-        self.buffer += chunk.decode("utf-8", "ignore")
+        self.buffer += chunk.decode("latin-1", "ignore")
         # log.msg("data: [{0}]".format(repr(chunk)))
 
         if b"TWGS v2.20b" in chunk and b"www.eisonline.com" in chunk:
@@ -353,7 +374,7 @@ class Game(protocol.Protocol):
                 )
                 chunk = (
                     chunk[0 : pos + len(target)]
-                    + message.encode()
+                    + message.encode('latin-1')
                     + chunk[pos + len(target) :]
                 )
 
@@ -366,7 +387,7 @@ class Game(protocol.Protocol):
         if self.to_player:
             self.queue_game.put(chunk)
 
-        # self.buffer += chunk.decode("utf-8", "ignore")
+        # self.buffer += chunk.decode("latin-1", "ignore")
 
         #
         # Begin processing the buffer
@@ -398,40 +419,6 @@ class Game(protocol.Protocol):
         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 = ""
@@ -473,7 +460,7 @@ class Player(protocol.Protocol):
             if type(chunk) == bytes:
                 self.transport.write(chunk)
             elif type(chunk) == str:
-                self.transport.write(chunk.encode())
+                self.transport.write(chunk.encode('latin-1'))
             else:
                 log.err("gameDataReceived: type (%s) given!".format(type(chunk)))
                 self.transport.write(chunk)
@@ -481,7 +468,7 @@ class Player(protocol.Protocol):
 
     def dataReceived(self, chunk):
         if self.user is None:
-            self.buffer += chunk.decode("utf-8", "ignore")
+            self.buffer += chunk.decode("latin-1", "ignore")
 
             parts = self.buffer.split("\x00")
             if len(parts) >= 5:
@@ -544,6 +531,42 @@ class Player(protocol.Protocol):
         log.msg("connectionFailed: %s" % why)
 
 
+
+class GlueFactory(protocol.ClientFactory):
+    # class GlueFactory(protocol.Factory):
+    maxDelay = 10
+    protocol = Game
+
+    def __init__(self, player: 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)
+
+
+
 if __name__ == "__main__":
     if "logfile" in config and config["logfile"]:
         log.startLogging(DailyLogFile("proxy.log", "."))