mirror of
https://git.code.sf.net/p/quake/quakeforge
synced 2024-11-10 15:22:04 +00:00
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:
parent
97c99de581
commit
15a906aadb
5 changed files with 417 additions and 5 deletions
154
tools/io_qfmap/__init__.py
Normal file
154
tools/io_qfmap/__init__.py
Normal 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()
|
|
@ -1,5 +1,5 @@
|
|||
# vim:ts=4:et
|
||||
from script import Script
|
||||
from .script import Script
|
||||
|
||||
MAX_FLAGS = 8
|
||||
|
||||
|
|
42
tools/io_qfmap/import_map.py
Normal file
42
tools/io_qfmap/import_map.py
Normal 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
216
tools/io_qfmap/map.py
Normal 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)
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue