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.
This commit is contained in:
Bill Currie 2012-08-30 13:19:30 +09:00
parent 97c99de581
commit 15a906aadb
5 changed files with 417 additions and 5 deletions

154
tools/io_qfmap/__init__.py Normal file
View file

@ -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
# <pep8 compliant>
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()

View file

@ -1,5 +1,5 @@
# vim:ts=4:et
from script import Script
from .script import Script
MAX_FLAGS = 8

View file

@ -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 #####
# <pep8 compliant>
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'}

216
tools/io_qfmap/map.py Normal file
View file

@ -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)

View file

@ -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