mirror of
https://git.code.sf.net/p/quake/quakeforge
synced 2025-03-11 11:51:17 +00:00
[qfmap] Parse field info from quaked comments
Id's comments are a little inconsistent, but for the most part usable info can be extracted. While not yet supported, Arcane Dimensions' comments are extremely consistent (just some issues with hyphen counts in separators), so parsing out usable info will be fairly easy. The hard part will be presenting it.
This commit is contained in:
parent
2e14300a11
commit
6e8a3b8153
4 changed files with 162 additions and 38 deletions
|
@ -253,7 +253,7 @@ class pldata:
|
|||
elif type(item) in [int, float]:
|
||||
self.write_string(str(item))
|
||||
else:
|
||||
raise PListError(0, "unsupported type")
|
||||
raise PListError(0, f"unsupported type {type(item)}")
|
||||
def write(self, item):
|
||||
self.data = []
|
||||
self.write_item(item, 0)
|
||||
|
|
|
@ -25,6 +25,9 @@ from bpy.props import BoolProperty, FloatProperty, StringProperty, EnumProperty
|
|||
from bpy.props import BoolVectorProperty, CollectionProperty, PointerProperty
|
||||
from bpy.props import FloatVectorProperty, IntProperty
|
||||
from mathutils import Vector
|
||||
from math import ceil
|
||||
|
||||
from textwrap import TextWrapper
|
||||
|
||||
from .entityclass import EntityClass
|
||||
|
||||
|
@ -158,23 +161,6 @@ class QFEntpropRemove(bpy.types.Operator):
|
|||
qfentity.fields.remove(qfentity.field_idx)
|
||||
return {'FINISHED'}
|
||||
|
||||
def reflow_text(text, max_width):
|
||||
lines = []
|
||||
for text_line in text.split("\n"):
|
||||
if not text_line:
|
||||
continue
|
||||
words = text_line.split(" ")
|
||||
flowed_line = ""
|
||||
while words:
|
||||
if len(flowed_line) + len(words[0]) > max_width:
|
||||
lines.append(flowed_line)
|
||||
flowed_line = ""
|
||||
flowed_line += (" " if flowed_line else "") + words[0]
|
||||
del words[0]
|
||||
if flowed_line:
|
||||
lines.append(flowed_line)
|
||||
return lines
|
||||
|
||||
class OBJECT_PT_EntityPanel(bpy.types.Panel):
|
||||
bl_space_type = 'PROPERTIES'
|
||||
bl_region_type = 'WINDOW'
|
||||
|
@ -198,9 +184,22 @@ class OBJECT_PT_EntityPanel(bpy.types.Panel):
|
|||
row = layout.row()
|
||||
row.prop(qfentity, "classname")
|
||||
box=layout.box()
|
||||
lines = reflow_text(ec.comment, 40)
|
||||
for l in lines:
|
||||
box.label(text=l)
|
||||
width = int(ceil(context.region.width / 6))
|
||||
mainwrap = TextWrapper(width = width)
|
||||
subwrap = TextWrapper(width = width - 8)
|
||||
for c in ec.comment:
|
||||
clines = mainwrap.wrap(c)
|
||||
for l in clines:
|
||||
box.label(text=l)
|
||||
for f in ec.fields.values():
|
||||
print(f.name)
|
||||
flines = subwrap.wrap(f.comment)
|
||||
box.label(text=f"{f.name}")
|
||||
for l in flines:
|
||||
box.label(text=f" {l}")
|
||||
if hasattr(f, "sounds"):
|
||||
for s in f.sounds:
|
||||
box.label(text=f" {s[0]} {s[1]}")
|
||||
row = layout.row()
|
||||
for c in range(3):
|
||||
col = row.column()
|
||||
|
|
|
@ -20,9 +20,14 @@
|
|||
# <pep8 compliant>
|
||||
|
||||
import os
|
||||
from .script import Script
|
||||
from .qfplist import pldata
|
||||
from . import quakechr
|
||||
try:
|
||||
from .script import Script
|
||||
from .qfplist import pldata
|
||||
from . import quakechr
|
||||
except ImportError:
|
||||
from script import Script
|
||||
from qfplist import pldata
|
||||
import quakechr
|
||||
|
||||
MAX_FLAGS = 8
|
||||
|
||||
|
@ -34,16 +39,46 @@ class EntityClassError(Exception):
|
|||
def entclass_error(self, msg):
|
||||
raise EntityClassError(self.filename, self.line, msg)
|
||||
|
||||
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
|
||||
|
||||
class EntityClass:
|
||||
def __init__(self, name, color, size, flagnames, comment):
|
||||
def __init__(self, name, color, size, flagnames, comment, fields):
|
||||
self.name = name
|
||||
self.color = color
|
||||
self.size = size
|
||||
self.flagnames = flagnames
|
||||
self.comment = comment
|
||||
self.fields = fields
|
||||
@classmethod
|
||||
def null(cls):
|
||||
return cls('', (1, 1, 1), None, (), "")
|
||||
return cls('', (1, 1, 1), None, (), "", {})
|
||||
@classmethod
|
||||
def from_quaked(cls, text, filename, line = 0):
|
||||
script = Script(filename, text)
|
||||
|
@ -59,8 +94,49 @@ class EntityClass:
|
|||
else:
|
||||
size = None
|
||||
flagnames = ()
|
||||
comment = cls.extract_comment(script)
|
||||
return cls(name, color, size, flagnames, comment)
|
||||
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)
|
||||
@classmethod
|
||||
def from_dictionary(cls, name, d):
|
||||
if "color" in d:
|
||||
|
@ -81,11 +157,21 @@ class EntityClass:
|
|||
if "comment" in d:
|
||||
comment = d["comment"]
|
||||
else:
|
||||
comment = ""
|
||||
return cls(name, color, size, flagnames, comment)
|
||||
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)
|
||||
def to_dictionary(self):
|
||||
fields = {}
|
||||
for f in self.fields:
|
||||
fields[f] = self.fields[f].to_dictionary()
|
||||
d = {"color":self.color, "flagnames":self.flagnames,
|
||||
"comment":self.comment}
|
||||
"comment":self.comment, "fields":fields}
|
||||
if self.size:
|
||||
d["size"] = self.size
|
||||
return d
|
||||
|
@ -93,8 +179,11 @@ class EntityClass:
|
|||
def parse_vector(cls, script):
|
||||
if script.getToken() != "(":
|
||||
script.error("Missing (")
|
||||
v = (float(script.getToken()), float(script.getToken()),
|
||||
float(script.getToken()))
|
||||
s = script.getToken(), script.getToken(), script.getToken()
|
||||
try:
|
||||
v = (float(s[0]), float(s[1]), float(s[2]))
|
||||
except ValueError:
|
||||
v = s
|
||||
if script.getToken() != ")":
|
||||
script.error("Missing )")
|
||||
return v
|
||||
|
@ -118,7 +207,6 @@ class EntityClass:
|
|||
if len(flagnames) < MAX_FLAGS:
|
||||
flagnames.append(script.token)
|
||||
return tuple(flagnames)
|
||||
@classmethod
|
||||
def extract_comment(cls, script):
|
||||
if not script.tokenAvailable(True):
|
||||
return ""
|
||||
|
@ -203,3 +291,35 @@ class EntityClassDict:
|
|||
self.entity_classes = {}
|
||||
for k in ec.keys():
|
||||
self.entity_classes[k] = EntityClass.from_dictionary(k, ec[k])
|
||||
|
||||
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()
|
||||
|
|
|
@ -26,17 +26,19 @@ class ScriptError(Exception):
|
|||
|
||||
class Script:
|
||||
def __init__(self, filename, text, single="{}()':", quotes=True):
|
||||
self.filename = filename
|
||||
if text[0:3] == "\xef\xbb\xbf":
|
||||
text = text[3:]
|
||||
elif text[0] == u"\ufeff":
|
||||
text = text[1:]
|
||||
self.token = ""
|
||||
self.unget = False
|
||||
self.text = text
|
||||
self.pos = 0
|
||||
self.filename = filename
|
||||
self.line = 1
|
||||
self.no_quote_lines = False
|
||||
self.single = single
|
||||
self.quotes = quotes
|
||||
self.pos = 0
|
||||
self.line = 1
|
||||
self.unget = False
|
||||
def error(self, msg):
|
||||
raise ScriptError(self.filename, self.line, msg)
|
||||
def tokenAvailable(self, crossline=False):
|
||||
|
@ -102,6 +104,9 @@ class Script:
|
|||
self.error("EOF inside quoted string")
|
||||
return None
|
||||
if self.text[self.pos] == "\n":
|
||||
if self.no_quote_lines:
|
||||
self.error("EOL inside quoted string")
|
||||
return None
|
||||
self.line += 1
|
||||
self.pos += 1
|
||||
self.token = self.text[start:self.pos]
|
||||
|
|
Loading…
Reference in a new issue