/*
** 2011 April 5
**
** The author disclaims copyright to this source code. In place of
** a legal notice, here is a blessing:
** May you do good and not evil.
** May you find forgiveness for yourself and forgive others.
** May you share freely, never taking more than you give.
*/
package info.ata4.bsplib;
import info.ata4.bsplib.app.SourceApp;
import info.ata4.bsplib.app.SourceAppDB;
import info.ata4.bsplib.app.SourceAppID;
import static info.ata4.bsplib.app.SourceAppID.*;
import info.ata4.bsplib.entity.Entity;
import info.ata4.bsplib.io.EntityInputStream;
import info.ata4.bsplib.lump.*;
import info.ata4.bsplib.struct.*;
import info.ata4.io.DataReader;
import info.ata4.io.DataReaders;
import static info.ata4.io.Seekable.Origin.CURRENT;
import info.ata4.log.LogUtils;
import info.ata4.util.EnumConverter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* All-purpose BSP file and lump reader.
*
* @author Nico Bergemann <barracuda415 at yahoo.de>
*/
public class BspFileReader {
private static final Logger L = LogUtils.getLogger();
// BSP headers and data
private final BspFile bspFile;
private final BspData bspData;
private int appID;
// statistical stuff
private Set<String> entityClasses = new TreeSet<>();
public BspFileReader(BspFile bspFile, BspData bspData) throws IOException {
this.bspFile = bspFile;
this.bspData = bspData;
this.appID = bspFile.getSourceApp().getAppID();
if (bspFile.getFile() == null) {
// "Gah! Hear me, man? Gah!"
throw new BspException("BSP file is unloaded");
}
// uncompress all lumps first
if (bspFile.isCompressed()) {
bspFile.uncompress();
}
}
public BspFileReader(BspFile bspFile) throws IOException {
this(bspFile, new BspData());
}
/**
* Loads all supported lumps
*/
public void loadAll() {
loadEntities();
loadVertices();
loadEdges();
loadFaces();
loadOriginalFaces();
loadModels();
loadSurfaceEdges();
loadOccluders();
loadTexInfo();
loadTexData();
loadStaticProps();
loadCubemaps();
loadPlanes();
loadBrushes();
loadBrushSides();
loadAreaportals();
loadClipPortalVertices();
loadDispInfos();
loadDispVertices();
loadDispTriangleTags();
loadDispMultiBlend();
loadNodes();
loadLeaves();
loadLeafFaces();
loadLeafBrushes();
loadOverlays();
loadFlags();
}
public void loadPlanes() {
if (bspData.planes != null) {
return;
}
bspData.planes = loadLump(LumpType.LUMP_PLANES, DPlane.class);
}
public void loadBrushes() {
if (bspData.brushes != null) {
return;
}
bspData.brushes = loadLump(LumpType.LUMP_BRUSHES, DBrush.class);
}
public void loadBrushSides() {
if (bspData.brushSides != null) {
return;
}
Class struct = DBrushSide.class;
if (appID == VINDICTUS) {
struct = DBrushSideVin.class;
} else if (bspFile.getVersion() >= 21 && appID != LEFT_4_DEAD_2) {
// newer BSP files have a slightly different struct that is still reported
// as version 0
struct = DBrushSideV2.class;
}
bspData.brushSides = loadLump(LumpType.LUMP_BRUSHSIDES, struct);
}
public void loadVertices() {
if (bspData.verts != null) {
return;
}
bspData.verts = loadLump(LumpType.LUMP_VERTEXES, DVertex.class);
}
public void loadClipPortalVertices() {
if (bspData.clipPortalVerts != null) {
return;
}
bspData.clipPortalVerts = loadLump(LumpType.LUMP_CLIPPORTALVERTS, DVertex.class);
}
public void loadEdges() {
if (bspData.edges != null) {
return;
}
Class struct = DEdge.class;
if (appID == VINDICTUS) {
struct = DEdgeVin.class;
}
bspData.edges = loadLump(LumpType.LUMP_EDGES, struct);
}
private void loadFaces(boolean orig) {
if ((orig && bspData.origFaces != null) || (!orig && bspData.faces != null)) {
return;
}
Class struct = DFace.class;
switch (appID) {
case VAMPIRE_BLOODLINES:
struct = DFaceVTMB.class;
break;
case VINDICTUS:
LumpType lt = orig ? LumpType.LUMP_ORIGINALFACES : LumpType.LUMP_FACES;
int facesver = getLump(lt).getVersion();
if (facesver == 2) {
struct = DFaceVinV2.class;
} else {
struct = DFaceVinV1.class;
} break;
default:
switch (bspFile.getVersion()) {
case 17:
struct = DFaceBSP17.class;
break;
case 18:
struct = DFaceBSP18.class;
break;
} break;
}
if (orig) {
bspData.origFaces = loadLump(LumpType.LUMP_ORIGINALFACES, struct);
} else {
// use LUMP_FACES_HDR if LUMP_FACES is empty
if (getLump(LumpType.LUMP_FACES).getLength() == 0) {
bspData.faces = loadLump(LumpType.LUMP_FACES_HDR, struct);
} else {
bspData.faces = loadLump(LumpType.LUMP_FACES, struct);
}
}
}
public void loadFaces() {
loadFaces(false);
}
public void loadOriginalFaces() {
loadFaces(true);
}
public void loadModels() {
if (bspData.models != null) {
return;
}
Class struct = DModel.class;
if (appID == DARK_MESSIAH) {
struct = DModelDM.class;
}
bspData.models = loadLump(LumpType.LUMP_MODELS, struct);
}
public void loadSurfaceEdges() {
if (bspData.surfEdges != null) {
return;
}
bspData.surfEdges = loadIntegerLump(LumpType.LUMP_SURFEDGES);
}
public void loadStaticProps() {
if (bspData.staticProps != null && bspData.staticPropName != null) {
return;
}
L.fine("Loading static props");
GameLump sprpLump = bspFile.getGameLump("sprp");
if (sprpLump == null) {
// static prop lump not available
bspData.staticProps = new ArrayList<>();
return;
}
DataReader in = DataReaders.forByteBuffer(sprpLump.getBuffer());
int sprpver = sprpLump.getVersion();
try {
final int padsize = 128;
final int psnames = in.readInt();
L.log(Level.FINE, "Static prop names: {0}", psnames);
bspData.staticPropName = new ArrayList<>(psnames);
for (int i = 0; i < psnames; i++) {
bspData.staticPropName.add(in.readStringFixed(padsize));
}
// model path strings in Zeno Clash
if (appID == ZENO_CLASH) {
int psextra = in.readInt();
in.seek(psextra * padsize, CURRENT);
}
// StaticPropLeafLump_t
final int propleaves = in.readInt();
L.log(Level.FINE, "Static prop leaves: {0}", propleaves);
bspData.staticPropLeaf = new ArrayList<>(propleaves);
for (int i = 0; i < propleaves; i++) {
bspData.staticPropLeaf.add(in.readUnsignedShort());
}
// extra data for Vindictus
if (appID == VINDICTUS && sprpver == 6) {
int psextra = in.readInt();
in.seek(psextra * 16, CURRENT);
}
// StaticPropLump_t
final int propStaticCount = in.readInt();
// don't try to read static props if there are none
if (propStaticCount == 0) {
bspData.staticProps = Collections.emptyList();
return;
}
// calculate static prop struct size
final int propStaticSize = (int) in.remaining() / propStaticCount;
Class<? extends DStaticProp> structClass = null;
// special cases where derivative lump structures are used
switch (appID) {
case THE_SHIP:
if (propStaticSize == 188) {
structClass = DStaticPropV5Ship.class;
}
break;
case BLOODY_GOOD_TIME:
if (propStaticSize == 192) {
structClass = DStaticPropV6BGT.class;
}
break;
case ZENO_CLASH:
if (propStaticSize == 68) {
structClass = DStaticPropV7ZC.class;
}
break;
case DARK_MESSIAH:
if (propStaticSize == 136) {
structClass = DStaticPropV6DM.class;
}
break;
case DEAR_ESTHER:
if (propStaticSize == 76) {
structClass = DStaticPropV9DE.class;
}
break;
case VINDICTUS:
// newer maps report v6 even though the size is still 60, so
// force v5 in all cases
if (propStaticSize == 60) {
structClass = DStaticPropV5.class;
}
break;
case LEFT_4_DEAD:
// old L4D maps use v7 that is incompatible to the newer
// Source 2013 v7
if (sprpver == 7 && propStaticSize == 68) {
structClass = DStaticPropV7L4D.class;
}
break;
case TEAM_FORTRESS_2:
// there's been a short period where TF2 used v7, which later
// became v10 in all Source 2013 game
if (sprpver == 7 && propStaticSize == 72) {
structClass = DStaticPropV10.class;
}
break;
case COUNTER_STRIKE_GO:
// custom v10 for CS:GO, not compatible with Source 2013 v10
if (sprpver == 10) {
structClass = DStaticPropV10CSGO.class;
}
break;
}
// get structure class for the static prop lump version if it's not
// a special case
if (structClass == null) {
try {
String className = DStaticProp.class.getName();
structClass = (Class<? extends DStaticProp>) Class.forName(className + "V" + sprpver);
} catch (ClassNotFoundException ex) {
L.log(Level.WARNING, "Couldn''t find static prop struct for version {0}", sprpver);
structClass = null;
}
}
// check if the size is correct
if (structClass != null) {
int propStaticSizeActual = structClass.newInstance().getSize();
if (propStaticSizeActual != propStaticSize) {
L.log(Level.WARNING, "Static prop struct size mismatch: expected {0}, got {1} (using {2})",
new Object[]{propStaticSize, propStaticSizeActual, structClass.getSimpleName()});
structClass = null;
}
}
// if the correct class is still unknown at this point, fall back to
// a very basic version that should hopefully work in all situations
int numFillBytes = 0;
if (structClass == null) {
L.log(Level.WARNING, "Falling back to static prop v4");
structClass = DStaticPropV4.class;
numFillBytes = propStaticSize - 56;
}
bspData.staticProps = new ArrayList<>(propStaticCount);
for (int i = 0; i < propStaticCount; i++) {
DStaticProp sp = structClass.newInstance();
sp.read(in);
if (numFillBytes > 0) {
in.seek(numFillBytes, CURRENT);
}
bspData.staticProps.add(sp);
}
L.log(Level.FINE, "Static props: {0}", propStaticCount);
checkRemaining(in);
} catch (IOException ex) {
lumpError(sprpLump, ex);
} catch (InstantiationException | IllegalAccessException ex) {
L.log(Level.SEVERE, "Lump struct class error", ex);
}
}
public void loadCubemaps() {
if (bspData.cubemaps != null) {
return;
}
bspData.cubemaps = loadLump(LumpType.LUMP_CUBEMAPS, DCubemapSample.class);
}
public void loadDispInfos() {
if (bspData.dispinfos != null) {
return;
}
Class struct = DDispInfo.class;
int bspv = bspFile.getVersion();
// the lump version is useless most of the time, use the AppID instead
switch (appID) {
case VINDICTUS:
struct = DDispInfoVin.class;
break;
case HALF_LIFE_2:
if (bspv == 17) {
struct = DDispInfoBSP17.class;
}
break;
case DOTA_2_BETA:
if (bspv == 22) {
struct = DDispInfoBSP22.class;
} else if (bspv >= 23) {
struct = DDispInfoBSP23.class;
}
break;
}
bspData.dispinfos = loadLump(LumpType.LUMP_DISPINFO, struct);
}
public void loadDispVertices() {
if (bspData.dispverts != null) {
return;
}
bspData.dispverts = loadLump(LumpType.LUMP_DISP_VERTS, DDispVert.class);
}
public void loadDispTriangleTags() {
if (bspData.disptris != null) {
return;
}
bspData.disptris = loadLump(LumpType.LUMP_DISP_TRIS, DDispTri.class);
}
public void loadDispMultiBlend() {
if (bspData.dispmultiblend != null) {
return;
}
bspData.dispmultiblend = loadLump(LumpType.LUMP_DISP_MULTIBLEND, DDispMultiBlend.class);
}
public void loadTexInfo() {
if (bspData.texinfos != null) {
return;
}
Class struct = DTexInfo.class;
if (appID == DARK_MESSIAH) {
struct = DTexInfoDM.class;
}
bspData.texinfos = loadLump(LumpType.LUMP_TEXINFO, struct);
}
public void loadTexData() {
if (bspData.texdatas != null) {
return;
}
bspData.texdatas = loadLump(LumpType.LUMP_TEXDATA, DTexData.class);
loadTexDataStrings(); // load associated texdata strings
}
private void loadTexDataStrings() {
L.log(Level.FINE, "Loading {0}", LumpType.LUMP_TEXDATA_STRING_DATA);
byte[] stringData;
Lump lump = getLump(LumpType.LUMP_TEXDATA_STRING_DATA);
DataReader in = DataReaders.forByteBuffer(lump.getBuffer());
try {
final int tdsds = lump.getLength();
stringData = new byte[tdsds];
in.readBytes(stringData);
checkRemaining(in);
} catch (IOException ex) {
lumpError(lump, ex);
return;
}
L.log(Level.FINE, "Loading {0}", LumpType.LUMP_TEXDATA_STRING_TABLE);
lump = getLump(LumpType.LUMP_TEXDATA_STRING_TABLE);
in = DataReaders.forByteBuffer(lump.getBuffer());
try {
final int size = 4;
final int tdsts = lump.getLength() / size;
bspData.texnames = new ArrayList<>(tdsts);
tdst:
for (int i = 0; i < tdsts; i++) {
int ofs = in.readInt();
int ofsNull;
// find null byte offset
for (ofsNull = ofs; ofsNull < stringData.length; ofsNull++) {
if (stringData[ofsNull] == 0) {
// build string from string data array
bspData.texnames.add(new String(stringData, ofs, ofsNull - ofs));
continue tdst;
}
}
}
L.log(Level.FINE, "Texture data strings: {0}", tdsts);
checkRemaining(in);
} catch (IOException ex) {
lumpError(lump, ex);
}
}
public void loadEntities() {
if (bspData.entities != null) {
return;
}
L.log(Level.FINE, "Loading {0}", LumpType.LUMP_ENTITIES);
Lump lump = getLump(LumpType.LUMP_ENTITIES);
try (EntityInputStream entReader = new EntityInputStream(lump.getInputStream())) {
// allow escaped quotes for VTBM
entReader.setAllowEscSeq(bspFile.getVersion() == 17);
bspData.entities = new ArrayList<>();
entityClasses.clear();
Entity ent;
while ((ent = entReader.readEntity()) != null) {
bspData.entities.add(ent);
entityClasses.add(ent.getClassName());
}
// detect appID with heuristics to handle special BSP formats if it's
// still unknown or undefined at this point
if (appID == UNKNOWN) {
SourceAppDB appDB = SourceAppDB.getInstance();
SourceApp app = appDB.find(bspFile.getName(), bspFile.getVersion(), entityClasses);
bspFile.setSourceApp(app);
appID = app.getAppID();
}
} catch (IOException ex) {
L.log(Level.SEVERE, "Couldn''t read entity lump", ex);
}
L.log(Level.FINE, "Entities: {0}", bspData.entities.size());
}
public void loadNodes() {
if (bspData.nodes != null) {
return;
}
Class struct = DNode.class;
if (appID == VINDICTUS) {
// use special struct for Vindictus
struct = DNodeVin.class;
}
bspData.nodes = loadLump(LumpType.LUMP_NODES, struct);
}
public void loadLeaves() {
if (bspData.leaves != null) {
return;
}
Class struct = DLeafV1.class;
if (appID == VINDICTUS) {
// use special struct for Vindictus
struct = DLeafVin.class;
} else if (getLump(LumpType.LUMP_LEAFS).getVersion() == 0
&& bspFile.getVersion() == 19) {
// read AmbientLighting, it was used in initial Half-Life 2 maps
// only and doesn't exist in newer or older versions
struct = DLeafV0.class;
}
bspData.leaves = loadLump(LumpType.LUMP_LEAFS, struct);
}
public void loadLeafFaces() {
if (bspData.leafFaces != null) {
return;
}
bspData.leafFaces = loadIntegerLump(LumpType.LUMP_LEAFFACES, appID != VINDICTUS);
}
public void loadLeafBrushes() {
if (bspData.leafBrushes != null) {
return;
}
bspData.leafBrushes = loadIntegerLump(LumpType.LUMP_LEAFBRUSHES, appID != VINDICTUS);
}
public void loadOverlays() {
if (bspData.overlays != null) {
return;
}
Class struct = DOverlay.class;
if (appID == VINDICTUS) {
struct = DOverlayVin.class;
} else if (appID == DOTA_2_BETA) {
struct = DOverlayDota2.class;
}
bspData.overlays = loadLump(LumpType.LUMP_OVERLAYS, struct);
// read fade distances
if (bspData.overlayFades == null) {
bspData.overlayFades = loadLump(LumpType.LUMP_OVERLAY_FADES, DOverlayFade.class);
}
// read CPU/GPU levels
if (bspData.overlaySysLevels == null) {
bspData.overlaySysLevels = loadLump(LumpType.LUMP_OVERLAY_SYSTEM_LEVELS, DOverlaySystemLevel.class);
}
}
public void loadAreaportals() {
if (bspData.areaportals != null) {
return;
}
Class struct = DAreaportal.class;
if (appID == VINDICTUS) {
struct = DAreaportalVin.class;
}
bspData.areaportals = loadLump(LumpType.LUMP_AREAPORTALS, struct);
}
public void loadOccluders() {
if (bspData.occluderDatas != null) {
return;
}
L.log(Level.FINE, "Loading {0}", LumpType.LUMP_OCCLUSION);
Lump lump = getLump(LumpType.LUMP_OCCLUSION);
DataReader in = DataReaders.forByteBuffer(lump.getBuffer());
try {
// load occluder data
final int occluders = lump.getLength() == 0 ? 0 : in.readInt();
bspData.occluderDatas = new ArrayList<>(occluders);
for (int i = 0; i < occluders; i++) {
int lumpVersion = lump.getVersion();
// Contagion maps report lump version 0, but they're actually
// using 1
if (bspFile.getSourceApp().getAppID() == SourceAppID.CONTAGION) {
lumpVersion = 1;
}
DOccluderData od;
if (lumpVersion > 0) {
od = new DOccluderDataV1();
} else {
od = new DOccluderData();
}
od.read(in);
bspData.occluderDatas.add(od);
}
L.log(Level.FINE, "Occluders: {0}", occluders);
// load occluder polys
final int occluderPolys = lump.getLength() == 0 ? 0 : in.readInt();
bspData.occluderPolyDatas = new ArrayList<>(occluderPolys);
for (int i = 0; i < occluderPolys; i++) {
DOccluderPolyData opd = new DOccluderPolyData();
opd.read(in);
bspData.occluderPolyDatas.add(opd);
}
L.log(Level.FINE, "Occluder polygons: {0}", occluderPolys);
// load occluder vertices
final int occluderVertices = lump.getLength() == 0 ? 0 : in.readInt();
bspData.occluderVerts = new ArrayList<>(occluderVertices);
for (int i = 0; i < occluderVertices; i++) {
bspData.occluderVerts.add(in.readInt());
}
L.log(Level.FINE, "Occluder vertices: {0}", occluderVertices);
checkRemaining(in);
} catch (IOException ex) {
lumpError(lump, ex);
}
}
public void loadFlags() {
if (bspData.mapFlags != null) {
return;
}
L.log(Level.FINE, "Loading {0}", LumpType.LUMP_MAP_FLAGS);
Lump lump = getLump(LumpType.LUMP_MAP_FLAGS);
if (lump.getLength() == 0) {
return;
}
DataReader in = DataReaders.forByteBuffer(lump.getBuffer());
try {
bspData.mapFlags = EnumConverter.fromInteger(LevelFlag.class, in.readInt());
L.log(Level.FINE, "Map flags: {0}", bspData.mapFlags);
checkRemaining(in);
} catch (IOException ex) {
lumpError(lump, ex);
}
}
public void loadPrimitives() {
if (bspData.prims != null) {
return;
}
bspData.prims = loadLump(LumpType.LUMP_PRIMITIVES, DPrimitive.class);
}
public void loadPrimIndices() {
if (bspData.primIndices != null) {
return;
}
bspData.primIndices = loadIntegerLump(LumpType.LUMP_PRIMINDICES, true);
}
public void loadPrimVerts() {
if (bspData.primVerts != null) {
return;
}
bspData.primVerts = loadLump(LumpType.LUMP_PRIMVERTS, DVertex.class);
}
private <E extends DStruct> List<E> loadLump(LumpType lumpType, Class<E> struct) {
// don't try to read lumps that aren't supported
if (!bspFile.canReadLump(lumpType)) {
return Collections.emptyList();
}
Lump lump = getLump(lumpType);
// don't try to read empty lumps
if (lump.getLength() == 0) {
return Collections.emptyList();
}
L.log(Level.FINE, "Loading {0}", lumpType);
DataReader in = DataReaders.forByteBuffer(lump.getBuffer());
try {
final int structSize = struct.newInstance().getSize();
final int packetCount = lump.getLength() / structSize;
List<E> packets = new ArrayList<>(packetCount);
for (int i = 0; i < packetCount; i++) {
E packet = struct.newInstance();
long pos = in.position();
packet.read(in);
if (in.position() - pos != packet.getSize()) {
throw new IOException("Bytes read: " + pos + "; expected: " + packet.getSize());
}
packets.add(packet);
}
checkRemaining(in);
L.log(Level.FINE, "{0} {1} objects", new Object[]{packets.size(), struct.getSimpleName()});
return packets;
} catch (IOException ex) {
lumpError(lump, ex);
} catch (IllegalAccessException | InstantiationException ex) {
L.log(Level.SEVERE, "Lump struct class error", ex);
}
return null;
}
private List<Integer> loadIntegerLump(LumpType lumpType, boolean unsignedShort) {
L.log(Level.FINE, "Loading {0}", lumpType);
Lump lump = getLump(lumpType);
DataReader in = DataReaders.forByteBuffer(lump.getBuffer());
try {
final int size = unsignedShort ? 2 : 4;
final int arraySize = lump.getLength() / size;
List<Integer> list = new ArrayList<>(arraySize);
for (int i = 0; i < arraySize; i++) {
if (unsignedShort) {
list.add(in.readUnsignedShort());
} else {
list.add(in.readInt());
}
}
L.log(Level.FINE, "{0} Integer objects", arraySize);
checkRemaining(in);
return list;
} catch (IOException ex) {
lumpError(lump, ex);
}
return null;
}
private List<Integer> loadIntegerLump(LumpType lumpType) {
return loadIntegerLump(lumpType, false);
}
private void lumpError(AbstractLump lump, IOException ex) {
L.log(Level.SEVERE, "Lump reading error in " + lump, ex);
}
/**
* Checks the byte buffer for remaining bytes. Should always be called when
* no remaining bytes are expected.
*
* @throws IOException if remaining bytes are found
*/
private void checkRemaining(DataReader in) throws IOException {
if (in.hasRemaining()) {
throw new IOException(in.remaining()
+ " bytes remaining");
}
}
/**
* Returns the lump for the given lump type
*
* @param type
* @throws IllegalArgumentException if the current BSP doesn't support this lump type
* @return
*/
private Lump getLump(LumpType type) {
return bspFile.getLump(type);
}
/**
* Returns the set of unique entity classes that was generated by {@link #loadEntities}
*
* @return set of entity class names, null if {@link #loadEntities} hasn't been called yet
*/
public Set<String> getEntityClassSet() {
return Collections.unmodifiableSet(entityClasses);
}
public BspFile getBspFile() {
return bspFile;
}
public BspData getData() {
return bspData;
}
}