Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
Like my 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 =
self.num_files = struct.unpack("<l",[0]
self.points = []
file_defs = []
for _ in range(self.num_files):
lump =
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:
self.levels.append(Level([:-4], io.BytesIO(
#Skip over file because it's not a map
#print("Skipping %s (%d bytes)" % (, 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)
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): = name
self.version = struct.unpack("<l",[0]
self.x, self.y, self.z = struct.unpack("<lll",
self.ang, self.cursectnum = struct.unpack("<hh",
self.sectors = []
numsectors = struct.unpack("<h",[0]
for _ in range(numsectors):
self.points = []
numpoints = struct.unpack("<h",[0]
for _ in range(numpoints):
numsprites = struct.unpack("<h",[0]*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))
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))
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])))
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('.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))
dwg.add(dwg.line(a, b, stroke='#999', stroke_width=3))
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.version))
print('You need to pass a GRP file as the only argument')

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