Skip to content

Instantly share code, notes, and snippets.

@jasonsperske
Last active August 24, 2024 20:54
Show Gist options
  • Save jasonsperske/31324a0cdffd8edf9683 to your computer and use it in GitHub Desktop.
Save jasonsperske/31324a0cdffd8edf9683 to your computer and use it in GitHub Desktop.
Like my WADParser.py but for Duke Nukem 3D MAP files
#!/usr/bin/env python3
import struct
import re
import io
from collections import namedtuple
GrpFileDefinition = namedtuple('GrpFileDefinition', 'name size')
Line = namedtuple('Line', 'a b is_one_sided')
class Grp(object):
"""Encapsulates the data found inside a GRP file"""
def __init__(self, grpFile):
"""Each GRP files contains the contents of several files"""
self.levels = []
with open(grpFile, "rb") as f:
header_size = 12
self.ken = f.read(12)
self.num_files = struct.unpack("<l", f.read(4))[0]
self.points = []
file_defs = []
for _ in range(self.num_files):
lump = f.read(16)
file_name = lump[0:12].decode('UTF-8').rstrip('\0')
file_size = struct.unpack("<l", lump[12:16])[0]
file_defs.append(GrpFileDefinition(file_name, file_size))
for file_def in file_defs:
if(re.match('E\dL\d+\.MAP', file_def.name)):
self.levels.append(Level(file_def.name[:-4], io.BytesIO(f.read(file_def.size))))
else:
#Skip over file because it's not a map
#print("Skipping %s (%d bytes)" % (file_def.name, file_def.size))
f.read(file_def.size)
class Sector(object):
def __init__(self, data):
self.wallptr, self.wallnum = struct.unpack("<hh", data[0:4])
def lines(self, level):
for index in range(self.wallptr, self.wallptr+self.wallnum):
a = level.points[index]
b = level.points[a.point2]
yield Line(a, b, a.nextsector == -1)
return
class Point(object):
def __init__(self, data):
self.x, self.y = struct.unpack("<ll", data[0:8])
self.point2, self.nextwall, self.nextsector = struct.unpack("<hhh", data[8:14])
class Level(object):
def __init__(self, name, f):
self.name = name
self.version = struct.unpack("<l", f.read(4))[0]
self.x, self.y, self.z = struct.unpack("<lll", f.read(12))
self.ang, self.cursectnum = struct.unpack("<hh", f.read(4))
self.sectors = []
numsectors = struct.unpack("<h", f.read(2))[0]
for _ in range(numsectors):
self.sectors.append(Sector(f.read(40)))
self.points = []
numpoints = struct.unpack("<h", f.read(2))[0]
for _ in range(numpoints):
self.points.append(Point(f.read(32)))
numsprites = struct.unpack("<h", f.read(2))[0]
f.read(numsprites*44) #Skip over sprites (not needed for mapping)
self.lower_left = None
self.upper_right = None
for sector in self.sectors:
for line in sector.lines(self):
if self.lower_left is None:
self.lower_left = (min(line.a.x, line.b.x), min(line.a.y, line.b.y))
else:
self.lower_left = (min(self.lower_left[0], line.a.x, line.b.x), min(self.lower_left[1], line.a.y, line.b.y))
if self.upper_right is None:
self.upper_right = (max(line.a.x, line.b.x), max(line.a.y, line.b.y))
else:
self.upper_right = (max(self.upper_right[0], line.a.x, line.b.x), max(self.upper_right[1], line.a.y, line.b.y))
self.shift = (100-self.lower_left[0],100-self.lower_left[1])
# Scale the drawing to fit inside a 1024x1024 canvas (iPhones don't like really large SVGs even if they have the same detail)
self.view_box_size = ((self.shift[0]+self.upper_right[0]+200),(self.shift[1]+self.upper_right[1]+200))
if self.view_box_size[0] > self.view_box_size[1]:
self.canvas_size = (1024, int(1024*(float(self.view_box_size[1])/self.view_box_size[0])))
else:
self.canvas_size = (int(1024*(float(self.view_box_size[0])/self.view_box_size[1])), 1024)
self.scale_x = 1/8
self.scale_y = 1/8
self.view_box_size = (int(self.view_box_size[0]*self.scale_x), int(self.view_box_size[1]*self.scale_y))
def normalize(self, point):
return (int((self.shift[0]+point[0])*self.scale_x), int((self.shift[1]+point[1])*self.scale_y))
def save_svg(self):
import svgwrite
dwg = svgwrite.Drawing(self.name+'.svg', profile='tiny', size=self.canvas_size, viewBox=('0 0 %d %d' % self.view_box_size))
for sector in self.sectors:
for line in sector.lines(self):
a = self.normalize((line.a.x, line.a.y))
b = self.normalize((line.b.x, line.b.y))
if line.is_one_sided:
dwg.add(dwg.line(a, b, stroke='#333', stroke_width=10))
else:
dwg.add(dwg.line(a, b, stroke='#999', stroke_width=3))
dwg.save()
if __name__ == "__main__":
import sys
if len(sys.argv) > 1:
grp = Grp(sys.argv[1])
for level in grp.levels:
print("Saving %s.svg (map ver. %d)" % (level.name, level.version))
level.save_svg()
else:
print('You need to pass a GRP file as the only argument')
@jasonsperske
Copy link
Author

Fixed with help from Ken Silverman himself

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment