diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..38a2fa4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +parsers +__pycache__ +Plugins/__pycache__ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..a8c3483 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "valorlib"] + path = valorlib + url = https://github.com/swrlly/valorlib.git diff --git a/ConditionEffect.py b/ConditionEffect.py new file mode 100644 index 0000000..975d16b --- /dev/null +++ b/ConditionEffect.py @@ -0,0 +1,70 @@ +effect0 = { + "Quiet" : 2, + "Weak" : 4, + "Slowed" : 8, + "Sick" : 16, + "Dazed" : 32, + "Stunned" : 64, + "Blind" : 128, + "Drunk" : 512, + "Confused" : 1024, + "StunImmune" : 2048, + "Invisible" : 4096, + "Paralyzed" : 8192, + "Speedy" : 16384, + "NinjaSpeedy" : 268435456, + "Hallucinating" : 256, + "Healing" : 131072, + "Damaging" : 262144, + "Berserk" : 524288, + "Paused" : 1048576, + "Stasis" : 2097152, + "Invincible" : 8388608, + "Invulnerable" : 16777216, + "Armored" : 33554432, + "ArmorBroken" : 67108864, + "ArmorBrokenImmune" : 65536, + "SlowedImmune" : 2147483648, + "Unstable" : 536870912, + "Darkness" : 1073741824, + "Bleeding" : 32768 +} + +effect1 = { + "Swiftness" : 16, + "ParalyzeImmune" : 2, + "DazedImmune" : 1, + "Petrified" : 4, + "PetrifiedImmune" : 8, + "Cursed" : 32, + "CursedImmune" : 64, + "Hidden" : 32768, + "SamuraiBerserk" : 8388608, + "Relentless" : 67108864, + "Vengeance" : 134217728, + "Alliance" : 536870912, + "Grasp" : 4194304, + "Bravery" : 262144, + "Exhausted" : 524288, + "JacketOffense" : 2, + "JacketDefense" : 4 +} + +effect2 = { + "EmpoweredImmunity" : 8, + "ConfusedImmunity" : 16, + "WeakImmunity" : 32, + "BlindImmunity" : 64, + "QuietImmunity" : 128, + "BleedingImmunity" : 256, + "SickImmunity" : 512, + "DrunkImmunity" : 1024, + "HallucinatingImmunity" : 2048, + "HexedImmunity" : 4096, + "UnstableImmunity" : 8192, + "DarknessImmunity" : 16384, + "ExhaustedImmunity" : 32768, + "StasisImmune" : 4194304, + "CorruptedImmune" : 65536 +} + diff --git a/PluginManager.py b/PluginManager.py new file mode 100644 index 0000000..0f07b84 --- /dev/null +++ b/PluginManager.py @@ -0,0 +1,54 @@ +class PluginManager: + + + def __init__(self): + # key is plugin class, value is true if on else false + self.plugins = {} + # key is packet type, value is set of plugin classes (key for self.plugins) + self.hooks = {} + + """ + initialize all plugins. + creates a dictionary with key + returns true if success, false o.w. + """ + def initializePlugins(self) -> bool: + + import os + for plugin in os.listdir("Plugins"): + if plugin[-3:] == ".py": + t = plugin.replace(".py", "") + exec("from Plugins.{} import *".format(t)) + try: + # by default, the plugin is not active. + if eval(t).load == True: + self.plugins.update({eval(t + "()") : False}) + except Exception as e: + print("There was an error when loading plugins. Make sure you follow the naming convention when writing your own plugins.") + print("Error:", e) + return False + + print("[Initializer]: Successfully loaded {} plugins.".format(len(self.plugins))) + return True + + + + """ + Creates a dictionary with key = PacketType, value = key of plugins + allows us to just call specific plugins on specific packets + """ + def initializeHookDictionary(self): + for plugin in self.plugins: + for hook in plugin.hooks: + # add new packettype hook + if hook not in self.hooks: + self.hooks.update({hook : {plugin}}) + # already exists + else: + self.hooks[hook].add(plugin) + + def initialize(self): + if not self.initializePlugins(): + return False + self.initializeHookDictionary() + return True \ No newline at end of file diff --git a/Plugins/Godmode.py b/Plugins/Godmode.py new file mode 100644 index 0000000..bdd3027 --- /dev/null +++ b/Plugins/Godmode.py @@ -0,0 +1,44 @@ +# these imports are always necessary +from valorlib.Packets.Packet import * +from client import Client + +# this is a short tutorial on how to write a plugin that will **only edit packets, not send new ones.** +# make sure the class name is capitalized, and matches the file's name. +class Godmode: + + """ + for each plugin, you need to instantiate a *class variable* called hook. + make sure this is a set. + this will tell the program what packets you intend to hook + why? suppose you have 10 plugins that utilize NewTick. You don't want to reread + newtick 10 times. Also, you only want to call the plugins which contain a newtick + hook. Remember, the faster this proxy is, the faster it can route packets. + """ + + hooks = {PacketTypes.PlayerHit, PacketTypes.GroundDamage} + + # also, make sure you put this class variable to tell the PluginManager whether to load this plugin or not. If this is absent, + # the manager will throw an exception. + load = True + + """ + Next, you need to write functions that will handle each packet type in your hooks. + Make sure your function name is on + the capitalization found in PacketTypes.py, otherwise your function will not be called. + This is all you need to write. Here is an example + + def onPacketType(self, client: Client, packet: Packet, send: bool) + client is an instance of client + packet is an instance of the specific packet type your function will handle. + send is whether or not this packet will be sent + returns: (updated packet, send) + send = true if you wish to send the packet, else false + + Below is an example of these handlers. Since godmode is just blocking the packet, we can just set send to false + + """ + + def onPlayerHit(self, client: Client, packet: PlayerHit, send: bool) -> (PlayerHit, bool): + return (packet, False) + + def onGroundDamage(self, client: Client, packet: GroundDamage, send: bool) -> (GroundDamage, bool): + return (packet, False) diff --git a/Plugins/Invulnerable.py b/Plugins/Invulnerable.py new file mode 100644 index 0000000..aa39556 --- /dev/null +++ b/Plugins/Invulnerable.py @@ -0,0 +1,72 @@ +from valorlib.Packets.Packet import * +from ConditionEffect import * +from client import Client + +""" +this does not work. works for like one bullet or something +""" + +class Invulnerable: + + hooks = {PacketTypes.NewTick} + load = False + + def onNewTick(self, client: Client, packet: NewTick, send: bool) -> (NewTick, bool): + + for obj in range(len(packet.statuses)): + + # got a packet that updates our stats + if packet.statuses[obj].objectID == client.objectID: + + for s in range(len(packet.statuses[obj].stats)): + + if packet.statuses[obj].stats[s].statType == 29: + packet.statuses[obj].stats[s].statValue |= effect0['Invulnerable'] + break + + # if we didn't receive a packet that will update our stats, just add a new statdata object that does + else: + s = StatData() + s.statType = 29 + s.statValue = client.effect0bits | effect0['Invulnerable'] + client.effect0bits |= effect0['Invulnerable'] + packet.statuses[obj].stats.append(s) + break + + # else if the newtick doesn't modify our stats, create a new objectstatusdata that gives speedy + else: + o = ObjectStatusData() + o.objectID = client.objectID + # doesn't even matter if position isn't valid + o.pos = WorldPosData() + s = StatData() + s.statType = 29 + s.statValue = client.effect0bits | effect0['Invulnerable'] + client.effect0bits |= effect0['Invulnerable'] + packet.statuses.append(o) + + + return (packet, send) + + # here we also utilize packet injection to turn off the hack + + def shutdown(self, client: Client) -> None: + + packet2 = NewTick() + packet2.tickID = 0 + packet2.tickTime = 0 + o = ObjectStatusData() + o.objectID = client.objectID + # doesn't even matter if position isn't valid + o.pos = WorldPosData() + s = StatData() + s.statType = 29 + # remove speedy + if client.effect1bits & effect0['Invulnerable']: + s.statValue = client.effect0bits - effect0['Invulnerable'] + else: + s.statValue = client.effect0bits + o.stats.append(s) + packet2.statuses.append(o) + packet2 = CreatePacket(packet2) + client.SendPacketToClient(packet2) \ No newline at end of file diff --git a/Plugins/NoDebuff.py b/Plugins/NoDebuff.py new file mode 100644 index 0000000..2fdfe6b --- /dev/null +++ b/Plugins/NoDebuff.py @@ -0,0 +1,53 @@ +from valorlib.Packets.Packet import * +from ConditionEffect import * +from client import Client + +class NoDebuff: + + """ + Need to research a bit more into blocking the effect entirely. Only removes after new tick happens; client is in control of the behavior of status effect. + This means you need some type of dict that tells you each enemy's bullet IDs, then join bullet ID with object ID in update packet probably. + Search XML's next + """ + + hooks = {PacketTypes.NewTick} + load = True + effect0Remove = ["Quiet", "Hallucinating", "Weak", "Slowed", "Sick", "Stunned", "Blind", "Drunk", "Confused", "Paralyzed", "Stasis", "ArmorBroken", "Darkness", "Unstable", "Bleeding"] + + def onNewTick(self, client: Client, packet: NewTick, send: bool) -> (NewTick, bool): + + + for obj in range(len(packet.statuses)): + if packet.statuses[obj].objectID == client.objectID: + for s in range(len(packet.statuses[obj].stats)): + # if we got a packet representing us and a conditioneffect statdata + if packet.statuses[obj].stats[s].statType == 29: + # for each debuff + for remove in self.effect0Remove: + # if the bit is on + if packet.statuses[obj].stats[s].statValue & (1 << self.getExponent(effect0[remove])): + # remove the bit + packet.statuses[obj].stats[s].statValue -= effect0[remove] + # keep track of the state + client.effect0bits -= effect0[remove] + break + # if we did not get a packet, just inject a new one without bad status effects + else: + s = StatData() + s.statType = 29 + for remove in self.effect0Remove: + if client.effect0bits & (1 << self.getExponent(effect0[remove])): + client.effect0bits -= effect0[remove] + s.statValue = client.effect0bits + packet.statuses[obj].stats.append(s) + + return (packet, send) + + # given a number of the form 2^k, k >= 1 returns k + # if you give it a number x where 2^k <= x < 2^{k+1}, it will return k. + def getExponent(self, num): + cnt = 0 + while num != 1: + num = num // 2 + cnt += 1 + return cnt \ No newline at end of file diff --git a/Plugins/NoProjectile.py b/Plugins/NoProjectile.py new file mode 100644 index 0000000..edfeee8 --- /dev/null +++ b/Plugins/NoProjectile.py @@ -0,0 +1,10 @@ +from valorlib.Packets.Packet import * +from client import Client + +class NoProjectile: + + hooks = {PacketTypes.EnemyShoot} + load = True + + def onEnemyShoot(self, client: Client, packet: Packet, send: bool) -> (EnemyShoot, bool): + return (packet, False) \ No newline at end of file diff --git a/Plugins/Speedy.py b/Plugins/Speedy.py new file mode 100644 index 0000000..1581de5 --- /dev/null +++ b/Plugins/Speedy.py @@ -0,0 +1,73 @@ +from valorlib.Packets.Packet import * +from ConditionEffect import * +from client import Client + +""" +here is a more involved situation where we edit the NewTick packet. +""" + +class Speedy: + + hooks = {PacketTypes.NewTick} + load = True + + def onNewTick(self, client: Client, packet: NewTick, send: bool) -> (NewTick, bool): + + for obj in range(len(packet.statuses)): + + # got a packet that updates our stats + if packet.statuses[obj].objectID == client.objectID: + + for s in range(len(packet.statuses[obj].stats)): + + if packet.statuses[obj].stats[s].statType == 29: + packet.statuses[obj].stats[s].statValue |= effect0['Speedy'] + break + + # if we didn't receive a packet that will update our stats, just add a new statdata object that does + else: + s = StatData() + s.statType = 29 + s.statValue = client.effect0bits | effect0['Speedy'] + # update the internal bit state to account for 1+ status effect mods + client.effect0bits |= effect0['Speedy'] + packet.statuses[obj].stats.append(s) + break + + # else if the newtick doesn't modify our stats, create a new objectstatusdata that gives speedy + else: + o = ObjectStatusData() + o.objectID = client.objectID + # doesn't even matter if position isn't valid + o.pos = WorldPosData() + s = StatData() + s.statType = 29 + s.statValue = client.effect0bits | effect0['Speedy'] + client.effect0bits |= effect0['Speedy'] + packet.statuses.append(o) + + + + return (packet, send) + + def shutdown(self, client: Client) -> None: + + packet2 = NewTick() + packet2.tickID = 0 + packet2.tickTime = 0 + o = ObjectStatusData() + o.objectID = client.objectID + # doesn't even matter if position isn't valid + o.pos = WorldPosData() + s = StatData() + s.statType = 29 + # remove speedy, this is why we keep the internal state + if client.effect0bits & effect0['Speedy']: + s.statValue = client.effect0bits - effect0['Speedy'] + client.effect0bits -= effect0['Speedy'] + else: + s.statValue = client.effect0bits + o.stats.append(s) + packet2.statuses.append(o) + packet2 = CreatePacket(packet2) + client.SendPacketToClient(packet2) \ No newline at end of file diff --git a/Plugins/Swiftness.py b/Plugins/Swiftness.py new file mode 100644 index 0000000..06b0062 --- /dev/null +++ b/Plugins/Swiftness.py @@ -0,0 +1,76 @@ +from valorlib.Packets.Packet import * +from ConditionEffect import * +from client import Client + +""" +Stacks with speedy! +""" + +class Swiftness: + + hooks = {PacketTypes.NewTick} + load = True + word = "Swiftness" + + def onNewTick(self, client: Client, packet: NewTick, send: bool) -> (NewTick, bool): + + + + for obj in range(len(packet.statuses)): + + # got a packet that updates our stats + if packet.statuses[obj].objectID == client.objectID: + + for s in range(len(packet.statuses[obj].stats)): + + if packet.statuses[obj].stats[s].statType == 96: + packet.statuses[obj].stats[s].statValue |= effect1[self.word] + break + + # if we didn't receive a packet that will update our stats, just add a new statdata object that does + else: + s = StatData() + s.statType = 96 + s.statValue = client.effect1bits | effect1[self.word] + # update the internal bit state to account for 1+ status effect mods + client.effect1bits |= effect1[self.word] + packet.statuses[obj].stats.append(s) + break + + # else if the newtick doesn't modify our stats, create a new objectstatusdata that gives speedy + else: + o = ObjectStatusData() + o.objectID = client.objectID + # doesn't even matter if position isn't valid + o.pos = WorldPosData() + s = StatData() + s.statType = 96 + s.statValue = client.effect1bits | effect1[self.word] + client.effect1bits |= effect1[self.word] + packet.statuses.append(o) + + + + return (packet, send) + + def shutdown(self, client: Client) -> None: + + packet2 = NewTick() + packet2.tickID = 0 + packet2.tickTime = 0 + o = ObjectStatusData() + o.objectID = client.objectID + # doesn't even matter if position isn't valid + o.pos = WorldPosData() + s = StatData() + s.statType = 96 + # remove speedy + if client.effect1bits & effect1[self.word]: + s.statValue = client.effect1bits - effect1[self.word] + client.effect1bits -= effect1[self.word] + else: + s.statValue = client.effect1bits + o.stats.append(s) + packet2.statuses.append(o) + packet2 = CreatePacket(packet2) + client.SendPacketToClient(packet2) \ No newline at end of file diff --git a/RC4.py b/RC4.py new file mode 100644 index 0000000..711958e --- /dev/null +++ b/RC4.py @@ -0,0 +1,42 @@ +""" +Decrypts a stream of bytes, maintaining an internal state. +""" + +class RC4: + + def __init__(self, key): + + self.key = key + self.reset() + + """ every time you enter a new map, reset ciphers""" + def reset(self): + + self.S = list(range(256)) + j = 0 + + for i in range(256): + j = j + self.S[i] + self.key[i % len(self.key)] & 255 + tmp = self.S[i] + self.S[i] = self.S[j] + self.S[j] = tmp + + self.i = 0 + self.j = 0 + + + + def encrypt(self, data): + + for idx in range(len(data)): + + self.i = self.i + 1 & 255 + self.j = self.j + self.S[self.i] & 255 + tmp = self.S[self.i] + self.S[self.i] = self.S[self.j] + self.S[self.j] = tmp; + + data[idx] = data[idx] ^ self.S[tmp + self.S[self.i] & 255] + + def decrypt(self, data): + self.encrypt(data) diff --git a/README.md b/README.md new file mode 100644 index 0000000..5fdfc2b --- /dev/null +++ b/README.md @@ -0,0 +1,59 @@ +# vrelay + +A man-in-the-middle proxy server for the RotMG private server Valor. This is the Python version of [KRelay](https://github.com/TheKronks/KRelay) but for Valor RotMG. + +The goal is to learn more about networking, reverse engineering, and writing hacks for RotMG. + +**Updated for Valor version 3.2.2.** + +

+ +

+ + +## How to use + +1. Install Python 64-bit. You can find installations of Python (here)[https://www.python.org/downloads/]. Install the 64-bit version or the GUI will not work. +2. A download link to the hack will be provided below. In this step, download the hack and unzip it. + - *Note*: Github is not a virus site. It's just a site to collaborate on software engineering projects. It is widely used in tech companies. +3. Replace your original `Valor.swf` with the `Valor.swf` in the unzipped folder. This `.swf` file has been modded so you can connect to the proxy server. If you only see `Valor`, that means you have your file extensions off. +4. Open the command line (type `⊞ + r` and type in `cmd`, press enter. the command line will now be open), `cd` into the folder containing the code. + - This will involve typing `cd C:\path\to\folder` then pressing enter. + - If you downloaded the hack in another drive, such as `Z:` or `D:`, then you need to type the drive name (`Z:` or `D:`, respectively) in order to tell command prompt you wish to be in that drive. +5. Once you're in the folder in the command prompt, type `python proxy.py` in the command line to start the proxy. If you have previously installed Python, also try `py proxy.py` or `python3 proxy.py` if this does not work. +6. Connect to the proxy server in the server list and you're good to go. + + +## Features + +All features have a button in the GUI to turn the hack on or off. + +- **Godmode**: Immune to all bullet and ground damage. Does not block AoE damage. See this [YouTube video for a demo.](https://www.youtube.com/watch?v=cNerTN7HwhM) +- **No projectile**: Hides all projectiles from appearing; essentially another godmode (but very obvious to others as you do not know where to dodge). + - Does not hide AoE damage (you will still take damage from AoE). +- **Speedy**: Apply speedy to yourself! +- **Swiftness**: Apply swiftness (stronger form of Speedy) to yourself! Stacks with Speedy. +- **Remove client-side debuffs**: This will remove client-side debuffs one tick after they are applied. These will not remove server-sided debuffs like bleeding, quiet, etc. +- **Shut down all plugins**: This will turn off all active plugins. Reduces amount of clicking you need to do if you have many plugins active. +- Ability to write your own plugins! +- Ability to inject packets. + +## Writing your own plugins +Will document this section later. For now: + +- See `Plugins/Godmode.py` for an explanation on what functions to implement. +- See `Plugins/Speedy.py` to see an in-depth example on how the `NewTick` packet was modified to apply speedy. + + +## Notes +- You can use `/dep` to put potions into potion vault anywhere (in dungeons and realms). Currently spams your screen tho. + +## Credits +- [JPEXS](https://www.free-decompiler.com/flash/download/) for reverse engineering and modifying the client. + +#### TODO: +- Actual KA instead of enemy-dependent +- Auto Aim? +- Block status effects +- Add a force restart +- There is a bug where if Godmode and either Speedy/Swiftness are both active, all negative status effects are cancelled. While this is great, this is a bug from a software persepective. \ No newline at end of file diff --git a/Valor.swf b/Valor.swf new file mode 100644 index 0000000..980ad40 Binary files /dev/null and b/Valor.swf differ diff --git a/client.py b/client.py new file mode 100644 index 0000000..2c3e73c --- /dev/null +++ b/client.py @@ -0,0 +1,514 @@ +import socket +import struct +import select +import time + +from valorlib.Packets.Packet import * +from PluginManager import * +from RC4 import RC4 + +class Client: + """ + This class holds data relevant to clients. + in here, we will also put the sockets. this allows the client class to send packets. + from a design persepctive, this also makes sense since this can scale to clientless + """ + + def __init__(self, pm: PluginManager): + self.remoteHostAddr = "51.222.11.213" + self.remoteHostPort = 2050 + self.objectID = None + self.charID = None + self.reconnecting = False + self.connected = False + self.clientSendKey = RC4(bytearray.fromhex("BA15DE")) + self.clientReceiveKey = RC4(bytearray.fromhex("612a806cac78114ba5013cb531")) + self.serverSendKey = RC4(bytearray.fromhex("612a806cac78114ba5013cb531")) + self.serverReceiveKey = RC4(bytearray.fromhex("BA15DE")) + self.gameSocket = None + self.serverSocket = None + self.pluginManager = pm + self.currentMap = "None" + self.gameIDs = { + -1 : "Nexus", + -2 : "Nexus", + -5 : "Vault", + -15 : "Marketplace", + -16 : "Ascension Enclave", + -17 : "Aspect Hall" + } + + # stuff to ignore when debugging + #self.ignoreIn = [] + #self.ignoreOut = [] + self.ignoreOut = [PacketTypes.Message, PacketTypes.Move, PacketTypes.Pong, PacketTypes.GotoAck, PacketTypes.PlayerShoot, PacketTypes.ShootAck] + self.ignoreIn = [PacketTypes.Ping, PacketTypes.Goto, PacketTypes.Update, PacketTypes.NewTick] + + # client state syncs, these are public + self.disableSpeedy = False + self.disableInvulnerable = False + self.disableSwiftness = False + self.disableVengeance = False + + # containerType of the weapon you are using + self.containerType = 0 + # first bullet time. allows server sync + self.firstBulletTime = None + # internal bullet ID to spoof packets + self.internalBulletID = 0 + # last time enemies damaged themselves + self.lastEnemySelfDamage = 0 + # last time you multiplied damage + self.lastEnemyHitSpam = 0 + + self.effect0bits = 0 + self.effect1bits = 0 + self.effect2bits = 0 + + + self.lastReconnect = 0 + self.lastReconKey = [] + self.lastGameID = 0 + self.useDisconnectedKey = None + + # disconnect the client from the proxy + # disconnect the proxy from the server + def Disconnect(self): + self.connected = False + if self.serverSocket: + self.serverSocket.shutdown(socket.SHUT_RDWR) + self.serverSocket.close() + if self.gameSocket: + self.gameSocket.shutdown(socket.SHUT_RDWR) + self.gameSocket.close() + self.gameSocket = None + self.serverSocket = None + + # reset ciphers to default state + def ResetCipher(self): + self.clientSendKey.reset() + self.clientReceiveKey.reset() + self.serverSendKey.reset() + self.serverReceiveKey.reset() + + # for now, we can just recon lazily + def Reconnect(self): + self.ConnectRemote(self.remoteHostAddr, self.remoteHostPort) + self.connected = True + + # Connect to remote host. Block until client connected + def ConnectRemote(self, host, port): + # the invalid recon key bug is when client doesn't connect to the proxy server's socket + # reduced sleep time and it seems to be ok now + while self.gameSocket == None: + time.sleep(0.005) + + self.serverSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.serverSocket.connect((host, port)) + + # closes both sockets + def Close(self): + self.gameSocket.close() + self.serverSocket.close() + + # restart entire connection + def reset(self): + self.internalBulletID = 0 + self.firstBulletTime = None + self.Disconnect() + self.ResetCipher() + self.Reconnect() + + # call this fcn when you reset connection + def resetMapName(self): + self.currentMap = "Nexus" + + """ + Listens to packets from the client. + """ + def ListenToClient(self): + + header = self.gameSocket.recv(5) + + if len(header) == 0 or self.reconnecting: + # try and fix this possible bug? why is the client sending nothing?? + # this only happens when you nexus + # print("got length 0 packet from client") + self.reset() + return + + #while len(header) != 5: + # header += self.gameSocket.recv(5 - len(header)) + + packetID = header[4] + expectedPacketLength = struct.unpack("!i", header[:4])[0] + + # read the packet, subtract 5 cuz you already read header + leftToRead = expectedPacketLength - 5 + data = bytearray() + + while (leftToRead > 0): + buf = bytearray(self.gameSocket.recv(leftToRead)) + data += buf + leftToRead -= len(buf) + + # decipher it to update our internal state + self.clientSendKey.decrypt(data) + packet = Packet(header, data, packetID) + send = True + + """ + # for debugging + try: + if packet.ID not in self.ignoreOut: + print("Client sent:", PacketTypes.reverseDict[packet.ID]) + except: + print("Got unknown packet from client") + """ + + # hooks + if packet.ID == PacketTypes.PlayerText: + self.onPlayerText(packet) + + elif packet.ID == PacketTypes.Hello: + # hello is always sent, try another map update name here + p = Hello() + p.read(packet.data) + + if p.gameID in self.gameIDs: + self.currentMap = self.gameIDs[p.gameID] + + # plugin management + elif packet.ID == PacketTypes.PlayerHit: + packet, send = self.routePacket(packet, send, self.onPlayerHit) + + elif packet.ID == PacketTypes.GroundDamage: + packet, send = self.routePacket(packet, send, self.onGroundDamage) + + elif packet.ID == PacketTypes.EnemyHit: + packet, send = self.routePacket(packet, send, self.onEnemyHit) + + elif packet.ID == PacketTypes.PlayerShoot: + # make sure you're tracking the internal state + # update the id now since this will percolate downstream to enemyhit + self.internalBulletID = (self.internalBulletID + 1) % 128 + packet, send = self.routePacket(packet, send, self.onPlayerShoot) + + + elif packet.ID == PacketTypes.OtherHit: + # seems to help with the KA packet rejection rate + # send = False + pass + + if not send: + return + else: + self.SendPacketToServer(packet) + + """ + Listens to packets from the server. + """ + def ListenToServer(self): + + header = self.serverSocket.recv(5) + + if len(header) == 0 or self.reconnecting: + # try and fix this possible bug? this happens every so often. + # why this bug happens: client sends hello, but then server sends nothing. + # doesn't matter if you keep trying to read bytes, server always sends nothing. + # print("got 0 length packet from server") + self.reset() + return + + + #while len(header) != 5: + # header += self.serverSocket.recv(5 - len(header)) + + packetID = header[4] + expectedPacketLength = struct.unpack("!i", header[:4])[0] + # read the packet, subtract 5 cuz you already read header + leftToRead = expectedPacketLength - 5 + data = bytearray() + + while (leftToRead > 0): + buf = bytearray(self.serverSocket.recv(leftToRead)) + data += buf + leftToRead -= len(buf) + + # decipher it to update our internal state + self.serverSendKey.decrypt(data) + packet = Packet(header, data, packetID) + send = True + + """ + # for debugging + try: + if packet.ID not in self.ignoreIn: + print("Server sent:", PacketTypes.reverseDict[packet.ID]) + except: + print("Got unknown packet from server, id", packet.ID) + """ + + # hooks + if packet.ID == PacketTypes.CreateSuccess: + self.OnCreateSuccess(packet) + + elif packet.ID == PacketTypes.Reconnect: + # update map name. + p = Reconnect() + p.read(packet.data) + #p.PrintString() + self.lastReconnect = time.time() + self.lastGameID = p.gameID + self.lastReconKey = p.key + self.currentMap = p.name + self.reconnecting = True + + # update internal effect state + elif packet.ID == PacketTypes.NewTick: + """ + p = NewTick() + p.read(packet.data) + p.PrintString() + for obj in range(len(p.statuses)): + if p.statuses[obj].objectID != self.objectID: + p.statuses[obj].PrintString() + for s in range(len(p.statuses[obj].stats)): + p.statuses[obj].stats[s].PrintString() + """ + packet, send = self.routePacket(packet, send, self.onNewTick) + + elif packet.ID == PacketTypes.EnemyShoot: + packet, send = self.routePacket(packet, send, self.onEnemyShoot) + + elif packet.ID == PacketTypes.Failure: + packet, send = self.routePacket(packet, send, self.onFailure) + + + if send: + self.SendPacketToClient(packet) + + """ + Starts to listen for packets + """ + def Listen(self): + + while True: + try: + + ready = select.select([self.gameSocket, self.serverSocket], [], [])[0] + + # prioritize reconnects + if self.reconnecting: + + if self.gameSocket in ready: + self.gameSocket.recv(50000) + + if self.serverSocket in ready: + self.serverSocket.recv(50000) + + self.onReconnect() + self.reconnecting = False + return + + elif self.connected: + + # client has data ready to send to server + if self.gameSocket in ready: + self.ListenToClient() + # server has data ready to send to client + if self.serverSocket in ready: + self.ListenToServer() + + except ConnectionAbortedError as e: + print("Connection was aborted:", e) + self.reset() + self.resetMapName() + + except ConnectionResetError as e: + print("Connection was reset") + self.reset() + self.resetMapName() + + except KeyboardInterrupt: + print("User aborted. Shutting down proxy.") + self.connected = False + self.Close() + return + + #except Exception as e: + # print("Something went terribly wrong.") + # print("Error:", e) + # print("Restarting...") + # self.reset() + + # activate godmode (actually not needed) + def ActivateGodmode(self, packet) -> Packet: + p = PlayerHit() + p.read(packet.data) + p.objectID = 0 + packet = CreatePacket(p) + return packet + + + """ + + Given a specific packet type, call the relevant client onPacketType function to read the packet + Then, iterate through hook dict to call plugins which hook this packet. + + :param packet: A Packet object + :param send: whether or not to send the packet + :param onPacketType: The implemented callback inside a plugin when this packet type is encountered. + This function will be defined within the Client class. + + returns: (Packet, send) + """ + def routePacket(self, packet: Packet, send, onPacketType) -> (Packet, bool): + + p = onPacketType(packet) + #modified = False + + if packet.ID in self.pluginManager.hooks: + for plugin in self.pluginManager.hooks[packet.ID]: + # if the plugin is active + if self.pluginManager.plugins[plugin]: + # at each step, we are editing the packet on the wire + # also make sure you're spelling your class methods correctly. + p, send = getattr(plugin, "on" + PacketTypes.reverseDict[packet.ID])(self, p, send) + modified = True + + # always create a new packet; this ensures our internal bullet ID state is synced + # with our real shots + #if modified: + packet = CreatePacket(p) + return (packet, send) + + + # server -> client + def SendPacketToClient(self, packet): + self.clientReceiveKey.encrypt(packet.data) + self.gameSocket.sendall(packet.format()) + + # client -> server + def SendPacketToServer(self, packet): + self.serverReceiveKey.encrypt(packet.data) + self.serverSocket.sendall(packet.format()) + + # set objid to client's + def OnCreateSuccess(self, packet): + p = CreateSuccess() + p.read(packet.data) + self.objectID = p.objectID + self.charID = p.charID + + +########################## +# various necessary hooks +########################## + + def onFailure(self, packet: Packet) -> Failure: + p = Failure() + p.read(packet.data) + #p.PrintString() + return p + + def onPlayerHit(self, packet: Packet) -> PlayerHit: + p = PlayerHit() + p.read(packet.data) + return p + + def onGroundDamage(self, packet: Packet) -> GroundDamage: + p = GroundDamage() + p.read(packet.data) + return p + + def onEnemyShoot(self, packet: Packet) -> EnemyShoot: + p = EnemyShoot() + p.read(packet.data) + return p + + def onEnemyHit(self, packet: Packet) -> EnemyHit: + p = EnemyHit() + p.read(packet.data) + # since we have plugins that modify the bulletID, we need to make sure bulletID is synced as well + #p.bulletID = self.internalBulletID + return p + + def onPlayerShoot(self, packet: Packet) -> PlayerShoot: + p = PlayerShoot() + p.read(packet.data) + self.containerType = p.containerType + # to get bullet clock syncs, do int(time.time() * 1000 - self.firstBulletTime) + # fix this later lmfao + if self.firstBulletTime == None: + self.firstBulletTime = time.time() * 1000 - p.time + # this is causing bugs for multishot piercing weapons. ex: decimator goes from 0 -> 6 -> 12 every single playershoot + #p.bulletID = self.internalBulletID + return p + + def onNewTick(self, packet: Packet) -> NewTick: + p = NewTick() + p.read(packet.data) + + for obj in range(len(p.statuses)): + # got a packet that updates our stats + if p.statuses[obj].objectID == self.objectID: + for s in range(len(p.statuses[obj].stats)): + # 29, effect 0 + # 96, effect 1 + # 205, effect 2 + # armored, damaging serversided + + if p.statuses[obj].stats[s].statType == 29: + self.effect0bits = p.statuses[obj].stats[s].statValue + + elif p.statuses[obj].stats[s].statType == 96: + self.effect1bits = p.statuses[obj].stats[s].statValue + + elif p.statuses[obj].stats[s].statType == 205: + self.effect2bits = p.statuses[obj].stats[s].statValue + + # now cover edge cases in plugins + # pretty terrible engineering + # this ensures our injected status effects are turned off and doesn't interfere with real speedy (like from warrior) + self.turnOffInjectedStatuses() + + return p + + def turnOffInjectedStatuses(self): + + for plugin in self.pluginManager.plugins: + if self.disableSpeedy and type(plugin).__name__ == "Speedy": + plugin.shutdown(self) + self.disableSpeedy = False + + elif self.disableSwiftness and type(plugin).__name__ == "Swiftness": + plugin.shutdown(self) + self.disableSwiftness = False + + elif self.disableInvulnerable and type(plugin).__name__ == "Invulnerable": + plugin.shutdown(self) + self.disableInvulnerable = False + + elif self.disableVengeance and type(plugin).__name__ == "Vengeance": + plugin.shutdown(self) + self.disableVengeance = False + + # playertext hook + def onPlayerText(self, packet) -> None: + p = PlayerText() + p.read(packet.data) + + if p.text == "/dep": + for i in range(0, 12): + pp = PotionStorageInteraction() + pp.type = i + pp.action = 0 + self.SendPacketToServer(CreatePacket(pp)) + + return p + + # when server sends reconnect + def onReconnect(self): + + self.reset() \ No newline at end of file diff --git a/gui.py b/gui.py new file mode 100644 index 0000000..97fcd2d --- /dev/null +++ b/gui.py @@ -0,0 +1,267 @@ +import time +import sys + +from tkinter import * +from PluginManager import * +from client import Client + +class GUI: + + COLWIDTH = 20 + PADX = 10 + PADY = 5 + HEIGHT = 2 + RED = "#CF0029" + GREEN = "#43810D" + + + def __init__(self, pm: PluginManager, client: Client, proxy): + + # new gui + self.pluginManager = pm + self.client = client + self.proxy = proxy + self.root = Tk() + self.root.geometry("460x220") + self.root.resizable(False, False) + self.root.title("vrelay by swrlly - for valor v3.2.2") + + self.buttonFrame = Frame(self.root) + self.buttonFrame.grid(row = 0, column = 0) + + # godmode text + self.godmodelabel = Label(self.buttonFrame, text = "Godmode", width = self.COLWIDTH, height = self.HEIGHT) + self.godmodelabel.grid(row = 0, column = 0, padx = self.PADX, pady = self.PADY) + + # godmode button + self.godmodetxt = StringVar() + self.godmodetxt.set("OFF") + self.godmodebtn = Button(self.buttonFrame, bd = 5, textvariable = self.godmodetxt, command = self.godmodeHandler, fg = self.RED) + self.godmodebtn.grid(row = 0, column = 1, padx = self.PADX, pady = self.PADY) + + # noprojectile text + self.noprojectilelabel = Label(self.buttonFrame, text = "Hide projectiles", width = self.COLWIDTH, height = self.HEIGHT) + self.noprojectilelabel.grid(row = 1, column = 0, padx = self.PADX, pady = self.PADY) + + # noprojectile button + self.noprojectiletxt = StringVar() + self.noprojectiletxt.set("OFF") + self.noprojectilebtn = Button(self.buttonFrame, bd = 5, textvariable = self.noprojectiletxt, command = self.noProjectileHandler, fg = self.RED) + self.noprojectilebtn.grid(row = 1, column = 1, padx = self.PADX, pady = self.PADY) + + # speedy text + self.speedylabel = Label(self.buttonFrame, text = "Speedy", width = self.COLWIDTH, height = self.HEIGHT) + self.speedylabel.grid(row = 0, column = 2, padx = self.PADX, pady = self.PADY) + + # speedy button + self.speedytxt = StringVar() + self.speedytxt.set("OFF") + self.speedybtn = Button(self.buttonFrame, bd = 5, textvariable = self.speedytxt, command = self.speedyHandler, fg = self.RED) + self.speedybtn.grid(row = 0, column = 3, padx = self.PADX, pady = self.PADY) + + # swiftness text + self.swiftnesslabel = Label(self.buttonFrame, text = "Swiftness", width = self.COLWIDTH, height = self.HEIGHT) + self.swiftnesslabel.grid(row = 1, column = 2, padx = self.PADX, pady = self.PADY) + + # swiftness button + self.swiftnesstxt = StringVar() + self.swiftnesstxt.set("OFF") + self.swiftnessbtn = Button(self.buttonFrame, bd = 5, textvariable = self.swiftnesstxt, command = self.swiftnessHandler, fg = self.RED) + self.swiftnessbtn.grid(row = 1, column = 3, padx = self.PADX, pady = self.PADY) + + # extradamage text + #self.extradamagelabel = Label(self.buttonFrame, text = "Extra damage\n(non-piercing only!)", width = self.COLWIDTH, height = self.HEIGHT) + #self.extradamagelabel.grid(row = 2, column = 0, padx = self.PADX, pady = self.PADY) + + # extradamage button + # self.extradamagetxt = StringVar() + # self.extradamagetxt.set("OFF") + # self.extradamagebtn = Button(self.buttonFrame, bd = 5, textvariable = self.extradamagetxt, command = self.extraDamageHandler, fg = self.RED) + # self.extradamagebtn.grid(row = 2, column = 1, padx = self.PADX, pady = self.PADY) + + # nodebuff text + self.nodebufflabel = Label(self.buttonFrame, text = "Remove client-side debuffs", width = self.COLWIDTH, height = self.HEIGHT) + self.nodebufflabel.grid(row = 2, column = 0, padx = self.PADX, pady = self.PADY) + + # nodebuff button + self.nodebufftxt = StringVar() + self.nodebufftxt.set("OFF") + self.nodebuffbtn = Button(self.buttonFrame, bd = 5, textvariable = self.nodebufftxt, command = self.noDebuffHandler, fg = self.RED) + self.nodebuffbtn.grid(row = 2, column = 1, padx = self.PADX, pady = self.PADY) + + # ka text + #self.kalabel = Label(self.buttonFrame, text = "Enemy-dependent kill aura \n (non-piercing only!)", width = self.COLWIDTH, height = self.HEIGHT) + #self.kalabel.grid(row = 3, column = 0, padx = self.PADX, pady = self.PADY) + + # ka button + # self.katxt = StringVar() + # self.katxt.set("OFF") + # self.kabtn = Button(self.buttonFrame, bd = 5, textvariable = self.katxt, command = self.badKillAuraHandler, fg = self.RED) + # self.kabtn.grid(row = 3, column = 1, padx = self.PADX, pady = self.PADY) + + # realm text + + self.textFrame = Frame(self.root) + self.textFrame.grid(row = 1, column = 0) + + self.shutdownbtn = Button(self.textFrame, bd = 5, text = "Shut down all plugins", command = self.shutdownHandler) + self.shutdownbtn.grid(row = 0, column = 0, padx = 20, pady = 20) + + self.location = StringVar() + self.location.set("Connected to {}!".format(client.currentMap)) + self.locationentry = Label(self.textFrame, bd = 1, textvariable = self.location) + self.locationentry.grid(row = 0, column = 1, padx = 20, pady = 20) + + + + def shutdownHandler(self): + + # turn off hooks + for plugin in self.pluginManager.plugins: + self.pluginManager.plugins[plugin] = False + + # change UI + self.godmodetxt.set("OFF") + self.godmodebtn.config(fg = self.RED) + self.noprojectiletxt.set("OFF") + self.noprojectilebtn.config(fg = self.RED) + #self.extradamagetxt.set("OFF") + #self.extradamagebtn.config(fg = self.RED) + self.speedytxt.set("OFF") + self.speedybtn.config(fg = self.RED) + self.swiftnesstxt.set("OFF") + self.swiftnessbtn.config(fg = self.RED) + #self.katxt.set("OFF") + #self.kabtn.config(fg = self.RED) + self.nodebufftxt.set("OFF") + self.nodebuffbtn.config(fg = self.RED) + + # next new tick packet will actually send shutdown packet + self.client.disableSpeedy = True + self.client.disableSwiftness = True + + """ + # restart text + self.restartlabel = Label(text = "Restart Proxy", width = self.COLWIDTH, height = self.HEIGHT) + self.restartlabel.grid(row = 2, column = 0, padx = self.PADX, pady = self.PADY) + + # restart button + self.restartlabeltxt = StringVar() + self.restartlabeltxt.set("Restart") + self.restartbtn = Button(self.root, bd = 5, textvariable = self.restartlabeltxt, command = self.restartHandler) + self.restartbtn.grid(row = 2, column = 1, padx = self.PADX, pady = self.PADY) + """ + + + # when the button is clicked + def godmodeHandler(self): + if self.pluginManager.plugins[self.findClass("Godmode")]: + self.pluginManager.plugins[self.findClass("Godmode")] = False + self.godmodetxt.set("OFF") + self.godmodebtn.config(fg = self.RED) + else: + self.pluginManager.plugins[self.findClass("Godmode")] = True + self.godmodetxt.set("ON") + self.godmodebtn.config(fg = self.GREEN) + + # when the button is clicked + def noDebuffHandler(self): + if self.pluginManager.plugins[self.findClass("NoDebuff")]: + self.pluginManager.plugins[self.findClass("NoDebuff")] = False + self.nodebufftxt.set("OFF") + self.nodebuffbtn.config(fg = self.RED) + else: + self.pluginManager.plugins[self.findClass("NoDebuff")] = True + self.nodebufftxt.set("ON") + self.nodebuffbtn.config(fg = self.GREEN) + + # when the button is clicked + def noProjectileHandler(self): + if self.pluginManager.plugins[self.findClass("NoProjectile")]: + self.pluginManager.plugins[self.findClass("NoProjectile")] = False + self.noprojectiletxt.set("OFF") + self.noprojectilebtn.config(fg = self.RED) + else: + self.pluginManager.plugins[self.findClass("NoProjectile")] = True + self.noprojectiletxt.set("ON") + self.noprojectilebtn.config(fg = self.GREEN) + + # when the button is clicked + def speedyHandler(self): + if self.pluginManager.plugins[self.findClass("Speedy")]: + self.client.disableSpeedy = True + self.pluginManager.plugins[self.findClass("Speedy")] = False + self.speedytxt.set("OFF") + self.speedybtn.config(fg = self.RED) + else: + self.client.disableSpeedy = False + self.pluginManager.plugins[self.findClass("Speedy")] = True + self.speedytxt.set("ON") + self.speedybtn.config(fg = self.GREEN) + + # when the button is clicked + def extraDamageHandler(self): + if self.pluginManager.plugins[self.findClass("ExtraDamage")]: + self.pluginManager.plugins[self.findClass("ExtraDamage")] = False + self.extradamagetxt.set("OFF") + self.extradamagebtn.config(fg = self.RED) + else: + self.pluginManager.plugins[self.findClass("ExtraDamage")] = True + self.extradamagetxt.set("ON") + self.extradamagebtn.config(fg = self.GREEN) + + # when the button is clicked + def swiftnessHandler(self): + if self.pluginManager.plugins[self.findClass("Swiftness")]: + self.client.disableSwiftness = True + self.pluginManager.plugins[self.findClass("Swiftness")] = False + self.swiftnesstxt.set("OFF") + self.swiftnessbtn.config(fg = self.RED) + else: + self.client.disableSwiftness = False + self.pluginManager.plugins[self.findClass("Swiftness")] = True + self.swiftnesstxt.set("ON") + self.swiftnessbtn.config(fg = self.GREEN) + + # when the button is clicked + def badKillAuraHandler(self): + if self.pluginManager.plugins[self.findClass("BadKillAura")]: + self.pluginManager.plugins[self.findClass("BadKillAura")] = False + self.katxt.set("OFF") + self.kabtn.config(fg = self.RED) + else: + self.pluginManager.plugins[self.findClass("BadKillAura")] = True + self.katxt.set("ON") + self.kabtn.config(fg = self.GREEN) + + """ + # when user wishes to restart + def restartHandler(self): + # break out of Listen thread + self.proxy.Restart() + """ + + # returns relevant class you searched for + def findClass(self, text: str): + for plugin in self.pluginManager.plugins: + if type(plugin).__name__ == text: + return plugin + + + def start(self): + while True: + #self.root.update_idletasks() + try: + self.root.update() + self.location.set("Connected to {}!".format(self.client.currentMap)) + except KeyboardInterrupt: + return + except TclError: + print("Closed GUI. Shutting down proxy.") + return + time.sleep(0.005) + + + + + \ No newline at end of file diff --git a/proxy.py b/proxy.py new file mode 100644 index 0000000..61874b9 --- /dev/null +++ b/proxy.py @@ -0,0 +1,79 @@ +import threading +import sys + +from client import * +from PluginManager import * +from gui import GUI + +class Proxy: + + def __init__(self, pm: PluginManager, client: Client): + + self.localHostAddr = "127.0.0.1" + self.localHostPort = 2050 # look up 843 and flash + self.managerSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.pluginManager = pm + self.client = client + + self.active = False + self.serverMonitorThread = None + + """ + the purpose of this function is to allow client -> proxy then server -> proxy reconnects. + it's in a thread to "monitor" when our client requires a new socket to the proxy + in a sense, it is the server socket. + """ + def ServerMonitor(self): + self.managerSocket.bind((self.localHostAddr, self.localHostPort)) + self.managerSocket.listen(3) + # always listening for client connect + while True: + self.client.gameSocket, addr = self.managerSocket.accept() + + def Start(self): + self.active = True + # start up server socket + self.serverMonitorThread = threading.Thread(target = self.ServerMonitor, daemon = True) + self.serverMonitorThread.start() + self.Connect() + + def Connect(self): + # connect sockets first + self.client.ConnectRemote(self.client.remoteHostAddr, self.client.remoteHostPort) + self.client.connected = True + + # listen for packets + while True: + self.client.Listen() + + """ + def Restart(self): + self.client.Disconnect() + self.client.ResetCipher() + threading.Thread(target = self.Connect, daemon = False).start() + """ + +def main(): + print("[Initializer]: Loading plugins...") + plugins = PluginManager() + if not plugins.initialize(): + print("Shutting down.") + return + + print("[Initializer]: Starting proxy...") + client = Client(plugins) + proxy = Proxy(plugins, client) + + threading.Thread(target = proxy.Start, daemon = True).start() + + print("[Initializer]: Proxy started!") + + print("[Initializer]: Starting GUI...") + gui = GUI(plugins, client, proxy) + print("[Initializer]: GUI started!") + + gui.start() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/valorlib b/valorlib new file mode 160000 index 0000000..7b0cd11 --- /dev/null +++ b/valorlib @@ -0,0 +1 @@ +Subproject commit 7b0cd1121ad436eb8fe3015211afa91ac7293ed6 diff --git a/vrelay v1.png b/vrelay v1.png new file mode 100644 index 0000000..a51f89c Binary files /dev/null and b/vrelay v1.png differ