2012-08-29 06:43:53 +00:00
|
|
|
# vim:ts=4:et
|
2012-09-10 03:40:55 +00:00
|
|
|
# ##### BEGIN GPL LICENSE BLOCK #####
|
|
|
|
#
|
|
|
|
# This program is free software; you can redistribute it and/or
|
|
|
|
# modify it under the terms of the GNU General Public License
|
|
|
|
# as published by the Free Software Foundation; either version 2
|
|
|
|
# of the License, or (at your option) any later version.
|
|
|
|
#
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU General Public License
|
|
|
|
# along with this program; if not, write to the Free Software Foundation,
|
|
|
|
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
|
|
#
|
|
|
|
# ##### END GPL LICENSE BLOCK #####
|
|
|
|
|
|
|
|
# <pep8 compliant>
|
2012-09-03 04:35:18 +00:00
|
|
|
|
|
|
|
import os
|
2022-05-27 11:06:33 +00:00
|
|
|
try:
|
|
|
|
from .script import Script
|
|
|
|
from .qfplist import pldata
|
|
|
|
from . import quakechr
|
|
|
|
except ImportError:
|
|
|
|
from script import Script
|
|
|
|
from qfplist import pldata
|
|
|
|
import quakechr
|
2012-08-29 06:43:53 +00:00
|
|
|
|
|
|
|
MAX_FLAGS = 8
|
|
|
|
|
2012-09-10 06:00:30 +00:00
|
|
|
class EntityClassError(Exception):
|
|
|
|
def __init__(self, fname, line, message):
|
|
|
|
Exception.__init__(self, "%s:%d: %s" % (fname, line, message))
|
|
|
|
self.line = line
|
|
|
|
|
|
|
|
def entclass_error(self, msg):
|
|
|
|
raise EntityClassError(self.filename, self.line, msg)
|
|
|
|
|
2022-05-27 11:06:33 +00:00
|
|
|
class EntityField:
|
|
|
|
def __init__(self, name, default, comment):
|
|
|
|
self.name = name
|
|
|
|
self.default = default
|
|
|
|
self.comment = comment
|
|
|
|
def to_dictionary(self):
|
|
|
|
d = {}
|
|
|
|
if self.default != None:
|
|
|
|
d["default"] = self.default
|
|
|
|
if self.comment:
|
|
|
|
d["comment"] = self.comment
|
|
|
|
if hasattr(self, "sounds"):
|
|
|
|
d["sounds"] = self.sounds
|
|
|
|
return d
|
|
|
|
@classmethod
|
|
|
|
def from_dictionary(cls, name, d):
|
|
|
|
if "default" in d:
|
|
|
|
default = d["default"]
|
|
|
|
else:
|
|
|
|
default = None
|
|
|
|
if "comment" in d:
|
|
|
|
comment = d["comment"]
|
|
|
|
else:
|
|
|
|
comment = ""
|
|
|
|
field = cls(name, default, comment)
|
|
|
|
if "sounds" in d:
|
|
|
|
field.sounds = d["sounds"]
|
|
|
|
return field
|
|
|
|
|
2012-08-29 06:43:53 +00:00
|
|
|
class EntityClass:
|
2022-05-27 11:06:33 +00:00
|
|
|
def __init__(self, name, color, size, flagnames, comment, fields):
|
2012-09-03 04:35:18 +00:00
|
|
|
self.name = name
|
|
|
|
self.color = color
|
|
|
|
self.size = size
|
|
|
|
self.flagnames = flagnames
|
|
|
|
self.comment = comment
|
2022-05-27 11:06:33 +00:00
|
|
|
self.fields = fields
|
2012-09-03 04:35:18 +00:00
|
|
|
@classmethod
|
2012-09-09 08:11:57 +00:00
|
|
|
def null(cls):
|
2022-05-27 11:06:33 +00:00
|
|
|
return cls('', (1, 1, 1), None, (), "", {})
|
2012-09-09 08:11:57 +00:00
|
|
|
@classmethod
|
2012-09-03 04:35:18 +00:00
|
|
|
def from_quaked(cls, text, filename, line = 0):
|
2012-08-29 06:43:53 +00:00
|
|
|
script = Script(filename, text)
|
2012-09-10 06:00:30 +00:00
|
|
|
script.error = entclass_error.__get__(script, Script)
|
2012-08-29 06:43:53 +00:00
|
|
|
if line:
|
|
|
|
script.line = line
|
|
|
|
script.getToken() # skip over the leading '/*QUAKED'
|
2012-09-03 04:35:18 +00:00
|
|
|
name = script.getToken()
|
|
|
|
color = cls.parse_vector(script)
|
2012-09-13 03:21:06 +00:00
|
|
|
if script.tokenAvailable():
|
|
|
|
size = cls.parse_size(script)
|
|
|
|
flagnames = cls.parse_flags(script)
|
|
|
|
else:
|
|
|
|
size = None
|
|
|
|
flagnames = ()
|
2022-05-27 11:06:33 +00:00
|
|
|
comment = []
|
|
|
|
fields = {}
|
|
|
|
script.quotes = False
|
|
|
|
script.single = ""
|
|
|
|
while script.tokenAvailable(True):
|
|
|
|
line = []
|
|
|
|
while script.tokenAvailable():
|
|
|
|
script.getToken()
|
|
|
|
if script.token[-2:] == "*/":
|
|
|
|
break;
|
|
|
|
line.append(script.token)
|
|
|
|
if line:
|
|
|
|
if ((line[0][0] == '"' and line[0][-1] == '"')
|
|
|
|
or (len(line) > 1 and line[1] == '=')):
|
|
|
|
if line[0][0] == '"':
|
|
|
|
fname = line[0][1:-1]
|
|
|
|
line = line[1:]
|
|
|
|
else:
|
|
|
|
fname = line[0]
|
|
|
|
line = line[2:]
|
|
|
|
default = None
|
|
|
|
for i, t in enumerate(line[:-1]):
|
|
|
|
if t[0] == '(' and line[i + 1] == "default)":
|
|
|
|
default = t[1:]
|
|
|
|
break
|
|
|
|
line = " ".join(line)
|
|
|
|
fields[fname] = EntityField(fname, default, line)
|
|
|
|
line = None
|
|
|
|
elif "sounds" in fields:
|
|
|
|
sounds = fields["sounds"]
|
|
|
|
if not hasattr(sounds, "sounds"):
|
|
|
|
sounds.sounds = []
|
|
|
|
if line[0][-1] == ')':
|
|
|
|
line[0] = line[0][:-1]
|
|
|
|
sounds.sounds.append((line[0], " ".join(line[1:])))
|
|
|
|
line = None
|
|
|
|
else:
|
|
|
|
line = " ".join(line)
|
|
|
|
if line:
|
|
|
|
comment.append(line)
|
|
|
|
if script.token[-2:] == "*/":
|
|
|
|
break;
|
|
|
|
return cls(name, color, size, flagnames, comment, fields)
|
2012-09-03 04:35:18 +00:00
|
|
|
@classmethod
|
|
|
|
def from_dictionary(cls, name, d):
|
|
|
|
if "color" in d:
|
|
|
|
color = d["color"]
|
|
|
|
color = float(color[0]), float(color[1]), float(color[2])
|
|
|
|
else:
|
|
|
|
color = (0.0, 0.0, 0.0)
|
|
|
|
if "size" in d:
|
|
|
|
mins, maxs = d["size"]
|
|
|
|
size = ((float(mins[0]), float(mins[1]), float(mins[2])),
|
|
|
|
(float(maxs[0]), float(maxs[1]), float(maxs[2])))
|
|
|
|
else:
|
|
|
|
size = None
|
|
|
|
if "flagnames" in d:
|
2012-09-09 08:01:23 +00:00
|
|
|
flagnames = tuple(d["flagnames"])
|
2012-09-03 04:35:18 +00:00
|
|
|
else:
|
2012-09-09 08:01:23 +00:00
|
|
|
flagnames = ()
|
2012-09-03 04:35:18 +00:00
|
|
|
if "comment" in d:
|
|
|
|
comment = d["comment"]
|
|
|
|
else:
|
2022-05-27 11:06:33 +00:00
|
|
|
comment = []
|
|
|
|
if "fields" in d:
|
|
|
|
field_dict = d["fields"]
|
|
|
|
fields = {}
|
|
|
|
for f in field_dict:
|
|
|
|
fields[f] = EntityField.from_dictionary(f, field_dict[f])
|
|
|
|
else:
|
|
|
|
fields = {}
|
|
|
|
return cls(name, color, size, flagnames, comment, fields)
|
2012-09-03 04:35:18 +00:00
|
|
|
def to_dictionary(self):
|
2022-05-27 11:06:33 +00:00
|
|
|
fields = {}
|
|
|
|
for f in self.fields:
|
|
|
|
fields[f] = self.fields[f].to_dictionary()
|
2012-09-03 04:35:18 +00:00
|
|
|
d = {"color":self.color, "flagnames":self.flagnames,
|
2022-05-27 11:06:33 +00:00
|
|
|
"comment":self.comment, "fields":fields}
|
2012-09-03 04:35:18 +00:00
|
|
|
if self.size:
|
|
|
|
d["size"] = self.size
|
|
|
|
return d
|
|
|
|
@classmethod
|
|
|
|
def parse_vector(cls, script):
|
2012-08-29 06:43:53 +00:00
|
|
|
if script.getToken() != "(":
|
2012-09-10 06:00:30 +00:00
|
|
|
script.error("Missing (")
|
2022-05-27 11:06:33 +00:00
|
|
|
s = script.getToken(), script.getToken(), script.getToken()
|
|
|
|
try:
|
|
|
|
v = (float(s[0]), float(s[1]), float(s[2]))
|
|
|
|
except ValueError:
|
|
|
|
v = s
|
2012-08-29 06:43:53 +00:00
|
|
|
if script.getToken() != ")":
|
2012-09-10 06:00:30 +00:00
|
|
|
script.error("Missing )")
|
2012-08-29 06:43:53 +00:00
|
|
|
return v
|
2012-09-03 04:35:18 +00:00
|
|
|
@classmethod
|
|
|
|
def parse_size(cls, script):
|
2012-08-29 06:43:53 +00:00
|
|
|
if script.getToken() == "?":
|
|
|
|
return None # use brush size
|
2012-09-13 03:21:06 +00:00
|
|
|
elif script.token == "(":
|
|
|
|
script.ungetToken()
|
|
|
|
return cls.parse_vector(script), cls.parse_vector(script)
|
|
|
|
else:
|
|
|
|
script.ungetToken()
|
|
|
|
return None
|
2012-09-03 04:35:18 +00:00
|
|
|
@classmethod
|
|
|
|
def parse_flags(cls, script):
|
2012-08-29 06:43:53 +00:00
|
|
|
flagnames = []
|
|
|
|
while script.tokenAvailable():
|
|
|
|
#any remaining words on the line are flag names, but only MAX_FLAGS
|
|
|
|
#names are kept.
|
|
|
|
script.getToken()
|
|
|
|
if len(flagnames) < MAX_FLAGS:
|
|
|
|
flagnames.append(script.token)
|
|
|
|
return tuple(flagnames)
|
2012-09-03 04:35:18 +00:00
|
|
|
def extract_comment(cls, script):
|
2012-08-29 06:43:53 +00:00
|
|
|
if not script.tokenAvailable(True):
|
|
|
|
return ""
|
|
|
|
start = pos = script.pos
|
|
|
|
while pos < len(script.text) and script.text[pos:pos + 2] != "*/":
|
|
|
|
if script.text[pos] == "\n":
|
|
|
|
script.line += 1
|
|
|
|
pos += 1
|
|
|
|
comment = script.text[start:pos]
|
|
|
|
if pos < len(script.text):
|
|
|
|
pos += 2
|
|
|
|
script.pos = pos
|
|
|
|
return comment
|
|
|
|
|
2012-09-03 04:35:18 +00:00
|
|
|
class EntityClassDict:
|
|
|
|
def __init__(self):
|
|
|
|
self.path = ""
|
|
|
|
self.entity_classes = {}
|
2012-09-16 01:55:06 +00:00
|
|
|
def __len__(self):
|
|
|
|
return self.entity_classes.__len__()
|
|
|
|
def __getitem__(self, key):
|
2020-04-14 15:24:20 +00:00
|
|
|
if key == '.':
|
|
|
|
return EntityClass.null()
|
2012-09-16 01:55:06 +00:00
|
|
|
return self.entity_classes.__getitem__(key)
|
|
|
|
def __iter__(self):
|
|
|
|
return self.entity_classes.__iter__()
|
|
|
|
def __contains__(self, item):
|
|
|
|
return self.entity_classes.__contains__(item)
|
|
|
|
def keys(self):
|
|
|
|
return self.entity_classes.keys()
|
|
|
|
def values(self):
|
|
|
|
return self.entity_classes.values()
|
|
|
|
def items(self):
|
|
|
|
return self.entity_classes.items()
|
|
|
|
def get(self, key, default=None):
|
|
|
|
return self.entity_classes.get(key, default)
|
2012-09-03 04:35:18 +00:00
|
|
|
def scan_source(self, fname):
|
2012-09-13 03:19:26 +00:00
|
|
|
text = open(fname, "rt", encoding="idquake").read()
|
2012-09-03 04:35:18 +00:00
|
|
|
line = 1
|
|
|
|
pos = 0
|
|
|
|
while pos < len(text):
|
|
|
|
if text[pos:pos + 8] == "/*QUAKED":
|
|
|
|
start = pos
|
|
|
|
start_line = line
|
|
|
|
while pos < len(text) and text[pos:pos + 2] != "*/":
|
|
|
|
if text[pos] == "\n":
|
|
|
|
line += 1
|
|
|
|
pos += 1
|
|
|
|
if pos < len(text):
|
|
|
|
pos += 2
|
|
|
|
ec = EntityClass.from_quaked(text[start:pos], fname,
|
|
|
|
start_line)
|
|
|
|
self.entity_classes[ec.name] = ec
|
|
|
|
else:
|
2012-08-29 06:43:53 +00:00
|
|
|
if text[pos] == "\n":
|
|
|
|
line += 1
|
|
|
|
pos += 1
|
2012-09-03 04:35:18 +00:00
|
|
|
def scan_directory(self, path):
|
|
|
|
files = os.listdir(path)
|
|
|
|
files.sort()
|
|
|
|
for f in files:
|
|
|
|
if f[0] in [".", "_"]:
|
|
|
|
continue
|
|
|
|
if os.path.isdir(os.path.join(path, f)):
|
|
|
|
self.scan_directory(os.path.join(path, f))
|
|
|
|
else:
|
|
|
|
if f[-3:] == ".qc":
|
|
|
|
self.scan_source(os.path.join(path, f))
|
|
|
|
def from_source_tree(self, path):
|
|
|
|
self.path = path
|
|
|
|
self.entity_classes = {}
|
|
|
|
self.scan_directory(self.path)
|
|
|
|
def to_plist(self):
|
|
|
|
pl = pldata()
|
|
|
|
ec = {}
|
|
|
|
for k in self.entity_classes.keys():
|
|
|
|
ec[k] = self.entity_classes[k].to_dictionary()
|
|
|
|
return pl.write(ec)
|
|
|
|
def from_plist(self, plist):
|
|
|
|
pl = pldata(plist)
|
|
|
|
ec = pl.parse()
|
|
|
|
self.entity_classes = {}
|
|
|
|
for k in ec.keys():
|
|
|
|
self.entity_classes[k] = EntityClass.from_dictionary(k, ec[k])
|
2022-05-27 11:06:33 +00:00
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
import sys
|
|
|
|
from pprint import pprint
|
|
|
|
from textwrap import TextWrapper
|
|
|
|
|
|
|
|
mainwrap = TextWrapper(width = 70)
|
|
|
|
fieldwrap = TextWrapper(width = 50)
|
|
|
|
|
|
|
|
ecd = EntityClassDict()
|
|
|
|
for fname in sys.argv[1:]:
|
|
|
|
ecd.scan_source(fname)
|
|
|
|
text = ecd.to_plist()
|
|
|
|
print(text)
|
|
|
|
ecd.from_plist(text)
|
|
|
|
for ec in ecd.entity_classes.values():
|
|
|
|
print(f"{ec.name}: {ec.color} {ec.size} {ec.flagnames}")
|
|
|
|
for c in ec.comment:
|
|
|
|
mlines = mainwrap.wrap(c)
|
|
|
|
for m in mlines:
|
|
|
|
print(f" {m}")
|
|
|
|
print()
|
|
|
|
for f in ec.fields.values():
|
|
|
|
print(f" {f.name}: {f.default}")
|
|
|
|
flines = fieldwrap.wrap(f.comment)
|
|
|
|
for l in flines:
|
|
|
|
print(f" {l}")
|
|
|
|
if f.name == "sounds":
|
|
|
|
for s in f.sounds:
|
|
|
|
print(f" {s[0]} {s[1]}")
|
|
|
|
print()
|
|
|
|
print()
|