#!/usr/bin/env python3
import sys
from io import BytesIO
from collections import namedtuple
from struct import unpack, unpack_from, iter_unpack
from pprint import pprint

b3Dfile = namedtuple("b3dfile","""
    version
    pointCount
    planeCount
    radius
    unknown1
    unknown2
    planeDataOffset
    objectListOffset
    objectCount
    unknown3
    unknown4
    unknown5
    pointListOffset
    normalListOffset
    unknown6
    planeListOffset
""")

Point = namedtuple("Point", "x y z")
Plane = namedtuple("Plane", "unknown1 texture unknown2 points normal data")
PlanePoint = namedtuple("PlanePoint", "id u v")

def dataSlice(data, offset, length):
    return data[offset:offset+length]

def readPoints(hdr, data):
    for point in iter_unpack("<3i", dataSlice(data, hdr.pointListOffset, hdr.pointCount * 12)):
        yield Point(*point)

def readPlanes(hdr, data):
    planedata = (x[0] for x in iter_unpack("24s", dataSlice(data, hdr.planeListOffset, hdr.planeCount * 24)))
    normals   = (x[0] for x in iter_unpack("24s", dataSlice(data, hdr.planeListOffset, hdr.planeCount * 24)))
    data = BytesIO(data[hdr.planeListOffset:])
    for plane, normal in zip(planedata, normals):
        planePointCount, unknown1, texture, unknown2 = unpack("<2BH6s", data.read(10))
        planePoints = [PlanePoint(int(point[0]/12), *point[1:])
            for point in iter_unpack("<IHH", data.read(planePointCount * 8))]
        yield Plane(unknown1, texture, unknown2, planePoints, normal, plane)


#http://www.uesp.net/wiki/Daggerfall:ARCH3D.BSA + minor modifications?
#Notably the plane header is 10 bytes, not 8
#And textures are applied by level files, not
#by any property of the model
class b3DFile():
    def __init__(self, data):
        self.hdr = b3Dfile(*unpack_from("<4s15I", data, 0))
        self.data = data
        self.points = [point for point in readPoints(self.hdr, self.data)]
        self.planes = [plane for plane in readPlanes(self.hdr, self.data)]

def printPly(obj):
    print("""ply
format ascii 1.0
element vertex %d
property int x
property int y
property int z
element face %d
property list uchar int vertex_index
end_header""" % (len(obj.points), len(obj.planes)))

    for point in obj.points:
        print("%d %d %d" % point)
    for plane in obj.planes:
        print("%d %s" % (len(plane.points), " ".join([str(x.id) for x in plane.points])))

def printObj(obj):
    for point in obj.points:
        print("v", *[x for x in point])
    for plane in obj.planes:
        print("f", *[x.id+1 for x in plane.points])

data = b''
with open(sys.argv[1], "rb") as fd:
    data = fd.read()

printPly(b3DFile(data))

@@ -620,84 +620,8 @@
 %22)%0A%0A
-def dataSlice(data, offset, length):%0A    return data%5Boffset:offset+length%5D%0A%0A
 def 
@@ -679,36 +679,25 @@
 (%22%3C3i%22, data
-Slice(data, 
+%5B
 hdr.pointLis
@@ -699,26 +699,28 @@
 ntListOffset
-, 
+:%5D%5B:
 hdr.pointCou
@@ -726,17 +726,17 @@
 unt * 12
-)
+%5D
 ):%0A     
@@ -836,36 +836,25 @@
 (%2224s%22, data
-Slice(data, 
+%5B
 hdr.planeLis
@@ -846,36 +846,38 @@
 ta%5Bhdr.plane
-List
+Data
 Offset
-, 
+:%5D%5B:
 hdr.planeCou
@@ -879,25 +879,25 @@
 neCount * 24
-)
+%5D
 ))%0A    norma
@@ -900,28 +900,33 @@
 ormals   = (
-x%5B0%5D
+Point(*x)
  for x in it
@@ -940,39 +940,29 @@
 ck(%22
-24s
+%3C3i
 %22, data
-Slice(data, hdr.plane
+%5Bhdr.normal
 List
@@ -967,18 +967,20 @@
 stOffset
-, 
+:%5D%5B:
 hdr.plan
@@ -990,17 +990,17 @@
 unt * 24
-)
+%5D
 ))%0A    d
@@ -2453,19 +2453,8 @@
 %5D)%0A%0A
-data = b''%0A
 with
@@ -2496,16 +2496,27 @@
  data = 
+memoryview(
 fd.read(
@@ -2520,19 +2520,17 @@
 ad()
+)
 %0A%0A
-printPly(
+b3d = 
 b3DF
@@ -2538,10 +2538,24 @@
 le(data)
+%0A%0AprintPly(b3d
 )%0A

Use memoryview and generally be better