From 15a906aadb6232337e012ca4a18ee7a1656626a2 Mon Sep 17 00:00:00 2001 From: Bill Currie Date: Thu, 30 Aug 2012 13:19:30 +0900 Subject: [PATCH] Get map parsing pretty much working. No geometry is created yet. Both id and quest formats are supported. e1m1.map parses in less than two seconds on my system. --- tools/io_qfmap/__init__.py | 154 ++++++++++++++++++++++++ tools/io_qfmap/entityclass.py | 2 +- tools/io_qfmap/import_map.py | 42 +++++++ tools/io_qfmap/map.py | 216 ++++++++++++++++++++++++++++++++++ tools/io_qfmap/script.py | 8 +- 5 files changed, 417 insertions(+), 5 deletions(-) create mode 100644 tools/io_qfmap/__init__.py create mode 100644 tools/io_qfmap/import_map.py create mode 100644 tools/io_qfmap/map.py diff --git a/tools/io_qfmap/__init__.py b/tools/io_qfmap/__init__.py new file mode 100644 index 000000000..6ae62e975 --- /dev/null +++ b/tools/io_qfmap/__init__.py @@ -0,0 +1,154 @@ +# vim:ts=4:et +# ##### 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 ##### + +# copied from io_scene_obj + +# + +bl_info = { + "name": "Quake map format", + "author": "Bill Currie", + "blender": (2, 6, 3), + "api": 35622, + "location": "File > Import-Export", + "description": "Import-Export Quake maps", + "warning": "not even alpha", + "wiki_url": "", + "tracker_url": "", +# "support": 'OFFICIAL', + "category": "Import-Export"} + +# To support reload properly, try to access a package var, if it's there, +# reload everything +if "bpy" in locals(): + import imp + if "import_map" in locals(): + imp.reload(import_map) + if "export_map" in locals(): + imp.reload(export_map) + + +import bpy +from bpy.props import BoolProperty, FloatProperty, StringProperty, EnumProperty +from bpy.props import FloatVectorProperty, PointerProperty +from bpy_extras.io_utils import ExportHelper, ImportHelper, path_reference_mode, axis_conversion + +from . import import_map +#from . import export_map + +SYNCTYPE=( + ('ST_SYNC', "Syncronized", "Automatic animations are all together"), + ('ST_RAND', "Random", "Automatic animations have random offsets"), +) + +EFFECTS=( + ('EF_NONE', "None", "No effects"), + ('EF_ROCKET', "Rocket", "Leave a rocket trail"), + ('EF_GRENADE', "Grenade", "Leave a grenade trail"), + ('EF_GIB', "Gib", "Leave a trail of blood"), + ('EF_TRACER', "Tracer", "Green split trail"), + ('EF_ZOMGIB', "Zombie Gib", "Leave a smaller blood trail"), + ('EF_TRACER2', "Tracer 2", "Orange split trail + rotate"), + ('EF_TRACER3', "Tracer 3", "Purple split trail"), +) + +class QFMDLSettings(bpy.types.PropertyGroup): + eyeposition = FloatVectorProperty( + name="Eye Position", + description="View possion relative to object origin") + synctype = EnumProperty( + items=SYNCTYPE, + name="Sync Type", + description="Add random time offset for automatic animations") + rotate = BoolProperty( + name="Rotate", + description="Rotate automatically (for pickup items)") + effects = EnumProperty( + items=EFFECTS, + name="Effects", + description="Particle trail effects") + #doesn't work :( + #script = PointerProperty( + # type=bpy.types.Object, + # name="Script", + # description="Script for animating frames and skins") + script = StringProperty( + name="Script", + description="Script for animating frames and skins") + xform = BoolProperty( + name="Auto transform", + description="Auto-apply location/rotation/scale when exporting", + default=True) + md16 = BoolProperty( + name="16-bit", + description="16 bit vertex coordinates: QuakeForge only") + +class ImportMDL6(bpy.types.Operator, ImportHelper): + '''Load a Quake map File''' + bl_idname = "import_mesh.quake_map" + bl_label = "Import map" + + filename_ext = ".map" + filter_glob = StringProperty(default="*.map", options={'HIDDEN'}) + + def execute(self, context): + keywords = self.as_keywords (ignore=("filter_glob",)) + return import_map.import_map(self, context, **keywords) + +class ExportMDL6(bpy.types.Operator, ExportHelper): + '''Save a Quake map File''' + + bl_idname = "export_mesh.quake_map" + bl_label = "Export map" + + filename_ext = ".map" + filter_glob = StringProperty(default="*.map", options={'HIDDEN'}) + + @classmethod + def poll(cls, context): + return (context.active_object != None + and type(context.active_object.data) == bpy.types.Mesh) + + def execute(self, context): + keywords = self.as_keywords (ignore=("check_existing", "filter_glob")) + return export_map.export_map(self, context, **keywords) + +def menu_func_import(self, context): + self.layout.operator(ImportMDL6.bl_idname, text="Quake map (.map)") + + +def menu_func_export(self, context): + self.layout.operator(ExportMDL6.bl_idname, text="Quake map (.map)") + + +def register(): + bpy.utils.register_module(__name__) + + bpy.types.INFO_MT_file_import.append(menu_func_import) + bpy.types.INFO_MT_file_export.append(menu_func_export) + + +def unregister(): + bpy.utils.unregister_module(__name__) + + bpy.types.INFO_MT_file_import.remove(menu_func_import) + bpy.types.INFO_MT_file_export.remove(menu_func_export) + +if __name__ == "__main__": + register() diff --git a/tools/io_qfmap/entityclass.py b/tools/io_qfmap/entityclass.py index fc604876f..bcf235074 100644 --- a/tools/io_qfmap/entityclass.py +++ b/tools/io_qfmap/entityclass.py @@ -1,5 +1,5 @@ # vim:ts=4:et -from script import Script +from .script import Script MAX_FLAGS = 8 diff --git a/tools/io_qfmap/import_map.py b/tools/io_qfmap/import_map.py new file mode 100644 index 000000000..cc4fa9d4f --- /dev/null +++ b/tools/io_qfmap/import_map.py @@ -0,0 +1,42 @@ +# vim:ts=4:et +# ##### 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 ##### + +# + +import bpy +from bpy_extras.object_utils import object_data_add +from mathutils import Vector,Matrix + +from .map import parse_map, MapError + +def import_map(operator, context, filepath): + bpy.context.user_preferences.edit.use_global_undo = False + + for obj in bpy.context.scene.objects: + obj.select = False + + try: + entities = parse_map (filepath) + except MapError as err: + raise + operator.report({'ERROR'}, repr(err)) + return {'CANCELLED'} + if not entities: + return {'FINISHED'} + return {'FINISHED'} diff --git a/tools/io_qfmap/map.py b/tools/io_qfmap/map.py new file mode 100644 index 000000000..0c8e0b4f9 --- /dev/null +++ b/tools/io_qfmap/map.py @@ -0,0 +1,216 @@ +# vim:ts=4:et + +from mathutils import Vector + +from .script import Script + +class Entity: + def __init__(self): + self.d = {} + self.b = [] + pass + +class Texinfo: + def __init__(self, script, plane): + self.name = script.getToken() + script.getToken() + if script.token == "[": + hldef = True + self.s_vec = parse_vector(script) + self.s_offs = float(script.getToken()) + if script.getToken() != "]": + map_error(script, "Missing ]") + if script.getToken() != "[": + map_error(script, "Missing [") + self.t_vec = parse_vector(script) + self.t_offs = float(script.getToken()) + if script.getToken() != "]": + map_error(script, "Missing ]") + else: + hldef = False + self.s_vec, self.t_vec = texture_axis_from_plane(plane) + self.s_offs = float(script.token) + self.t_offs = float(script.getToken()) + self.rotate = float(script.getToken()) + self.scale = [0, 0] + self.scale[0] = float(script.getToken()) + self.scale[1] = float(script.getToken()) + +baseaxis = ( + (Vector((0,0, 1)), (Vector((1,0,0)), Vector((0,-1,0)))), #floor + (Vector((0,0,-1)), (Vector((1,0,0)), Vector((0,-1,0)))), #ceiling + (Vector(( 1,0,0)), (Vector((0,1,0)), Vector((0,0,-1)))), #west wall + (Vector((-1,0,0)), (Vector((0,1,0)), Vector((0,0,-1)))), #east wall + (Vector((0, 1,0)), (Vector((1,0,0)), Vector((0,0,-1)))), #south wall + (Vector((0,-1,0)), (Vector((1,0,0)), Vector((0,0,-1)))) #north wall +) + +def texture_axis_from_plane(plane): + best = 0 + bestaxis = 0 + for i in range(6): + dot = plane[0].dot(baseaxis[i][0]) + if dot > best: + best = dot + bestaxis = i + return baseaxis[bestaxis][1] + +def clip_poly(poly, plane, keepon): + new_poly = [] + last_dist = poly[-1].dot(plane[0]) - plane[1] + last_point = poly[-1] + for point in poly: + dist = point.dot(plane[0]) - plane[1] + if dist * last_dist < -1e-6: + #crossed the plane + frac = last_dist / (last_dist - dist) + new_poly.append(last_point + frac * (point - last_point)) + if dist < -1e-6 or (dist < 1e-6 and keepon): + new_poly.append(point) + last_point = point + last_dist = dist + return new_poly + +def clip_plane(plane, clip_planes): + s, t = texture_axis_from_plane(plane) + t = plane[0].cross(s) + t.normalize() + s = t.cross(plane[0]) + s *= 1e4 + t *= 1e4 + o = plane[0] * plane[1] + poly = [o + s + t, o + s - t, o - s - t, o - s + t] #CW + for p in clip_planes: + poly = clip_poly(poly, p, True) + return poly + +def convert_planes(planes): + verts = [] + faces = [] + for i in range(len(planes)): + poly = clip_plane(planes[i], planes[:i] + planes[i + 1:]) + face = [] + for v in poly: + ind = len(verts) + for i in range(len(verts)): + d = verts[i] - v + if d.dot(d) < 1e-6: + ind = i + break + if ind == len(verts): + verts.append(v) + face.append(ind) + faces.append(face) + return verts, faces + +def parse_vector(script): + v = (float(script.getToken()), float(script.getToken()), + float(script.getToken())) + return v + +def parse_verts(script): + if script.token != ":": + map_error(script, "Missing :") + script.getToken() + numverts = int(script.token) + verts = [] + for i in range(numverts): + script.tokenAvailable(True) + verts.append(parse_vector(script)) + return verts + +def parse_brush(script, mapent): + verts = [] + faces = [] + planes = [] + texdefs = [] + planepts = [None] * 3 + if script.getToken(True) != "(": + verts = parse_verts(script) + else: + script.ungetToken() + while True: + if script.getToken(True) in [None, "}"]: + break + if verts: + n_v = int(script.token) + face = [None] * n_v + if script.getToken() != "(": + map_error(script, "Missing (") + for i in range(n_v): + script.getToken() + face[i] = int(script.token) + if i < 3: + planepts[i] = Vector(verts[face[i]]) + if script.getToken() != ")": + map_error(script, "Missing )") + faces.append(face) + else: + for i in range(3): + if i != 0: + script.getToken(True) + if script.token != "(": + map_error(script, "Missing (") + planepts[i] = Vector(parse_vector(script)) + script.getToken() + if script.token != ")": + map_error(script, "Missing )") + t1 = planepts[0] - planepts[1] + t2 = planepts[2] - planepts[1] + norm = t1.cross(t2) + norm.normalize() + plane = (norm, planepts[1].dot(norm)) + planes.append(plane) + tx = Texinfo(script, plane) + detail = False + while script.tokenAvailable(): + script.getToken() + if script.token == "detail": + detail = True + else: + map_error(script, "invalid flag") + if not verts: + verts, faces = convert_planes(planes) + mapent.b.append((verts,faces)) + +def parse_epair(script, mapent): + key = script.token + script.getToken() + value = script.token + mapent.d[key] = value + +def parse_entity(script): + if script.getToken(True) == None: + return False + if script.token != "{": + map_error(script, "Missing {") + mapent = Entity() + while True: + if script.getToken(True) == None: + map_error(script, "EOF without closing brace") + if script.token == "}": + break + if script.token == "{": + parse_brush(script, mapent) + else: + parse_epair(script, mapent) + return mapent + +class MapError(Exception): + def __init__(self, fname, line, message): + Exception.__init__(self, "%s:%d: %s" % (fname, line, message)) + self.line = line + +def map_error(self, msg): + raise MapError(self.filename, self.line, msg) + +def parse_map(filename): + text = open(filename, "rt").read() + script = Script(filename, text) + script.error = map_error + entities = [] + while True: + ent = parse_entity(script) + if not ent: + break + entities.append(ent) diff --git a/tools/io_qfmap/script.py b/tools/io_qfmap/script.py index 7c2ebaa85..788e23bf3 100644 --- a/tools/io_qfmap/script.py +++ b/tools/io_qfmap/script.py @@ -12,7 +12,7 @@ class Script: if self.unget: return True while self.pos < len(self.text): - while self.text[self.pos].isspace(): + while self.pos < len(self.text) and self.text[self.pos].isspace(): if self.text[self.pos] == "\n": if not crossline: return False @@ -41,16 +41,16 @@ class Script: return self.token if not self.tokenAvailable(crossline): if not crossline: - self.error("line is incomplete") + self.error(self, "line is incomplete") return None if self.text[self.pos] == "\"": self.pos += 1 start = self.pos if self.text[self.pos] == len(self.text): - self.error("EOF inside quoted string") + self.error(self, "EOF inside quoted string") while self.text[self.pos] != "\"": if self.pos == len(self.text): - self.error("EOF inside quoted string") + self.error(self, "EOF inside quoted string") return None if self.text[self.pos] == "\n": self.line += 1