Skip to content

Instantly share code, notes, and snippets.

@FelixWolf
Created December 17, 2022 01:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save FelixWolf/9a70ced3ce2aab9174273ce20feff8d9 to your computer and use it in GitHub Desktop.
Save FelixWolf/9a70ced3ce2aab9174273ce20feff8d9 to your computer and use it in GitHub Desktop.
#!/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