Created
September 11, 2023 00:54
-
-
Save FelixWolf/4db713fe0829c5fb1290cc865e79ac77 to your computer and use it in GitHub Desktop.
zlib licensed. Provided AS-IS, without warranty.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
import struct | |
import socket | |
import sys | |
import traceback | |
import random | |
import libfurc.client | |
import libfurc.base | |
import io | |
import asyncio | |
import tkinter as tk | |
from tkinter import ttk | |
import time | |
import shlex | |
import os | |
import re | |
import codecs | |
import hashlib | |
import math | |
#TODO: Autopopulate this | |
DIGO_LIST = [ | |
'pillowen', 'korven', 'prrose', 'wallaben', 'flooki', 'mfsphynx', | |
'mfgryffe', 'lucken', 'kitsune', 'kitterjack', 'eco', 'spookity', | |
'kitw1', 'kitw2', 'triwngs', 'buggie', 'ctrollu', 'toaster', | |
'lynxen', 'fluttershen', 'parotu', 'snowmew', 'flitten', | |
'partykiwi', 'kirin', 'eagle', 'unicorn', 'meowlen', | |
'fwolf', 'phoekin', 'furretree', 'hedgen', 'chimera', | |
'fluttermouse', 'purrsian', 'bat', 'fluttercat', 'noblecanine', | |
'woolie', 'snayle', 'flutterbun', 'lichenthrope', 'flutterbugge', | |
'rubbaducky', 'chinchilla', 'doodlen', 'ottifet', 'kirikin', 'husken', | |
'firefox', 'wyrmme', 'icewing', 'dragonseye', 'ponikin', 'yeenen', 'canen', | |
'kitsukin', 'werewolf', 'fowl', 'mfdragon', 'mothkin', 'redkin', 'flynx', | |
'angelfish', 'bflywings', 'quacken', 'poneigh', 'gryfkin', 'swoon', | |
'raukor', 'leprechaun', 'cerdiere', 'naga', 'mfphoenix', 'pookie', | |
'ffel1', 'lovebird', 'nightmare', 'fleek', 'beachball', | |
'snowfurre', 'piggen', 'chitteren', 'adorapus', 'triwingman', | |
'harten', 'ffox', 'cheeten', 'squeeken', 'kiwi', 'seahorse', 'dragon', | |
'purrwing', 'swenguin', 'pacapuff', 'gryffe', 'raffen', 'frab', | |
'batikin', 'cupig', 'noble-lapine', 'lemurkin', 'floof', 'fluff', 'flox', | |
'fcat', 'flutterdog', 'turkey', 'present', 'merribou', 'clucken', 'fpan', | |
'nobleequine', 'hollydrake', 'raccoon', 'ufc', 'corgen', 'purrkin', | |
'penguin', 'gargoyle', 'fireclaw', 'chupacabra', 'rocketsnayle', | |
'kitterkin', 'snowwuff', 'noblehyooman', 'stoaten', 'manedwuff', | |
'easterbunny', 'jackalope', 'flutterphin', 'primewngs', | |
'drgnwngs', 'phoenix', 'mfunicorn', 'froggen', 'noble-squirrel', | |
'cabbit', 'fleer', 'troll', 'noble-musteline', 'toytle', 'santapaws', | |
'orchimodo', 'ffel2', 'vulpine', 'spiritwolf', 'goaten', 'snapdragon', | |
'koalen', 'ruffen', 'skunken', 'noblefeline', 'freebee', | |
'bruinen', 'jerbokin', 'lovepuppy', 'reindeer', 'tusker', 'mechakiwi', | |
'drakkin', 'batwings', 'noblerodent', 'snowyip', 'moonpuff', 'clsscwngs' | |
] | |
BBuddyJokes = [] | |
try: | |
with open("jokes.txt", "r") as f: | |
BBuddyJokes = list(filter(None,f.read().split("\n"))) | |
except FileNotFoundError as e: | |
pass | |
Fortunes = [] | |
try: | |
with open("/home/felix/Documents/Furcadia/Logs/fortunes_unique.txt", "r") as f: | |
Fortunes = list(filter(None,f.read().split("\n"))) | |
except FileNotFoundError as e: | |
pass | |
class Random: | |
def __init__(self, seed = None): | |
self.magic = 0x9908B0DF | |
self.period = 37 | |
self.statelen = 63 | |
self.state = [0] * (self.statelen + 1) | |
self.next = 1 | |
self.left = -1 | |
if seed: | |
self.seed(seed) | |
def seed(self, seed): | |
x = (1 | seed) & 0xffffffff | |
self.left = 0 | |
self.state[0] = x | |
for i in range(62): | |
x = 69069 * x & 4294967295 | |
self.state[i + 1] = x | |
def reload(self): | |
if self.left < -1: | |
self.seed(4357) | |
self.left = 61 | |
self.next = 1 | |
e = self.state[0] | |
h = self.state[1] | |
n = 26 | |
s = self.period | |
t = 0 | |
v = 2 | |
for n in reversed(range(1,27)): | |
i = (0x80000000 & e | 0x7fffffff & h) >> 1 | |
n = 0 | |
if 1 == (1 & h): | |
n = self.magic | |
self.state[t] = self.state[s] ^ i ^ n | |
t += 1 | |
s += 1 | |
e = h | |
h = self.state[v] | |
v += 1 | |
s = 0 | |
for n in reversed(range(1,self.period)): | |
i = (0x80000000 & e | 0x7fffffff & h) >> 1 | |
n = 0 | |
if 1 == (1 & h): | |
n = self.magic | |
self.state[t] = self.state[s] ^ i ^ n | |
t += 1 | |
s += 1 | |
e = h | |
h = self.state[v] | |
v += 1 | |
h = self.state[0] | |
o = (0x80000000 & e | 0x7fffffff & h) >> 1 | |
r = 0 | |
if 1 == (1 & h): | |
r = self.magic | |
self.state[t] = self.state[s] ^ o ^ r | |
h ^= h >> 11 | |
h ^= h << 7 & 0x9D2C5680 | |
h ^= h << 15 & 0xEFC60000 | |
return (h ^ h >> 18) >> 0 | |
def random(self): | |
self.left = self.left - 1 | |
if self.left < 0: | |
return self.reload() | |
y = self.state[self.next] | |
self.next += 1 | |
y ^= y >> 11 | |
y ^= y << 7 & 0x9D2C5680 | |
y ^= y << 15 & 0xEFC60000 | |
return (y ^ y >> 18) >> 0 | |
async def printtb(fakeClientWriter): | |
tb = traceback.format_exc().strip("\n") | |
try: | |
await sendMessage(fakeClientWriter, ["<font color=\"error\">{}</font>".format(i) for i in tb.split("\n")]) | |
except Exception as ee: | |
print(ee) | |
pass | |
def sortTreeView(tv, col, reverse=False): | |
l = [(tv.set(k, col), k) for k in tv.get_children('')] | |
l.sort(reverse=reverse, key=lambda x:int(x[1])) | |
# rearrange items in sorted positions | |
for index, (val, k) in enumerate(l): | |
tv.move(k, '', index) | |
class GUIClient(tk.Tk): | |
def __init__(self, client, fakeServerWriter, fakeClientWriter, attributes): | |
self.client = client | |
self.fakeServerWriter = fakeServerWriter | |
self.fakeClientWriter = fakeClientWriter | |
self.attributes = attributes | |
self.activeFurres = [] | |
self.activeVars = [] | |
super().__init__() | |
self.protocol("WM_DELETE_WINDOW", self.close) | |
self.interval = 1/60 | |
self.init() | |
self.update() | |
self.geometry("400x300") | |
self.lastUpdate = 0 | |
def init(self): | |
#Create furre list | |
#self.columnconfigure(0, weight=1) | |
#self.rowconfigure(0, weight=1) | |
notebook = ttk.Notebook(self) | |
#Furre list | |
furreListFrame = tk.Frame(notebook) | |
furreListFrame.grid(row=0, column=0, sticky="NEWS") | |
furreListFrame.columnconfigure(0, weight=1) | |
furreListFrame.rowconfigure(0, weight=1) | |
#furreListFrame.columnconfigure(1, weight=1) | |
furreListScroll = tk.Scrollbar(furreListFrame, orient=tk.VERTICAL) | |
furreListScroll.grid(row=0, column=1, sticky="NES") | |
furreList = ttk.Treeview(furreListFrame, | |
columns=tuple(range(1,5)), show='headings', height=3, | |
yscrollcommand = furreListScroll.set, | |
xscrollcommand = furreListScroll.set | |
) | |
def focusView(a): | |
curItem = furreList.focus() | |
b = furreList.item(curItem)["values"] | |
self.fakeClientWriter.write(b"@"+libfurc.base.b95encode(b[2], 2) + libfurc.base.b95encode(b[3], 2)+b"\n") | |
furreList.bind('<ButtonRelease-1>', focusView) | |
furreListScroll.config(command=furreList.yview) | |
furreList.heading(1, text="ID", anchor=tk.CENTER) | |
furreList.heading(2, text="Name", anchor=tk.CENTER) | |
furreList.heading(3, text="X", anchor=tk.CENTER) | |
furreList.heading(4, text="Y", anchor=tk.CENTER) | |
furreList.grid(row=0, column=0, sticky="NEWS") | |
self.furreList = furreList | |
#Var list | |
varListFrame = tk.Frame(notebook) | |
varListFrame.grid(row=0, column=0, sticky="NEWS") | |
varListFrame.columnconfigure(0, weight=1) | |
varListFrame.rowconfigure(0, weight=1) | |
#varListFrame.columnconfigure(1, weight=1) | |
varListScroll = tk.Scrollbar(varListFrame, orient=tk.VERTICAL) | |
varListScroll.grid(row=0, column=1, sticky="NES") | |
varList = ttk.Treeview(varListFrame, | |
columns=tuple(range(1,3)), show='headings', height=3, | |
yscrollcommand = varListScroll.set, | |
xscrollcommand = varListScroll.set | |
) | |
varListScroll.config(command=varList.yview) | |
varList.heading(1, text="Num", anchor=tk.CENTER) | |
varList.heading(2, text="Val", anchor=tk.CENTER) | |
varList.grid(row=0, column=0, sticky="NEWS") | |
self.varList = varList | |
#Random tool | |
randomToolFrame = tk.Frame(notebook) | |
randomToolFrame.grid(row=0, column=0, sticky="NEWS") | |
randomToolFrame.columnconfigure(0, weight=1) | |
randomToolFrame.rowconfigure(0) | |
self.rtspinner = tk.Spinbox(randomToolFrame, from_=0, to=65535) | |
self.rtspinner.grid(row=0, column=0, sticky="NEW") | |
self.rtoffset = tk.Spinbox(randomToolFrame, from_=0, to=65535) | |
self.rtoffset.grid(row=0, column=1, sticky="NEW") | |
self.rtvalue = tk.StringVar(randomToolFrame) | |
self.rtvalue.set("Prediction: {}".format(1)) | |
tk.Label(randomToolFrame, textvariable = self.rtvalue).grid(row=1, column=0, sticky="NEW") | |
#varListFrame.columnconfigure(1, weight=1) | |
#Finalize | |
notebook.add(furreListFrame, text='Furres') | |
notebook.add(varListFrame, text='Variables') | |
notebook.add(randomToolFrame, text='Random Tool') | |
notebook.pack(expand = 1, fill ="both") | |
self.loop = asyncio.create_task(self._update()) | |
async def poll(self, t): | |
if "furreTracker" in self.attributes and self.attributes["furreTracker"].dsAddon != None: | |
rng = Random(self.attributes["furreTracker"].dsAddon["randSeed"]) | |
for i in range(0, int(self.rtoffset.get())): | |
pass | |
v = int(self.rtspinner.get()) | |
if v == 0: | |
self.rtvalue.set("Prediction: {}".format(rng.random())) | |
else: | |
self.rtvalue.set("Prediction: {}".format(rng.random() % v)) | |
if "furreList" in self.attributes: | |
for fuid in self.activeFurres: | |
if fuid not in self.attributes["furreList"]: | |
self.furreList.delete(fuid) | |
self.activeFurres.remove(fuid) | |
for fuid in self.attributes["furreList"]: | |
furre = self.attributes["furreList"][fuid] | |
if fuid not in self.activeFurres: | |
self.activeFurres.append(fuid) | |
self.furreList.insert(parent='', index=0, iid=fuid, values=(fuid, furre["name"].decode().replace("|", " "), furre["x"], furre["y"])) | |
else: | |
self.furreList.item(fuid, values=(fuid, furre["name"].decode().replace("|", " "), furre["x"], furre["y"])) | |
sortTreeView(self.furreList, 1) | |
if "varList" in self.attributes: | |
for fuid in self.activeVars: | |
if fuid not in self.attributes["varList"]: | |
self.varList.delete(fuid) | |
self.activeVars.remove(fuid) | |
for fuid in self.attributes["varList"]: | |
furre = self.attributes["varList"][fuid] | |
if fuid not in self.activeVars: | |
self.activeVars.append(fuid) | |
self.varList.insert(parent='', index=0, iid=fuid, values=(fuid, str(furre))) | |
else: | |
self.varList.item(fuid, values=(fuid, str(furre))) | |
sortTreeView(self.varList, 1) | |
async def _update(self): | |
while self.interval: | |
t = time.time() | |
if t >= self.lastUpdate + 0.1: | |
try: | |
await self.poll(t) | |
except Exception as e: | |
print(traceback.format_exc()) | |
self.lastUpdate = t | |
self.update() | |
await asyncio.sleep(self.interval) | |
def close(self): | |
self.attributes["gui"] = None | |
self.interval = None | |
self.destroy() | |
recordables = { | |
b"m 1": "SW", | |
b"m 2": "SW", | |
b"m 3": "SE", | |
b"m 4": "NW", | |
b"m 5": "INVALID", | |
b"m 6": "SE", | |
b"m 7": "NW", | |
b"m 8": "NE", | |
b"m 9": "NE", | |
b"<": "Left", | |
b">": "Right", | |
b"sit": "Sit", | |
b"lie": "Lie", | |
b"stand": "Stand", | |
b"get": "Get", | |
b"use": "Use", | |
b"magic": "Magic", | |
b"gloam -3": "Gloam(-3)", | |
b"gloam -2": "Gloam(-2)", | |
b"gloam -1": "Gloam(-1)", | |
b"gloam 0": "Gloam(0)", | |
b"gloam 1": "Gloam(+1)", | |
b"gloam 2": "Gloam(+2)", | |
b"gloam 3": "Gloam(+3)", | |
} | |
ctable = [ | |
[40, 41, 35, 0, 1, 2, 8, 43, 5, 3, 6, 7, 42, 10, 11, 14, 12, 13, 15, 28, 18, 17, 16, 44, 19, 22, 21, 20, 23, 34, 29, 24, 39, 38, 37, 25, 36, 27, 26, 33, 30, 31, 32, 4, 9], | |
[0, 23, 2, 5, 20, 8, 10, 17, 4, 26, 12, 29, 30, 31, 33, 28, 32, 14, 27, 34, 35, 15, 37, 25, 38, 18, 39, 40, 24, 36, 41, 42, 43, 44, 19, 22, 1, 21, 13, 6, 7, 16, 11, 9, 3], | |
[31, 27, 30, 17, 1, 20, 21, 0, 16, 8, 24, 32, 33, 41, 35, 36, 3, 25, 2, 4, 26, 38, 12, 28, 13, 6, 5, 9, 14, 10, 11, 39, 37, 40, 23, 29, 7, 22, 18, 15, 42, 43, 44, 19, 34], | |
[42, 43, 41, 0, 1, 2, 44, 40, 6, 5, 3, 7, 8, 11, 10, 14, 12, 13, 18, 28, 15, 16, 17, 19, 23, 38, 34, 22, 21, 20, 29, 24, 39, 26, 25, 35, 36, 37, 27, 33, 32, 31, 30, 4, 9], | |
[23, 25, 24, 18, 0, 31, 26, 22, 6, 2, 32, 34, 3, 35, 36, 4, 21, 17, 5, 39, 40, 8, 7, 41, 10, 29, 42, 33, 43, 28, 37, 44, 14, 9, 38, 20, 30, 27, 13, 19, 12, 11, 15, 16, 1] | |
] | |
async def handleClientMessage(client, data, fakeServerWriter, fakeClientWriter, attributes): | |
print("CLIENT:",data) | |
if attributes.get("recordInputs", False) and (data in recordables or \ | |
data.startswith((b":", b'"'))): | |
attributes["recording"].append(data) | |
if data.startswith(b'wh') or data.startswith(b'"'): | |
return | |
try: | |
com = shlex.split(data.decode()) | |
except Exception as e: | |
try: | |
com = data.decode().split(" ",1) | |
except Exception as e: | |
print(e) | |
return | |
if len(com) == 0: | |
return | |
if com[0] == "script" and len(com) > 1: | |
if com[1] == "stop": | |
exit() | |
elif com[0] == "gui": | |
if "gui" in attributes and attributes["gui"]: | |
attributes["gui"].focus() | |
else: | |
attributes["gui"] = GUIClient(client, fakeServerWriter, fakeClientWriter, attributes) | |
elif com[0] == "pos": | |
await sendMessage(fakeClientWriter, "* {}, {}".format(attributes["pos"][0] * 2, attributes["pos"][1])) | |
elif com[0] == "faker": | |
fakeClientWriter.write(data[6:]+b"\n") | |
elif com[0] == "sendesc": | |
a = codecs.escape_decode(data[8:]) | |
print(a[0]) | |
fakeClientWriter.write(a[0]+b"\n") | |
elif com[0] == "particles" and len(com) > 1: | |
if com[1] == "watch" and len(com) > 2: | |
if "particlewatch" in attributes: | |
pass | |
else: | |
attributes["particlewatch"] = [com[2], 0] | |
async def spinner(): | |
if not os.path.isfile(attributes["particlewatch"][0]): | |
return | |
mtime = os.stat(attributes["particlewatch"][0]).st_mtime | |
if mtime <= attributes["particlewatch"][1]: | |
return | |
attributes["particlewatch"][1] = mtime | |
try: | |
with open(attributes["particlewatch"][0], "r") as f: | |
particle = libfurc.particles.Particles.loadsTxt(f.read()) | |
fakeClientWriter.write( | |
b"]I" | |
+ libfurc.base.b220encode(attributes["pos"][0], 2) | |
+ libfurc.base.b220encode(attributes["pos"][1], 2) | |
+ libfurc.base.b220encode(0, 2) | |
+ libfurc.base.b220encode(0, 2) | |
+ particle.dumpsMessage() | |
+ b"\n" | |
) | |
except Exception as e: | |
await printtb(fakeClientWriter) | |
attributes["particlewatch"].append(Timer(0.25, spinner)) | |
await sendMessage(fakeClientWriter, "Watching particles " + attributes["particlewatch"][0]) | |
elif com[1] == "stop": | |
if "particlewatch" not in attributes: | |
pass | |
else: | |
attributes["particlewatch"][2].cancel() | |
await sendMessage(fakeClientWriter, "Stopped watching particles " + attributes["particlewatch"][0]) | |
attributes.pop("particlewatch") | |
elif com[1] == "play" and len(com) > 1: | |
try: | |
with open(com[2], "r") as f: | |
particle = libfurc.particles.Particles.loadsTxt(f.read()) | |
fakeClientWriter.write( | |
b"]I" | |
+ libfurc.base.b220encode(attributes["pos"][0], 2) | |
+ libfurc.base.b220encode(attributes["pos"][1], 2) | |
+ libfurc.base.b220encode(0, 2) | |
+ libfurc.base.b220encode(0, 2) | |
+ particle.dumpsMessage() | |
+ b"\n" | |
) | |
except Exception as e: | |
await printtb(fakeClientWriter) | |
elif com[1] == "playcache" and len(com) > 1: | |
try: | |
with open("/home/felix/Documents/Furcadia/script/particles/"+com[2]+".vxn", "rb") as f: | |
particle = libfurc.particles.Particles.loadsVXN(f.read()) | |
fakeClientWriter.write( | |
b"]I" | |
+ libfurc.base.b220encode(attributes["pos"][0], 2) | |
+ libfurc.base.b220encode(attributes["pos"][1], 2) | |
+ libfurc.base.b220encode(0, 2) | |
+ libfurc.base.b220encode(0, 2) | |
+ particle.dumpsMessage() | |
+ b"\n" | |
) | |
except Exception as e: | |
await printtb(fakeClientWriter) | |
elif com[0] == "butler": | |
await sendMessage(fakeClientWriter, "* Holding: {}; Standing: {}".format(*attributes["butler"])) | |
elif com[0] == "bbuddyjoke": | |
await client.send(b'"'+random.choice(BBuddyJokes).encode()) | |
elif com[0] == "spinnage" and len(com) > 1: | |
if com[1] == "start": | |
if "spinnage" in attributes: | |
pass | |
else: | |
async def spinner(): | |
await client.send(b">") | |
attributes["spinnage"] = Timer(1.0, spinner) | |
elif com[1] == "stop": | |
if "spinnage" not in attributes: | |
pass | |
else: | |
attributes["spinnage"].cancel() | |
attributes.pop("spinnage") | |
elif com[0] == "digocycle" and len(com) > 1: | |
if com[1] == "start": | |
if "digocycle" in attributes: | |
pass | |
else: | |
async def spinner(): | |
#await client.send(random.choice(DIGO_LIST).encode()) | |
await client.send(b"nextdigo") | |
speed = 1.0 | |
if len(com) > 2: | |
speed = float(com[2]) | |
attributes["digocycle"] = Timer(speed, spinner) | |
elif com[1] == "stop": | |
if "digocycle" not in attributes: | |
pass | |
else: | |
attributes["digocycle"].cancel() | |
attributes.pop("digocycle") | |
elif com[0] == b"antiidle" and len(com) > 1: | |
if com[1] == "start": | |
if "antiidle" in attributes: | |
pass | |
else: | |
async def spinner(): | |
await client.send(b">") | |
await client.send(b"<") | |
await client.send(b"unafk") | |
attributes["antiidle"] = Timer(60.0, spinner) | |
elif com[1] == "stop": | |
if "antiidle" not in attributes: | |
pass | |
else: | |
attributes["antiidle"].cancel() | |
attributes.pop("antiidle") | |
elif com[0] == "gloamr" and len(com) > 1: | |
if com[1] == "start": | |
if "gloamr" in attributes: | |
pass | |
else: | |
async def spinner(): | |
i = 0 | |
cycle = [0,1,2,3,2,1,0,-1,-2,-3,-2,-1] | |
while True: | |
await client.send("gloam {}".format(cycle[i]).encode()) | |
i += 1 | |
if i >= len(cycle): | |
i = 0 | |
await asyncio.sleep(1.0) | |
attributes["gloamr"] = Timer(1.0, spinner) | |
elif com[1] == "stop": | |
if "gloamr" not in attributes: | |
pass | |
else: | |
attributes["gloamr"].cancel() | |
attributes.pop("gloamr") | |
elif com[0] == "colorr" and len(com) > 1: | |
if com[1] == "start": | |
if "colorr" in attributes: | |
pass | |
else: | |
async def spinner(): | |
print("STARTING WITH MODE", attributes["colorrmode"]) | |
def colFunc(cmin, cmax, table = None): | |
if attributes["colorrmode"] == 0: | |
return random.randint(cmin, cmax) | |
elif attributes["colorrmode"] == 1: | |
t = abs(math.sin(time.time() / 22.5)) | |
c = math.floor(t * len(ctable[table])) | |
if ctable[table][c] <= cmax-cmin: | |
return cmin+ctable[table][c] | |
i = 1 | |
while True: | |
if c - i > 0: | |
if cmin + ctable[table][c - i] > cmin: | |
return cmin + ctable[table][c - i] | |
if c + i < len(ctable[table]): | |
if cmin + ctable[table][c + i] <= cmax: | |
return cmin + ctable[table][c + i] | |
if c - i <= 0 and c + i >= len(ctable[table]): | |
return cmin | |
i += 1 | |
while True: | |
# `chcol w%%=/I7;;:?#:$# | |
# w############$# | |
# w;;O@J@@@@@;@$# | |
choice = bytes([ | |
119, # w | |
colFunc(35, 59, 1), # ; | |
colFunc(35, 59, 1), # ; | |
colFunc(35, 79, 0), # O | |
colFunc(35, 64, 3), # @ | |
colFunc(35, 74, 2), # J | |
colFunc(35, 64, 4), # @ | |
colFunc(35, 64, 4), # @ | |
colFunc(35, 64, 4), # @ | |
colFunc(35, 64, 4), # @ | |
colFunc(35, 64, 4), # @ | |
colFunc(35, 59, 1), # ; | |
colFunc(35, 64, 4), # @ | |
36, # $ | |
39, # | |
35, # # | |
]) | |
print("SEND", choice) | |
await client.send(b"chcol "+choice) | |
await asyncio.sleep(1) | |
attributes["colorrmode"] = 0 if len(com) < 3 else int(com[2]) | |
attributes["colorr"] = Timer(1.0, spinner) | |
elif com[1] == "stop": | |
if "colorr" not in attributes: | |
pass | |
else: | |
attributes["colorr"].cancel() | |
attributes.pop("colorr") | |
elif com[0] == "peek" and len(com) > 2: | |
x, y = int(com[1]), int(com[2]) | |
fakeClientWriter.write(b"@"+libfurc.base.b95encode(x, 2) + libfurc.base.b95encode(y, 2)+b"\n") | |
elif com[0] == "aafk": | |
await client.send(b"afk") | |
elif com[0] == "autosummon": | |
attributes["autosummon"] = not attributes.get("autosummon", True) | |
await sendMessage(fakeClientWriter, "* Autosummon is {}".format("on" if attributes["autosummon"] else "off")) | |
elif com[0] == "show" and len(com) > 1: | |
x = int(com[1]) | |
fakeClientWriter.write(b"%"+libfurc.base.b95encode(x, 2) + b"\n") | |
elif com[0] == "gloamtest" and len(com) > 4: | |
fakeClientWriter.write(b"]O" | |
+libfurc.base.b220encode(attributes["furreTracker"].selfID, 4) | |
+"{:0>2X}{:0>2X}{:0>2X}".format(int(com[3]),int(com[2]),int(com[1])).encode() | |
+libfurc.base.b220encode(int(com[4]), 2) | |
+b"\n") | |
elif com[0] == "mark" and len(com) > 3: | |
tile = int(com[1]) | |
buffer = b"1" | |
x = 0 | |
for i in range(0, len(com)-2, 2): | |
buffer += libfurc.base.b220encode(int(com[2+i]) // 2, 2) \ | |
+ libfurc.base.b220encode(int(com[3+i]), 2) \ | |
+ libfurc.base.b220encode(tile, 2) | |
x += 1 | |
fakeClientWriter.write(buffer + b"\n") | |
await sendMessage(fakeClientWriter, "* Marked {} locations".format(x)) | |
elif com[0] == "f" and len(com) > 2: | |
what, value = int(com[1]), int(com[2]) | |
result = ", ".join(["({},{})".format(*i) for i in attributes["objTracker"].find(what, value)]) | |
await sendMessage(fakeClientWriter, "{} is at: {}".format(value, result)) | |
elif com[0] == "record" and len(com) > 1: | |
if com[1] == "start": | |
await sendMessage(fakeClientWriter, "* Recording inputs.") | |
attributes["recording"] = [] | |
attributes["recordInputs"] = True | |
elif com[1] == "end": | |
attributes["recordInputs"] = False | |
await sendMessage(fakeClientWriter, "* Recording ended.") | |
elif com[1] == "list": | |
result = [] | |
for i in attributes.get("recording", []): | |
if i in recordables: | |
result.append(recordables[i]) | |
else: | |
result.append(i.decode()) | |
await sendMessage(fakeClientWriter, "* Recorded inputs: {}".format(", ".join(result))) | |
elif com[1] == "play": | |
attributes["recordInputs"] = False | |
delay = 1 | |
if len(com) > 2: | |
delay = float(com[2]) | |
if attributes.get("recordplayer", None) != None: | |
attributes["recordplayer"].cancel() | |
async def player(): | |
for step in attributes["recording"]: | |
await client.send(step) | |
await asyncio.sleep(delay) | |
await sendMessage(fakeClientWriter, "* Playback finished.") | |
return False | |
attributes["recordplayer"] = Timer(1, player) | |
await sendMessage(fakeClientWriter, "* Playing...") | |
elif com[1] == "loop": | |
attributes["recordInputs"] = False | |
delay = 1 | |
if len(com) > 2: | |
delay = float(com[2]) | |
if attributes.get("recordplayer", None) != None: | |
attributes["recordplayer"].cancel() | |
async def player(): | |
if len(attributes["recording"]) == 0: | |
return | |
while True: | |
for step in attributes["recording"]: | |
await client.send(step) | |
await asyncio.sleep(delay) | |
attributes["recordplayer"] = Timer(1, player) | |
await sendMessage(fakeClientWriter, "* Playing...") | |
elif com[1] == "stop": | |
if attributes.get("recordplayer", None) != None: | |
attributes["recordplayer"].cancel() | |
await sendMessage(fakeClientWriter, "* Stopped.") | |
elif com[1] == "clear": | |
attributes["recording"] = [] | |
await sendMessage(fakeClientWriter, "* Cleared...") | |
elif com[1] == "save" and len(com) > 2: | |
with open(com[2], "wb") as f: | |
f.write(b"\n".join(attributes["recording"])) | |
await sendMessage(fakeClientWriter, "* Saved...") | |
elif com[1] == "load" and len(com) > 2: | |
with open(com[2], "rb") as f: | |
attributes["recording"] = f.read().split(b"\n") | |
await sendMessage(fakeClientWriter, "* Loaded...") | |
class FurreTracker: | |
def __init__(self, client): | |
self.client = client | |
self.furres = {} | |
self.DSTarget = None | |
self.dsAddon = None | |
self.selfID = 0 | |
self.client.hook("Dream", self.Dream) | |
self.client.hook("FurreArrive", self.FurreArrive) | |
self.client.hook("HideAvatar", self.HideAvatar) | |
self.client.hook("MoveAvatar", self.MoveAvatar) | |
self.client.hook("RemoveAvatarID", self.RemoveAvatar) | |
self.client.hook("RemoveAvatar", self.RemoveAvatar) | |
self.client.hook("AnimateAvatar", self.MoveAvatar) | |
self.client.hook("SpawnAvatar", self.SpawnAvatar) | |
self.client.hook("DSEvent", self.DSEvent) | |
self.client.hook("DSEventAddon", self.DSEventAddon) | |
async def Dream(self, *args): | |
for item in list(self.furres.keys()): | |
self.furres.pop(item) | |
async def FurreArrive(self, fuid, xy, direction, shape, *args): | |
if fuid not in self.furres: | |
self.furres[fuid] = { | |
"x": 0, "y": 0, "d": 0, | |
"o": -1, | |
"c": -1, | |
"e": -1, | |
"name": b"unknown" | |
} | |
self.furres[fuid]["x"] = xy[0] | |
self.furres[fuid]["y"] = xy[1] | |
self.furres[fuid]["d"] = direction | |
async def HideAvatar(self, fuid, xy, *args): | |
if fuid not in self.furres: | |
self.furres[fuid] = { | |
"x": 0, "y": 0, "d": 0, | |
"o": -1, | |
"c": -1, | |
"e": -1, | |
"name": b"unknown" | |
} | |
self.furres[fuid]["x"] = xy[0] | |
self.furres[fuid]["y"] = xy[1] | |
async def RemoveAvatar(self, fuid, *args): | |
if fuid in self.furres: | |
self.furres.pop(fuid) | |
async def MoveAvatar(self, fuid, xy, direction, shape, *args): | |
if fuid not in self.furres: | |
self.furres[fuid] = { | |
"x": 0, "y": 0, "d": 0, | |
"o": -1, | |
"c": -1, | |
"e": -1, | |
"name": b"unknown" | |
} | |
self.furres[fuid]["x"] = xy[0] | |
self.furres[fuid]["y"] = xy[1] | |
self.furres[fuid]["d"] = direction | |
async def SpawnAvatar(self, fuid, xy, direction, shape, name, *args): | |
if fuid not in self.furres: | |
self.furres[fuid] = { | |
"x": 0, "y": 0, "d": 0, | |
"o": -1, | |
"c": -1, | |
"e": -1, | |
"name": b"unknown" | |
} | |
self.furres[fuid]["x"] = xy[0] | |
self.furres[fuid]["y"] = xy[1] | |
self.furres[fuid]["d"] = direction | |
self.furres[fuid]["name"] = name | |
async def DSEventAddon(self, a): | |
self.dsAddon = a | |
async def DSEvent(self, selfTrigger, a): | |
if selfTrigger: | |
self.selfID = self.dsAddon["userID"] | |
if self.dsAddon and self.dsAddon["userID"] != 0: | |
fuid = self.dsAddon["userID"] | |
if self.dsAddon["moveFlag"] == 1: | |
xy = a["to"] | |
else: | |
xy = a["from"] | |
if fuid not in self.furres: | |
self.furres[fuid] = { | |
"x": 0, "y": 0, "d": 0, | |
"o": -1, | |
"c": -1, | |
"e": -1, | |
"name": b"unknown" | |
} | |
self.furres[fuid]["d"] = self.dsAddon["facingDir"] | |
self.furres[fuid]["x"] = xy[0] | |
self.furres[fuid]["y"] = xy[1] | |
self.furres[fuid]["o"] = self.dsAddon["objPaws"] | |
self.furres[fuid]["c"] = self.dsAddon["triggererCookies"] | |
self.furres[fuid]["e"] = self.dsAddon["entryCode"] | |
def __del__(self): | |
self.client.off("Dream", self.Dream) | |
self.client.off("FurreArrive", self.FurreArrive) | |
self.client.off("HideAvatar", self.HideAvatar) | |
self.client.off("MoveAvatar", self.MoveAvatar) | |
self.client.off("RemoveAvatarID", self.RemoveAvatar) | |
self.client.off("RemoveAvatar", self.RemoveAvatar) | |
self.client.off("AnimateAvatar", self.MoveAvatar) | |
self.client.off("SpawnAvatar", self.SpawnAvatar) | |
self.client.off("DSEvent", self.DSEvent) | |
self.client.off("DSEventAddon", self.DSEventAddon) | |
class VarTracker: | |
def __init__(self, client): | |
self.client = client | |
self.vars = {} | |
self.stack = [] | |
self.client.hook("Dream", self.Dream) | |
self.client.hook("DSVariableStack", self.DSVariableStack) | |
self.client.hook("SetVariables", self.SetVariables) | |
async def Dream(self, *args): | |
for item in list(self.vars.keys()): | |
self.vars.pop(item) | |
async def DSVariableStack(self, stack): | |
self.stack = stack | |
def popStack(self): | |
if len(self.stack) == 0: | |
return 0 | |
else: | |
return self.stack.pop(0) | |
async def SetVariables(self, a): | |
for i in a: | |
self.vars[i] = a[i] | |
def __del__(self): | |
self.client.off("Dream", self.Dream) | |
self.client.off("DSVariableStack", self.DSVariableStack) | |
self.client.off("SetVariables", self.SetVariables) | |
class ObjTracker: | |
FLOOR = 0 | |
OBJECT = 1 | |
WALL = 2 | |
REGION = 3 | |
EFFECT = 4 | |
SFX = 5 | |
AMBIENT = 6 | |
MAX = 7 | |
def __init__(self, client): | |
self.client = client | |
self.tiles = {} | |
self.client.hook("Dream", self.Dream) | |
self.client.hook("SetRegion", self.SetRegion) | |
self.client.hook("SetEffect", self.SetEffect) | |
self.client.hook("SetWall", self.SetWall) | |
self.client.hook("SetFloor", self.SetFloor) | |
self.client.hook("SetObject", self.SetObject) | |
self.client.hook("SetSFX", self.SetSFX) | |
self.client.hook("SetAmbient", self.SetAmbient) | |
def set(self, where, what, value): | |
if where not in self.tiles: | |
self.tiles[where] = [None]*self.MAX | |
self.tiles[where][what] = value | |
def get(self, where, what): | |
if where not in self.tiles: | |
return None | |
return self.tiles[where][what] | |
def find(self, what, value): | |
result = [] | |
for tile in self.tiles: | |
if self.tiles[tile][what] == value: | |
result.append(tile) | |
return result | |
async def Dream(self, *args): | |
for item in list(self.tiles.keys()): | |
self.tiles.pop(item) | |
async def SetFloor(self, a): | |
for i in range(len(a)): | |
self.set(a[i]["pos"], self.FLOOR, a[i]["id"]) | |
async def SetObject(self, a): | |
for i in range(len(a)): | |
self.set(a[i]["pos"], self.OBJECT, a[i]["id"]) | |
async def SetWall(self, a): | |
for i in range(len(a)): | |
self.set(a[i]["pos"], self.WALL, a[i]["id"]) | |
async def SetRegion(self, a): | |
for i in range(len(a)): | |
self.set(a[i]["pos"], self.REGION, a[i]["id"]) | |
async def SetEffect(self, a): | |
for i in range(len(a)): | |
self.set(a[i]["pos"], self.EFFECT, a[i]["id"]) | |
async def SetSFX(self, a): | |
for i in range(len(a)): | |
self.set(a[i]["pos"], self.SFX, a[i]["id"]) | |
async def SetAmbient(self, a): | |
for i in range(len(a)): | |
self.set(a[i]["pos"], self.AMBIENT, a[i]["id"]) | |
def __del__(self): | |
self.client.off("Dream", self.Dream) | |
self.client.off("SetRegion", self.SetRegion) | |
self.client.off("SetEffect", self.SetEffect) | |
self.client.off("SetWall", self.SetWall) | |
self.client.off("SetFloor", self.SetFloor) | |
self.client.off("SetObject", self.SetObject) | |
self.client.off("SetSFX", self.SetSFX) | |
self.client.off("SetAmbient", self.SetAmbient) | |
rSummonReq = re.compile(r"^<font color='query'><name shortname='([^']+)'>(?:[^<]+)</name> requests permission to join your company\. To accept the request, <a href='command://summon'>click here</a> or type `summon and press <enter>\.</font>$", re.MULTILINE | re.IGNORECASE) | |
rWhichHeimdall = re.compile(r"^<img src='fsh://system\.fsh:86' /> You are connected to Heimdall \[(?P<port>[0-9]+):(?P<server>[0-9+]+)\] \(QTEMP (?P<qterp>[0-9]+)\). There are (?P<players>[0-9]+) players on this Heimdall, of which you are player index (?P<player>[0-9]+) with globalid (?P<globalid>[0-9]+), and you are on map (?P<map>[0-9]+)(?:. Heimdall is running version: (?P<version>[a-f0-9]+))?", re.MULTILINE | re.IGNORECASE) | |
rWhichHorton = re.compile(r"^<img src='fsh://system\.fsh:86' /> You are connected to Horton \[(?P<name>[a-z0-9]+)@(?P<host>[a-z0-9._-]+):(?P<port>[0-9]+)\] \(QTEMP (?P<qtemp>[0-9]+)\). There are (?P<players>[0-9]+) players in this horton, of which you are the player index (?P<player>[0-9]+) with global id (?P<globalid>[0-9]+). (?P<tag>(?:It's a beautiful day in Gosford Park\.)|[^.]+.) (?:Horton is running version: (?P<version>[a-f0-9]+))?", re.MULTILINE | re.IGNORECASE) | |
rWhichTribble = re.compile(r"^<img src='fsh://system\.fsh:86' /> You are connected to tribble \[(?P<port>[0-9]+)\] \(QTEMP (?P<qtemp>[0-9]+)\). There are (?P<players>[0-9]+) players on this tribble, of which you are player index (?P<player>[0-9]+) with global id (?P<globalid>[0-9]+). You are exactly at \((?P<x>[0-9]+),(?P<y>[0-9]+)\). This tribble feels like (?P<map>[a-z0-9-_]+)::(?P<checksum>[0-9]+)(?: and is running version: (?P<version>[a-f0-9]+))?", re.MULTILINE | re.IGNORECASE) | |
rPlayerMessage = re.compile(r"^(<font color='shout'>{S} )?<name shortname='(?P<shortname>[^']+)'>(?P<realname>[^<]+)</name>: (?P<message>.*)(</font>)?$", re.IGNORECASE) | |
rPlayerWhisper = re.compile(r"^<font color='whisper'>\[ <name shortname='(?P<shortname>[^']+)' src='whisper-from'>(?P<realname>[^<]+)</name> whispers, \"(?P<message>.*)\" to you. ]</font>$", re.MULTILINE | re.IGNORECASE) | |
MSG_BLOCK = ( | |
b"(<font color='success'>With force of will you suppress your Gloaming, keeping it at bay, for now.</font>", | |
b"(<font color='success'>The Gloaming rises up from within, affecting your entire being. To return to normal, <a href='command://gloam 0'>click here</a> or type `gloam 0 and press enter.</font>", | |
b"]gupdate.exe", | |
b"]hupdate.exe", | |
) | |
async def attachHooks(client, toServer, toClient, attributes): | |
client.recordInputs = False | |
client.recording = [] | |
await sendMessage(toClient, "* MITM agent attached") | |
attributes["furreTracker"] = FurreTracker(client) | |
attributes["furreList"] = attributes["furreTracker"].furres | |
attributes["varTracker"] = VarTracker(client) | |
attributes["varList"] = attributes["varTracker"].vars | |
attributes["objTracker"] = ObjTracker(client) | |
attributes["pos"] = (0,0) | |
attributes["butler"] = [0,0] | |
attributes["dream"] = { | |
"default": True, | |
"name": "unknown", | |
"checksum": 0, | |
"modern": False | |
} | |
@client.on("*") | |
async def test(name, *args): | |
if name == "Raw": | |
return | |
if name == "SetVariables": | |
return | |
if name == "RegionFlags": | |
return | |
print(name, *args) | |
@client.on("Raw") | |
async def raw(data): | |
if data in MSG_BLOCK: | |
return | |
toClient.write(data) | |
toClient.write(b"\n") | |
await toClient.drain() | |
@client.on("PrefixLine") | |
async def Prefix(data): | |
with open("/home/felix/Documents/Furcadia/script/chatlog.txt", "ab+") as f: | |
f.write(data) | |
@client.on("ButlerPaws") | |
async def ButlerPaws(data): | |
attributes["butler"][0] = data | |
@client.on("ButlerFeet") | |
async def ButlerFeet(data): | |
attributes["butler"][1] = data | |
@client.on("MoveCamera") | |
async def MoveCamera(data): | |
attributes["pos"] = data["to"] | |
@client.on("Dream") | |
async def Dream(default, name, checksum, modern): | |
attributes["dream"]["default"] = default | |
attributes["dream"]["name"] = name.decode() | |
attributes["dream"]["checksum"] = int(checksum.decode()) | |
attributes["dream"]["modern"] = modern | |
@client.on("Particles") | |
async def Particles(pos, offset, data): | |
p = data.dumpsVXN() | |
h = hashlib.md5(p).hexdigest() | |
pp = "/home/felix/Documents/Furcadia/script/particles/{}.vxn".format(h) | |
if not os.path.isfile(pp): | |
with open(pp, "wb+") as f: | |
f.write(p) | |
with open("/home/felix/Documents/Furcadia/script/particlelog.txt", "a+") as f: | |
f.write("{}: pos= {},{}; offset={},{}\n".format(h, *pos, *offset)) | |
@client.on("Message") | |
async def Message(data): | |
#TODO: add extended data to which | |
with open("/home/felix/Documents/Furcadia/script/chatlog.txt", "ab+") as f: | |
f.write(data + b"\n") | |
try: | |
if attributes.get("autosummon", True): | |
test = rSummonReq.match(data.decode("latin-1")) | |
if test: | |
await client.send(("summon "+test.group(1)).encode()) | |
except Exception as e: | |
await printtb(toClient) | |
#Player commands | |
try: | |
whisper = False | |
test = rPlayerMessage.match(data.decode("latin-1")) | |
if not test: | |
test = rPlayerWhisper.match(data.decode("latin-1")) | |
whisper = True | |
if test: | |
msg = test.group("message") | |
who = test.group("shortname") | |
who2 = test.group("realname") | |
if whisper: | |
if msg == ".where": | |
fuid = None | |
for entry in attributes["furreList"]: | |
if attributes["furreList"][entry]["name"].decode("latin-1") == who2: | |
fuid = entry | |
break | |
if fuid: | |
furre = attributes["furreList"][entry] | |
await client.send(("wh "+who+" You are player {} at ({},{}) facing {}. You entered with entry code {}, have {} cookies, and are holding item {}.".format( | |
fuid, | |
furre["x"], | |
furre["y"], | |
["SW", "SE", "NW", "NE"][furre["d"]] if 0 <= furre["d"] <= 3 else "INVALID DIRECTION", | |
"UNKNOWN" if furre["e"] == -1 else furre["e"], | |
"UNKNOWN" if furre["c"] == -1 else furre["c"], | |
"UNKNOWN" if furre["o"] == -1 else furre["o"], | |
)).encode()) | |
else: | |
await client.send(("wh "+who+" I don't have information on you yet, please wait for it to populate!").encode()) | |
elif msg == ".fortune": | |
await client.send(("wh "+who+" {}".format(random.choice(Fortunes))).encode()) | |
else: | |
if msg == ".cookie": | |
await client.send(("\"make-cookie "+who).encode()) | |
except Exception as e: | |
await printtb(toClient) | |
if False: #TODO: detect if I can see this, if not, print it | |
try: | |
test = rWhichHeimdall.match(data.decode()) | |
if test: | |
await sendMessage(toClient, "<img src='fsh://system.fsh:86' /> You are connected to tribble [{}] (QTEMP ?????). There are {} players on this tribble, of which you are player index ? with global id {}. You are exactly at ({},{}). This tribble feels like {}::{}".format( | |
test.group("map"), | |
len(attributes["furreList"]), | |
test.group("globalid"), | |
attributes["pos"][0], attributes["pos"][1], | |
attributes["dream"]["name"], attributes["dream"]["checksum"] | |
)) | |
except Exception as e: | |
await printtb(toClient) | |
# @client.on("DSVariableStack") | |
# async def DSEvent(stack): | |
# return | |
# mapping = { | |
# 2644: "one purple grimoire", | |
# 2650: "one green grimoire", | |
# 2638: "one yellow key", | |
# 2645: "blue goblet two", | |
# 2651: "red goblet two", | |
# 2639: "purple lamp two", | |
# 2646: "ball three orange", | |
# 2652: "ball three yellow", | |
# 2640: "grimoire three blue", | |
# 2647: "four green kiwi", | |
# 2653: "four purple kiwi", | |
# 2641: "four orange goblet", | |
# 2648: "red key five", | |
# 2654: "blue key five", | |
# 2642: "green ball five", | |
# 2649: "lamp six yellow", | |
# 2655: "lamp six orange", | |
# 2643: "kiwi six red", | |
# } | |
# movements = { | |
# "one": [7,7,9,9,9,7,1,1,1,3,3], | |
# "ball": [7,7,9,9,9,7,1,1,1,3,3], | |
# "red": [7,7,9,9,9,7,1,1,1,3,3], | |
# | |
# "yellow": [9,9,9,1,1], | |
# "kiwi": [9,9,9,1,1], | |
# "two": [9,9,9,1,1], | |
# | |
# "three": [3,3,9,9,9,1,1,7,7], | |
# "key": [3,3,9,9,9,1,1,7,7], | |
# "purple": [3,3,9,9,9,1,1,7,7], | |
# | |
# "four": [9,7,7,7,3,3,1], | |
# "lamp": [9,7,7,7,3,3,1], | |
# "blue": [9,7,7,7,3,3,1], | |
# | |
# "orange": [7,7,7,3,3], | |
# "grimoire": [7,7,7,3,3], | |
# "five": [7,7,7,3,3], | |
# | |
# "green": [7,9,9,9,1,1,3], | |
# "goblet": [7,9,9,9,1,1,3], | |
# "six": [7,9,9,9,1,1,3], | |
# } | |
# for key in mapping: | |
# if key in stack: | |
# await sendMessage(toClient, "Probably: {}".format(mapping.get(key, "idklmao"))) | |
# for match in mapping[key].split(" "): | |
# await sendMessage(toClient, "Match: {}".format(str(movements[match]))) | |
# for step in movements[match]: | |
# await client.send(("m {}".format(step)).encode()) | |
# await asyncio.sleep(0.1) | |
#=============================================================================== | |
async def sendMessage(client, msg): | |
if type(msg) == str: | |
msg = msg.split("\n") | |
for line in msg: | |
client.write(b"("+line.replace("\n","").encode()+b"\n") | |
await client.drain() | |
class Timer: | |
def __init__(self, timeout, callback, repeats = 0): | |
self._timeout = timeout | |
self._callback = callback | |
self._repeats = repeats | |
self._task = asyncio.ensure_future(self._job()) | |
async def _job(self): | |
while self._repeats > 1 or self._repeats == 0: | |
await asyncio.sleep(self._timeout) | |
res = await self._callback() | |
if res == False: | |
break | |
if self._repeats == 1: | |
break | |
if self._repeats > 1: | |
self._repeats = self._repeats - 1 | |
def cancel(self): | |
self._task.cancel() | |
def __del__(self): | |
self.cancel() | |
class Client(libfurc.client.PacketHooks, libfurc.client.Commands): | |
def __init__(self, server = None): | |
super().__init__() | |
self.reader = None | |
self.writer = None | |
async def attach(self, reader, writer, fakeClientWriter): | |
self.reader = reader | |
self.writer = writer | |
self.client = fakeClientWriter | |
async def disconnect(self): | |
writer.close() | |
await writer.wait_closed() | |
self.reader = None | |
self.writer = None | |
async def send(self, data): | |
if self.connected: | |
self.writer.write(data) | |
await self.writer.drain() | |
return True | |
return False | |
def command(self, data): | |
if type(data) == str: | |
data = data.encode() | |
return self.send(data + b"\n") | |
@property | |
def connected(self): | |
if self.reader == None or self.writer == None: | |
return False | |
return True | |
#Actual read loop, it is designed to be it's own task | |
async def run(self): | |
while self.connected: | |
data = await self.reader.readline() | |
if not data: #None = Disconnected | |
break | |
if data[-1] != 10: #No EOL means incomplete stream + disconnected | |
break | |
data = data[:-1] #Strip EOL | |
if len(data) == 0: #If empty, ignore | |
continue | |
try: | |
await self.handlePacket(data) | |
except Exception as e: | |
tb = traceback.format_exc().strip("\n") | |
try: | |
await sendMessage(self.client, ["<font color=\"error\">{}</font>".format(i) for i in tb.split("\n")]) | |
except Exception as ee: | |
print(ee) | |
pass | |
#We are out of the loop! Presume Disconnected! | |
self.reader = None | |
self.writer = None | |
class FakeIO: | |
def __init__(self): | |
self.buffer = b"" | |
self.readable = True | |
async def write(self, data): | |
if self.buffer == None: | |
return None | |
self.buffer += data | |
async def read(self, l = None): | |
if self.buffer == None: | |
return None | |
while not self.readable: | |
if self.buffer == None: | |
return None | |
await asyncio.sleep(0.01) | |
self.readable = False | |
if l == None: | |
l = len(self.buffer) | |
result = b"" | |
while l < len(result): | |
if self.buffer == None: | |
return None | |
if len(self.buffer) > 0: | |
needed = l - len(result) | |
result += self.buffer[:needed] | |
self.buffer = self.buffer[needed:] | |
await asyncio.sleep(0.01) | |
self.readable = True | |
return result | |
async def readline(self): | |
if self.buffer == None: | |
return None | |
while not self.readable: | |
if self.buffer == None: | |
return None | |
await asyncio.sleep(0.01) | |
self.readable = False | |
result = b"" | |
while True: | |
if self.buffer == None: | |
return None | |
if len(self.buffer) > 0: | |
for i in range(len(self.buffer)): | |
if self.buffer[i] == 10: | |
break | |
i += 1 | |
result += self.buffer[:i] | |
self.buffer = self.buffer[i:] | |
if result[-1] == 10: | |
break | |
await asyncio.sleep(0.01) | |
self.readable = True | |
return result | |
def close(self): | |
self.buffer = None | |
#=============================================================================== | |
#mitm code | |
#=============================================================================== | |
def mitm_header_read(data): | |
opcode = libfurc.base.b95decode(data[0:2]) | |
dlen = libfurc.base.b95decode(data[2:5]) | |
return opcode, dlen | |
def mitm_header_write(opcode, dlen): | |
return libfurc.base.b95encode(opcode, 2) + libfurc.base.b95encode(dlen, 3) | |
class WriterProxy: | |
def __init__(self, dest, direction): | |
self.real = dest | |
self.direction = direction | |
def write(self, data): | |
if not self.real: | |
return | |
for line in data.split(b"\n"): | |
line += b"\n" | |
self.real.write(mitm_header_write(self.direction, len(line))+line) | |
async def drain(self): | |
if not self.real: | |
return | |
await self.real.drain() | |
def close(): | |
self.real = None | |
FLAG_BLOCK_CLIENT = 1 | |
FLAG_BLOCK_SERVER = 2 | |
async def main(character = None): | |
attributes = {} | |
reader, writer = await asyncio.open_connection('127.0.0.1', 6501) | |
writer.write(mitm_header_write(3, 1)+b"\n") | |
writer.write(mitm_header_write(5, 3)+libfurc.base.b95encode(FLAG_BLOCK_SERVER, 2)+b"\n") | |
await writer.drain() | |
client = None | |
connected = False | |
connectionID = None | |
fakeServerWriter = None | |
fakeClientWriter = None | |
fakeReader = None | |
task = None | |
while True: | |
try: | |
try: | |
data = await asyncio.wait_for(reader.readline(), 30) | |
except asyncio.TimeoutError: | |
continue | |
if data == None: | |
break | |
opcode, dlen = mitm_header_read(data) | |
data = data[5:] | |
if dlen != len(data): | |
print("Length mismatch: {} {}".format(dlen, len(data))) | |
continue | |
if data[-1:] != b"\n": | |
print("Missing new line at end of data") | |
continue | |
data = data[:-1] #Remove newline | |
if opcode == 3: | |
if not client: | |
channels = data.split(b" ") | |
found = False | |
for channel in channels: | |
channel = channel.split(b":",1) | |
if len(channel) == 2 and (character == None or channel[1].decode() == character): | |
connectionID = channel[0] | |
writer.write(mitm_header_write(4, len(channel[0])+1)+channel[0]+b"\n") | |
await writer.drain() | |
found = True | |
break | |
if not found: | |
exit() | |
elif opcode == 4: | |
if data == b"ok": | |
attributes.clear() | |
print("Attached to connection {}".format(connectionID.decode())) | |
connected = True | |
fakeServerWriter = WriterProxy(writer, 1) #Send to SERVER | |
fakeClientWriter = WriterProxy(writer, 0) #Send to Client | |
fakeReader = FakeIO() | |
client = Client() | |
await attachHooks(client, fakeServerWriter, fakeClientWriter, attributes) | |
await client.attach(fakeReader, fakeServerWriter, fakeClientWriter) | |
task = asyncio.create_task(client.run()) | |
else: | |
print("Failed to attach to connection {}: {}".format(connectionID.decode(), data.decode())) | |
connected = False | |
connectionID = b"" | |
elif opcode == 0: | |
#From server | |
if fakeReader: | |
await fakeReader.write(data+b"\n") | |
elif opcode == 1: | |
#From client | |
if client: | |
await handleClientMessage(client, data, fakeServerWriter, fakeClientWriter, attributes) | |
elif opcode == 2: | |
print("Client {} disconnected".format(data.decode())) | |
if data == connectionID: | |
if client: | |
fakeReader.close() | |
#fakeWriter.close() | |
fakeReader = None | |
fakeWriter = None | |
client = None | |
print("Attached connection {} disconnected".format(connectionID.decode())) | |
elif opcode == 5: | |
print("Proxy flags set", libfurc.base.b95decode(data[0:2])) | |
elif opcode == 6: | |
if not connected: | |
print("Client {} connected".format(data.decode())) | |
writer.write(mitm_header_write(4, len(data)+1)+data+b"\n") | |
await writer.drain() | |
except Exception as e: | |
tb = traceback.format_exc().strip("\n") | |
try: | |
await sendMessage(fakeClientWriter, ["<font color=\"error\">{}</font>".format(i) for i in tb.split("\n")]) | |
except Exception as ee: | |
print(ee) | |
pass | |
print("-"*80) | |
print(tb) | |
print("-"*80) | |
if "gui" in attributes and attributes["gui"]: | |
gui.close() | |
async def main_loop(): | |
import argparse | |
parser = argparse.ArgumentParser(description='Process some integers.') | |
parser.add_argument("-c", "--character", default=None, | |
help="Character to attach to") | |
args = parser.parse_args() | |
while True: | |
await main(args.character) | |
await asyncio.sleep(1) | |
if __name__ == "__main__": | |
asyncio.run(main_loop()) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
import sys | |
import os | |
import asyncio | |
import struct | |
import asyncio.exceptions | |
import libfurc.base | |
import socket | |
class Notifyd: | |
def __init__(self): | |
self.host = os.getenv("NOTIFY_SOCKET", None) | |
self.socket = None | |
if self.host != None: | |
self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) | |
if self.host.startswith("@"): | |
self.socket.connect("\0" + self.host[1:]) | |
else: | |
self.socket.connect(self.host) | |
def send(self, msg): | |
if self.socket: | |
self.socket.sendall(msg) | |
else: | |
for line in msg.split(b"\n"): | |
if b"=" in line: | |
op, data = line.split(b"=",1) | |
print("[NOTIFY] {}: {}".format(op.decode(), data.decode())) | |
else: | |
print("[NOTIFY] MALFORMED MESSAGE: {}".format(line.decode())) | |
def ready(self): | |
self.send(b"READY=1") | |
def stopping(self): | |
self.send(b"STOPPING=1") | |
def reloading(self): | |
self.send(b"RELOADING=1") | |
def status(self, status): | |
if type(status) == str: | |
status = status.encode() | |
self.send(b"STATUS="+status.replace(b"\n", b"\\n")) | |
notify = Notifyd() | |
def mitm_header_read(data): | |
opcode = libfurc.base.b95decode(data[0:2]) | |
dlen = libfurc.base.b95decode(data[2:5]) | |
return opcode, dlen | |
def mitm_header_write(opcode, dlen): | |
return libfurc.base.b95encode(opcode, 2) + libfurc.base.b95encode(dlen, 3) | |
def wrapReader(self, reader, local = False): | |
readuntil = reader.readuntil | |
async def readuntilWrapped(*args, **kwargs): | |
data = await readuntil(*args, **kwargs) | |
if data: | |
self.rx += len(data) | |
if local: | |
self.parent.rxl += len(data) | |
else: | |
self.parent.rx += len(data) | |
return data | |
reader.readuntil = readuntilWrapped | |
read = reader.read | |
async def readWrapped(*args, **kwargs): | |
data = await read(*args, **kwargs) | |
if data: | |
self.rx += len(data) | |
if local: | |
self.parent.rxl += len(data) | |
else: | |
self.parent.rx += len(data) | |
return data | |
reader.read = readWrapped | |
def wrapWriter(self, writer, local = False): | |
write = writer.write | |
def writeWrapped(data, *args, **kwargs): | |
res = write(data, *args, **kwargs) | |
if data: | |
self.tx += len(data) | |
if local: | |
self.parent.txl += len(data) | |
else: | |
self.parent.tx += len(data) | |
return res | |
writer.write = writeWrapped | |
class MITMInstance: | |
FLAG_BLOCK_CLIENT = 1 | |
FLAG_BLOCK_SERVER = 2 | |
def __init__(self, parent, client_reader, client_writer): | |
self.parent = parent | |
self.disconnecting = False | |
self.rx = 0 | |
self.tx = 0 | |
self.client_reader, self.client_writer = client_reader, client_writer | |
wrapReader(self, self.client_reader, True) | |
wrapWriter(self, self.client_writer, True) | |
self.listenering = None | |
self.flags = 0 | |
@property | |
def blocks_server(self): | |
return self.flags & self.FLAG_BLOCK_SERVER | |
@property | |
def blocks_client(self): | |
return self.flags & self.FLAG_BLOCK_CLIENT | |
async def disconnect(self): | |
if self.disconnecting: | |
return | |
self.disconnecting = True | |
try: | |
self.listenering.listeners.remove(self) | |
except Exception as e: | |
pass | |
self.parent.mitm_connections.remove(self) | |
async def from_mitm(self): | |
while not self.disconnecting: | |
try: | |
data = await self.client_reader.readuntil(separator=b'\n') | |
except (asyncio.exceptions.IncompleteReadError, ConnectionResetError) as e: | |
data = None | |
if self.disconnecting or not data: | |
await self.disconnect() | |
return | |
opcode, dlen = mitm_header_read(data) | |
data = data[5:] | |
if dlen != len(data): | |
print("Length mismatch: {} {}".format(dlen, len(data))) | |
#print("Opcode {} size {}".format(opcode, dlen)) | |
if opcode == 0: | |
#Send to client | |
if not self.listenering: | |
continue | |
try: | |
self.listenering.client_writer.write(data) | |
await self.listenering.client_writer.drain() | |
except Exception as e: | |
pass | |
elif opcode == 1: | |
#Send to server | |
if not self.listenering: | |
continue | |
try: | |
self.listenering.server_writer.write(data) | |
await self.listenering.server_writer.drain() | |
except Exception as e: | |
pass | |
elif opcode == 2: | |
#Disconnection | |
if not self.listenering: | |
continue | |
try: | |
self.listenering.listeners.remove(self) | |
self.listenering = None | |
except Exception as e: | |
pass | |
elif opcode == 3: | |
#Find | |
cons = [] | |
for con in self.parent.connections: | |
cons.append("{}:{}".format(con.id, con.getName())) | |
data = " ".join(cons).encode() | |
self.client_writer.write(mitm_header_write(3, len(data)+1)+data+b"\n") | |
await self.client_writer.drain() | |
elif opcode == 4: | |
#Choose | |
try: | |
conn = None | |
i = int(data.decode()) | |
for con in self.parent.connections: | |
if con.id == i: | |
conn = con | |
if conn: | |
self.client_writer.write(mitm_header_write(4, 3)+b"ok\n") | |
await self.client_writer.drain() | |
else: | |
self.client_writer.write(mitm_header_write(4, 3)+b"no\n") | |
await self.client_writer.drain() | |
if self.listenering: | |
try: | |
self.listenering.listeners.remove(self) | |
self.listenering = None | |
except Exception as e: | |
print(e) | |
pass | |
self.listenering = conn | |
self.listenering.listeners.append(self) | |
except Exception as e: | |
print(e) | |
self.client_writer.write(mitm_header_write(4, 3)+b"er\n") | |
await self.client_writer.drain() | |
elif opcode == 5: | |
#Set flags | |
if len(data) == 3: | |
self.flags = libfurc.base.b95decode(data[:2]) | |
self.client_writer.write(mitm_header_write(5, 3)+libfurc.base.b95encode(self.flags, 2)+b"\n") | |
await self.client_writer.drain() | |
elif opcode == 6: | |
#New connect | |
#NOP | |
pass | |
elif opcode == 7: | |
#Ping | |
self.client_writer.write(mitm_header_write(8, len(data))+data+b"\n") | |
elif opcode == 8: | |
#Pong | |
pass | |
else: | |
self.client_writer.write(mitm_header_write(255, 1)+b"\n") | |
await self.client_writer.drain() | |
async def write(self, data): | |
self.client_writer.write(data) | |
await self.client_writer.drain() | |
async def start(self): | |
asyncio.create_task(self.from_mitm()) | |
class FurcadiaProxyInstance: | |
SERVER_HOST = "72.52.134.168" #"lightbringer.furcadia.com" | |
SERVER_PORT = 6500 | |
def __init__(self, parent, client_reader, client_writer, host=None, port=None): | |
self.parent = parent | |
self.disconnecting = False | |
self.host = host or self.SERVER_HOST | |
self.port = port or self.SERVER_PORT | |
self.rx = 0 | |
self.tx = 0 | |
self.client_reader, self.client_writer = client_reader, client_writer | |
wrapReader(self, self.client_reader) | |
wrapWriter(self, self.client_writer) | |
self.id2 = 0 | |
self.id = self.client_writer.get_extra_info("peername")[1] | |
self.listeners = [] | |
self.data = { | |
"character": "", | |
"script": None, | |
"script_logging": False | |
} | |
def getName(self): | |
if self.data["character"]: | |
return self.data["character"].decode() | |
return str(self.id) | |
async def disconnect(self): | |
if self.disconnecting: | |
return | |
self.disconnecting = True | |
c = str(self.id).encode() | |
await self.parent.announce(mitm_header_write(2, len(c)+1)+c+b"\n") | |
""" | |
for listener in self.listeners: | |
try: | |
await listener.write(mitm_header_write(2, 1)+b"\n") | |
except Exception as e: | |
print(e) | |
self.listeners.remove(listener) | |
""" | |
for p in [self.server_writer, self.client_writer]: | |
try: | |
p.close() | |
await p.wait_closed() | |
except Exception as e: | |
print(e) | |
pass | |
self.parent.connections.remove(self) | |
async def to_client(self): | |
while not self.disconnecting: | |
try: | |
data = await self.server_reader.readuntil(separator=b'\n') | |
except asyncio.exceptions.IncompleteReadError as e: | |
data = None | |
if self.disconnecting or not data: | |
await self.disconnect() | |
return | |
if self.disconnecting or self.client_writer.is_closing(): | |
await self.disconnect() | |
return | |
blocking = False | |
for listener in self.listeners: | |
try: | |
blocks = await listener.write(mitm_header_write(0, len(data))+data) | |
blocking = blocking or listener.blocks_server | |
except Exception as e: | |
print(e) | |
self.listeners.remove(listener) | |
if not blocking: | |
self.client_writer.write(data) | |
await self.client_writer.drain() | |
async def from_client(self): | |
while not self.disconnecting: | |
try: | |
data = await self.client_reader.readuntil(separator=b'\n') | |
except asyncio.exceptions.IncompleteReadError as e: | |
data = None | |
if self.disconnecting or not data: | |
await self.disconnect() | |
return | |
if self.disconnecting or self.server_writer.is_closing(): | |
await self.disconnect() | |
return | |
blocking = False | |
for listener in self.listeners: | |
try: | |
await listener.write(mitm_header_write(1, len(data))+data) | |
blocking = blocking or listener.blocks_client | |
except Exception as e: | |
print(e) | |
self.listeners.remove(listener) | |
if data == b"which\n": | |
self.client_writer.write("(<img src='fsh://system.fsh:86' /> You are connected to Hermes [{}] (QTEMP {}). There are {} players on this Hermes, of which you are player index {} with global id {}.\n".format( | |
self.parent.port, | |
self.id2, | |
len(self.parent.connections), | |
self.parent.connections.index(self) + 1 if self in self.parent.connections else "NaN", | |
self.id | |
).encode()) | |
await self.client_writer.drain() | |
elif data[:7] == b"script ": | |
if data[7:] == b"start\n": | |
if self.data["script"] != None: | |
self.client_writer.write("(<img src='fsh://system.fsh:86' /> <font color='error'>Script already running!</font>\n".encode()) | |
await self.client_writer.drain() | |
else: | |
await self.startScript() | |
elif data[7:] == b"restart\n": | |
if self.data["script"] == None: | |
self.client_writer.write("(<img src='fsh://system.fsh:86' /> <font color='error'>No script running!</font>\n".encode()) | |
await self.client_writer.drain() | |
else: | |
await self.stopScript() | |
await self.startScript() | |
elif data[7:] == b"stop\n": | |
if self.data["script"] == None: | |
self.client_writer.write("(<img src='fsh://system.fsh:86' /> <font color='error'>No script running!</font>\n".encode()) | |
await self.client_writer.drain() | |
else: | |
await self.stopScript() | |
elif data[7:] == b"status\n": | |
if self.data["script"] == None: | |
self.client_writer.write("(<img src='fsh://system.fsh:86' /> Script not running.\n".encode()) | |
await self.client_writer.drain() | |
elif self.data["script"] != None: | |
self.client_writer.write("(<img src='fsh://system.fsh:86' /> Script running.\n".encode()) | |
await self.client_writer.drain() | |
elif data[7:11] == b"log ": | |
if data[11:] == b"on\n": | |
self.client_writer.write("(<img src='fsh://system.fsh:86' /> <font color='success'>Logging enabled.</font>\n".encode()) | |
await self.client_writer.drain() | |
self.data["script_logging"] = True | |
elif data[11:] == b"off\n": | |
self.client_writer.write("(<img src='fsh://system.fsh:86' /> <font color='success'>Logging disabled.</font>\n".encode()) | |
await self.client_writer.drain() | |
self.data["script_logging"] = False | |
else: | |
self.client_writer.write("(<img src='fsh://system.fsh:86' /> <font color='error'>Unknown command</font>\n".encode()) | |
await self.client_writer.drain() | |
continue | |
elif data[:8] == b"connect ": | |
self.data["character"] = data.split(b" ")[1] | |
elif data[:8] == b"account ": | |
self.data["character"] = data.split(b" ")[2] | |
if not blocking: | |
self.server_writer.write(data) | |
await self.server_writer.drain() | |
async def startScript(self): | |
proc = await asyncio.create_subprocess_exec( | |
sys.executable, "-u", os.path.join("/home/felix/.scripts", "furc_mitm_agent.py"), | |
'--character', self.data["character"], | |
limit = 1024 * 1024, | |
stdout=asyncio.subprocess.PIPE, | |
stderr=asyncio.subprocess.STDOUT, | |
) | |
async def job(self, proc): | |
self.client_writer.write("(<img src='fsh://system.fsh:86' /> <font color='success'>Script started.</font>\n".encode()) | |
await self.client_writer.drain() | |
while True: | |
try: | |
data = await proc.stdout.readline() | |
except ValueError as e: | |
continue | |
except Exception as e: | |
self.client_writer.write("(<img src='fsh://system.fsh:86' /> {}\n".format(str(e).replace("\n","\\n")).encode()) | |
break | |
if data == b"" or data == None: | |
break | |
line = data[:-1] | |
if self.data["script_logging"] == True: | |
self.client_writer.write("(<img src='fsh://system.fsh:86' /> {}\n".format(line.decode().replace("\n","\\n")).encode()) | |
await self.client_writer.drain() | |
self.client_writer.write("(<img src='fsh://system.fsh:86' /> <font color='success'>Script stopped.</font>\n".encode()) | |
await self.client_writer.drain() | |
self.data["script"] = None | |
self.data["script"] = { | |
"proc": proc, | |
"job": asyncio.ensure_future(job(self, proc)) | |
} | |
async def stopScript(self): | |
if self.data["script"] != None: | |
self.data["script"]["proc"].terminate() | |
async def start(self): | |
self.server_reader, self.server_writer = \ | |
await asyncio.open_connection(self.host, self.port) | |
self.id2 = self.server_writer.get_extra_info("sockname")[1] | |
tmp = str(self.id).encode() | |
await self.parent.announce(mitm_header_write(6, len(tmp)+1)+tmp+b"\n") | |
asyncio.create_task(self.from_client()) | |
asyncio.create_task(self.to_client()) | |
class FurcadiaProxy: | |
PROXY_HOST = "127.0.0.1" | |
PROXY_PORT = 6500 | |
PROXY_MITM_PORT = 6501 | |
def __init__(self, host = None, port = None, mitm_port = None): | |
self.host = host or self.PROXY_HOST | |
self.port = port or self.PROXY_PORT | |
self.mitm_port = mitm_port or self.PROXY_MITM_PORT | |
self.connections = [] | |
self.mitm_connections = [] | |
self.rx = 0 | |
self.tx = 0 | |
self.rxl = 0 | |
self.txl = 0 | |
async def announce(self, data): | |
for client in self.mitm_connections: | |
await client.write(data) | |
async def handle_proxy(self, reader, writer): | |
instance = FurcadiaProxyInstance(self, reader, writer) | |
self.connections.append(instance) | |
asyncio.create_task(instance.start()) | |
async def handle_mitm(self, reader, writer): | |
instance = MITMInstance(self, reader, writer) | |
self.mitm_connections.append(instance) | |
asyncio.create_task(instance.start()) | |
async def start(self): | |
notify.ready() | |
self.proxy = await asyncio.start_server( | |
self.handle_proxy, self.host, self.port) | |
self.mitm = await asyncio.start_server( | |
self.handle_mitm, self.host, self.mitm_port) | |
print('Listening for Furcadia on {}'.format( | |
', '.join(str(sock.getsockname()) for sock in self.proxy.sockets) | |
) | |
) | |
print('Listening for MITM on {}'.format( | |
', '.join(str(sock.getsockname()) for sock in self.mitm.sockets) | |
) | |
) | |
async with self.proxy, self.mitm: | |
await asyncio.gather( | |
self.proxy.serve_forever(), | |
self.mitm.serve_forever(), | |
self.update() | |
) | |
async def update(self): | |
while True: | |
notify.status("Serving {} (rx {} / tx {}) clients and {} (rx {} / tx {}) agents".format( | |
len(self.connections), self.rx, self.tx, | |
len(self.mitm_connections), self.rxl, self.txl | |
)) | |
await asyncio.sleep(5) | |
proxy = FurcadiaProxy() | |
asyncio.run(proxy.start()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment