Created
December 17, 2022 01:06
-
-
Save FelixWolf/9a70ced3ce2aab9174273ce20feff8d9 to your computer and use it in GitHub Desktop.
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 io | |
""" | |
* indicates comment, this is read until newline | |
{ indicates string literal start. | |
} indicates string literal end. | |
@ indicates raw integer variable number. (EG: @12) | |
% indicates integer variable. (EG: %fdsa) | |
~ indicates a string variable. | |
.x after a integer variable adds 0 to it. | |
.y after a integer variable adds 1 to it. | |
[ after a integer variable indicates it is a array, it is followed by a number. | |
] after a array number indicates the end of a array length. | |
Arrays only need to be defined once as they pre-allocate variable space. | |
Arrays are really just the value of the base array plus whatever number in the | |
array. | |
Integers CAN be signed, but it is complicated, see below. (NOT YET IMPLEMENTED) | |
Integer variables come in twos unless they are arrays, in which case | |
they are 2 + (array size - 1) | |
String variables come in ones, unless they are arrays, in which case | |
they are 1 + (array size - 1) | |
Variables, regardless of type, are represented as 50000 + variable index | |
This is why I said signed integers are complicated. | |
If you sign -1, you get 65535, which is variable 15535. | |
But there can only be 1000 variables, which means range -15536 to -13536 is | |
off limits. | |
So that gives us around -13000 to 50000 in numbers to play with before stuff | |
begins to go haywire. | |
""" | |
class DSCompileError(Exception): | |
def __init__(self, message, line = None): | |
self.message = message | |
self.line = line | |
super().__init__(self.message) | |
def __str__(self): | |
if self.line: | |
return "Line #{}: {}".format(self.line + 1, self.message) | |
return self.message | |
class DSCompiler: | |
maxArgCount = 8 | |
def __init__(self, source = ""): | |
self.line = 0 | |
self.offset = 0 | |
self.source = source | |
self.version = (1, 0) | |
self.dsLines = [] | |
self.stringTable = ["Master Socket kidnapped my kiwi!"] | |
self.variableTable = [] | |
def pushDSLine(self, category, command, arguments): | |
self.dsLines.append([category, command] + arguments) | |
def pushString(self, data): | |
if data in self.stringTable: | |
return self.stringTable.index(data) | |
self.stringTable.append(data) | |
return len(self.stringTable) - 1 | |
def pushVariable(self, name, size = 2): | |
if name in self.variableTable: | |
return self.variableTable.index(name) | |
self.variableTable.append(name) | |
for i in range(1, size): | |
self.variableTable.append(None) | |
return (len(self.variableTable) - 1) + 50000 | |
def readLine(self): | |
if self.offset == len(self.source): | |
#Return empty if end of line | |
return "" | |
buffer = "" | |
while self.offset < len(self.source): | |
buffer += self.source[self.offset] | |
if self.source[self.offset] == "\n": | |
self.offset += 1 | |
break | |
self.offset += 1 | |
self.line += 1 | |
return buffer | |
def compile(self, source = None): | |
self.line = 0 | |
self.offset = 0 | |
self.version = (1, 0) | |
self.dsLines = [] | |
self.stringTable = ["Master Socket kidnapped my kiwi!"] | |
self.variableTable = [] | |
if source != None: | |
self.source = source | |
self.offset = 0 | |
#Read until DSPK indicator | |
while True: | |
line = self.readLine() | |
if not line: | |
#Just steal the string furcadia spits out when this happens. | |
raise DSCompileError( | |
"The DragonSpeak file is missing the "\ | |
"\"DSPK V##.## Furcadia\" -line from the very"\ | |
" beginning of the file.", | |
self.line | |
) from None | |
#Furcadia strips it, so we shall to. | |
line = line.strip() | |
#Look for "DSPK V" specifically, this is what Furcadia does | |
if line[0:6] == "DSPK V": | |
#Furcadia assumes the format is *ALWAYS* this format: | |
# "DSPK V<major>.<minor> Furcadia". | |
# Both major and minor are zero padded numbers of length 2. | |
# So we can safely extract the numbers based off position | |
try: | |
major = int(line[6:8]) | |
minor = int(line[10:12]) | |
except ValueError: | |
continue | |
self.version = (major, minor) | |
self.platform = line[12:] #No need to strip, already did so. | |
break | |
while True: | |
line = self.readLine() | |
if not line: | |
#Furcadia doesn't check if there is a "*Endtriggers*" | |
# for a successful compile, just assume it is end. | |
break | |
line = line.strip() | |
if line.startswith("*"): | |
#Comment or end of DSPK | |
if line[0:13] == "*Endtriggers*" \ | |
and line[-13:] == "*Endtriggers*": | |
try: | |
#Do we need this? Assume we need this. | |
midval = int(line[14:-14]) | |
except ValueError: | |
raise DSCompileError( | |
"Endtriggers found but got unexpected middle value!", | |
self.line | |
) from None | |
#Continue because this it can only be a comment | |
continue | |
i = 0 | |
l = len(line) | |
#Parse the actual line(s) | |
category = None | |
command = None | |
arguments = [0] * 8 | |
argnum = 0 | |
while i < l: | |
c = line[i] | |
if c == "*": | |
#Comment, escape! | |
i = l | |
break | |
#Raw variable | |
if c == "@": | |
if not command: | |
raise DSCompileError( | |
"Unexpected argument definition", | |
self.line | |
) from None | |
buffer = "" | |
i += 1 | |
c = line[i] | |
while i < l: | |
if not 48 <= ord(c) <= 57: | |
break | |
buffer += c | |
i += 1 | |
if buffer == "": | |
raise DSCompileError( | |
"Cannot define variable with empty name", | |
self.line | |
) from None | |
if argnum > self.maxArgCount: | |
raise DSCompileError( | |
"Too many arguments for DS entry ({}:{})".format( | |
category, | |
command | |
), | |
self.line | |
) from None | |
arguments[argnum] = buffer | |
argnum += 1 | |
#Integer variable | |
elif c == "%": | |
if not command: | |
raise DSCompileError( | |
"Unexpected argument definition", | |
self.line | |
) from None | |
coordinate = False | |
if i > 0 and line[i - 1] == "(": | |
coordinate = True | |
buffer = "" | |
arrayLength = 2 | |
offset = 0 | |
i += 1 | |
while i < l: | |
c = line[i] | |
#Array indicator | |
if c == "[": | |
subbuffer = "" | |
i += 1 | |
while i < l: | |
c = line[i] | |
if c == "]": #End marker | |
break | |
if not 48 <= ord(c) <= 57: | |
raise DSCompileError( | |
"Unexpected token {}".format(c), | |
self.line | |
) from None | |
subbuffer += c | |
i += 1 | |
#Check if we ended correctly | |
if i == l and line[i - 1] != "]": | |
raise DSCompileError( | |
"Unexpected end of string", | |
self.line | |
) from None | |
if subbuffer == "": | |
raise DSCompileError( | |
"Cannot define array with empty length", | |
self.line | |
) from None | |
arrayLength = int(subbuffer) | |
#Coordinate accessor | |
elif c == ".": | |
if offset: | |
break | |
if i + 1 < l: | |
c = line[i + 1] | |
if c.lower() == "x": | |
i += 2 | |
offset = 0 | |
elif c.lower() == "y": | |
i += 2 | |
offset = 1 | |
break | |
#Space or whatever | |
else: | |
v = ord(c) | |
if not (48 <= v <= 57 \ | |
or 65 <= v <= 90 \ | |
or 97 <= v <= 122): | |
break | |
buffer += c | |
i += 1 | |
if buffer == "": | |
raise DSCompileError( | |
"Cannot define variable with empty name", | |
self.line | |
) from None | |
#Check if we are being referenced as a coordinate variable | |
argCheck = 0 | |
if coordinate and line[i] == ")": | |
argCheck += 1 | |
i += 1 | |
else: | |
coordinate = False | |
if argnum + argCheck > self.maxArgCount: | |
raise DSCompileError( | |
"Too many arguments for DS entry ({}:{})".format( | |
category, | |
command | |
), | |
self.line | |
) from None | |
arguments[argnum] = self.pushVariable(buffer, arrayLength) + offset | |
argnum += 1 | |
if coordinate: | |
arguments[argnum] = self.pushVariable(buffer, arrayLength) + offset + 1 | |
argnum += 1 | |
#String variable | |
elif c == "~": | |
if not command: | |
raise DSCompileError( | |
"Unexpected argument definition", | |
self.line | |
) from None | |
buffer = "" | |
arrayLength = 1 | |
offset = 0 | |
i += 1 | |
while i < l: | |
c = line[i] | |
#Array indicator | |
if c == "[": | |
subbuffer = "" | |
i += 1 | |
while i < l: | |
c = line[i] | |
if c == "]": #End marker | |
break | |
if not 48 <= ord(c) <= 57: | |
raise DSCompileError( | |
"Unexpected token {}".format(c), | |
self.line | |
) from None | |
subbuffer += c | |
i += 1 | |
#Check if we ended correctly | |
if i == l and line[i - 1] != "]": | |
raise DSCompileError( | |
"Unexpected end of string", | |
self.line | |
) from None | |
if subbuffer == "": | |
raise DSCompileError( | |
"Cannot define array with empty length", | |
self.line | |
) from None | |
arrayLength = int(subbuffer) | |
#Space or whatever | |
else: | |
v = ord(c) | |
if not (48 <= v <= 57 \ | |
or 65 <= v <= 90 \ | |
or 97 <= v <= 122): | |
break | |
buffer += c | |
i += 1 | |
if buffer == "": | |
raise DSCompileError( | |
"Cannot define variable with empty name", | |
self.line | |
) from None | |
if argnum > self.maxArgCount: | |
raise DSCompileError( | |
"Too many arguments for DS entry ({}:{})".format( | |
category, | |
command | |
), | |
self.line | |
) from None | |
arguments[argnum] = self.pushVariable(buffer, arrayLength) + offset | |
argnum += 1 | |
#String literal | |
elif c == "{": | |
if not command: | |
raise DSCompileError( | |
"Unexpected argument definition", | |
self.line | |
) from None | |
buffer = "" | |
i += 1 | |
escaped = False | |
while i < l: | |
c = line[i] | |
if c == "\\": | |
i += 1 | |
if i < l: | |
buffer += line[i] | |
else: | |
raise DSCompileError( | |
"Unexpected end of string", | |
self.line | |
) from None | |
elif c == "}": | |
break | |
elif c == "%": | |
vbuffer = "" | |
offset = 0 | |
i += 1 | |
while i < l: | |
c = line[i] | |
#Coordinate accessor | |
if c == ".": | |
if offset: | |
break | |
if i + 1 < l: | |
c = line[i + 1] | |
if c.lower() == "x": | |
i += 2 | |
offset = 0 | |
elif c.lower() == "y": | |
i += 2 | |
offset = 1 | |
break | |
#Space or whatever | |
else: | |
v = ord(c) | |
if not (48 <= v <= 57 \ | |
or 65 <= v <= 90 \ | |
or 97 <= v <= 122): | |
break | |
vbuffer += c | |
i += 1 | |
buffer += "%"+str(self.pushVariable(vbuffer) + offset) | |
#String variable reference | |
elif c == "~": | |
vbuffer = "" | |
i += 1 | |
while i < l: | |
c = line[i] | |
#Space or whatever | |
v = ord(c) | |
if not (48 <= v <= 57 \ | |
or 65 <= v <= 90 \ | |
or 97 <= v <= 122): | |
break | |
vbuffer += c | |
i += 1 | |
buffer += "~"+str(self.pushVariable(vbuffer, 1)) | |
else: | |
buffer += c | |
i += 1 | |
#Check if we ended correctly | |
if i == l and line[i - 1] != "}": | |
raise DSCompileError( | |
"Unexpected end of string", | |
self.line | |
) from None | |
if argnum > self.maxArgCount: | |
raise DSCompileError( | |
"Too many arguments for DS entry ({}:{})".format( | |
category, | |
command | |
), | |
self.line | |
) from None | |
arguments[argnum] = self.pushString(buffer) | |
argnum += 1 | |
#Integer literal | |
elif 48 <= ord(c) <= 57: | |
buffer = "" | |
#Check for signing | |
if i > 0 and line[i - 1] == "-": | |
buffer += "-" | |
while i < l: | |
c = line[i] | |
if not 48 <= ord(c) <= 57: | |
break | |
buffer += c | |
i += 1 | |
#No need to validate because we only accept integers here | |
buffer = int(buffer) | |
if i < l and line[i] == ":": | |
#If category and command are set, assume new line | |
if category and command: | |
self.pushDSLine(category, command, arguments) | |
category = None | |
command = None | |
arguments = [0] * 8 | |
argnum = 0 | |
#If we are defining a category | |
elif category: | |
raise DSCompileError( | |
"Expected : after category declaration", | |
self.line | |
) from None | |
if category == None: | |
category = buffer | |
elif command == None: | |
command = buffer | |
else: | |
if argnum > self.maxArgCount: | |
raise DSCompileError( | |
"Too many arguments for DS entry ({}:{})".format( | |
category, | |
command | |
), | |
self.line | |
) from None | |
arguments[argnum] = buffer | |
argnum += 1 | |
i += 1 | |
if category: | |
self.pushDSLine(category, command, arguments) | |
#We will never have a only category declaration because of earlier | |
# checks | |
return True | |
def dumpDSB(self, handle): | |
handle.write(struct.pack("5sII9x", | |
b"DSB10", | |
len(self.dsLines), | |
0 | |
)) | |
lineStruct = struct.Struct("10H") | |
for line in self.dsLines: | |
handle.write(lineStruct.pack(*line)) | |
def dumpsDSB(self): | |
d = io.BytesIO() | |
self.dumpDSB(d) | |
d.seek(0) | |
return d.read() | |
if __name__ == "__main__": | |
import argparse | |
parser = argparse.ArgumentParser( | |
description='Compile dragonspeak into dragonspeak binaries' | |
) | |
parser.add_argument('input', help='Input source file') | |
parser.add_argument( | |
'output', | |
help='Output binary', | |
nargs="?", | |
default="o.dsb" | |
) | |
parser.add_argument( | |
'--dst', | |
help='DragonSpeak String Table file (Defaults to o.txb)', | |
default=None | |
) | |
parser.add_argument( | |
'--dvt', | |
help='DragonSpeak Variable Table file (Defaults to o.vxb)', | |
default=None | |
) | |
parser.add_argument( | |
'-v', '--verbose', | |
help='Do more screaming than usual', | |
action='store_true' | |
) | |
args = parser.parse_args() | |
compiler = DSCompiler() | |
with open(args.input, "r") as f: | |
compiler.compile(f.read()) | |
with open(args.output, "wb") as f: | |
compiler.dumpDSB(f) | |
prename = ".".join(args.output.split(".")[:-1]) | |
with open(args.dst or (prename + ".txb"), "w") as f: | |
f.write("DST10\n") | |
for i in range(0, len(compiler.stringTable)): | |
f.write("{}:{}\n".format(i, compiler.stringTable[i])) | |
with open(args.dvt or (prename + ".vxb"), "w") as f: | |
f.write("DVT10\n") | |
for i in range(0, len(compiler.variableTable)): | |
if compiler.variableTable[i] == None: | |
continue | |
f.write("{}:{}\n".format(i, compiler.variableTable[i])) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment