/*
** 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.bspsrc.modules.entity;
import info.ata4.bsplib.BspFileReader;
import info.ata4.bsplib.app.SourceAppID;
import info.ata4.bsplib.entity.Entity;
import info.ata4.bsplib.entity.EntityIO;
import info.ata4.bsplib.entity.KeyValue;
import info.ata4.bsplib.struct.*;
import info.ata4.bsplib.vector.Vector3f;
import info.ata4.bspsrc.*;
import info.ata4.bspsrc.modules.BspProtection;
import info.ata4.bspsrc.modules.ModuleDecompile;
import info.ata4.bspsrc.modules.VmfMeta;
import info.ata4.bspsrc.modules.geom.BrushMode;
import info.ata4.bspsrc.modules.geom.BrushSource;
import info.ata4.bspsrc.modules.geom.BrushUtils;
import info.ata4.bspsrc.modules.geom.FaceSource;
import info.ata4.bspsrc.modules.texture.TextureSource;
import info.ata4.bspsrc.util.AABB;
import info.ata4.bspsrc.util.SourceFormat;
import info.ata4.bspsrc.util.Winding;
import info.ata4.bspsrc.util.WindingFactory;
import info.ata4.log.LogUtils;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Decompiling module to write point and brush entities converted from various lumps.
*
* Based on several entity building methods from Vmex
*
* @author Nico Bergemann <barracuda415 at yahoo.de>
*/
public class EntitySource extends ModuleDecompile {
// logger
private static final Logger L = LogUtils.getLogger();
private static final Pattern INSTANCE_PREFIX = Pattern.compile("^([^-]+)-");
// sub-modules
private final BspSourceConfig config;
private final BrushSource brushsrc;
private final FaceSource facesrc;
private final TextureSource texsrc;
private final BspProtection bspprot;
private final VmfMeta vmfmeta;
// list of areaportal brush ids
private final Set<Integer> apBrushes = new HashSet<>();
// overlay target names
private final Map<Integer, String> overlayNames = new HashMap<>();
public EntitySource(BspFileReader reader, VmfWriter writer, BspSourceConfig config,
BrushSource brushsrc, FaceSource facesrc, TextureSource texsrc,
BspProtection bspprot, VmfMeta vmfmeta) {
super(reader, writer);
this.config = config;
this.brushsrc = brushsrc;
this.facesrc = facesrc;
this.texsrc = texsrc;
this.bspprot = bspprot;
this.vmfmeta = vmfmeta;
processEntities();
}
/**
* Writes all brush and point entities with exception of some internal
* entities, including
* - cubemaps
* - overlays
* - static props
* - detail brushes
* Those are written by separate methods because of their different data models.
*
* @see writeCubeMaps
* @see writeOverlays
* @see writeStaticProps
* @see writeDetails
*/
public void writeEntities() {
L.info("Writing entities");
// instances are currently supported in compilers for BSP v21+ only
boolean instances = bspFile.getVersion() >= 21;
// fix rotated instance brushes?
// this option is unnecessary for BSP files without instances, it will
// only cause errors
boolean fixRot = config.fixEntityRot && instances;
List<String> visgroups = new ArrayList<>();
for (Entity ent : bsp.entities) {
visgroups.clear();
final String className = ent.getClassName();
// don't write the worldspawn here
if (className.equals("worldspawn")) {
continue;
}
// workaround for a Hammer crashing bug
if (className.equals("env_sprite")) {
String model = ent.getValue("model");
if (model != null && model.startsWith("model")) {
ent.removeValue("scale");
}
}
// these two classes need special attention
final boolean isAreaportal = className.startsWith("func_areaportal");
final boolean isOccluder = className.equals("func_occluder");
// areaportals and occluders don't have a "model" key, take that
// into account
final boolean hasBrush = ent.getModelNum() > 0 || isAreaportal || isOccluder;
// skip point entities?
if (!config.writePointEntities && !hasBrush) {
continue;
}
// skip brush entities?
if (!config.writeBrushEntities && hasBrush) {
continue;
}
// skip areaportals?
if (!config.writeAreaportals && isAreaportal) {
continue;
}
// skip occluders?
if (!config.writeOccluders && isOccluder) {
continue;
}
// skip info_ladder entities, they are used by the engine only to get
// the mins and maxs of a func_ladder to ease bot navigation
if (className.equals("info_ladder")) {
continue;
}
// check for non-internal info_overlay entities
if (className.equals("info_overlay_accessor")) {
String oidStr = ent.getValue("OverlayID");
if (oidStr != null && !oidStr.isEmpty()) {
int oid = Integer.valueOf(oidStr);
// save the targetname for writeOverlays()
overlayNames.put(oid, ent.getTargetName());
// don't write this entity here
continue;
}
}
// re-use hammerid if possible, otherwise generate a new UID
int entID = getHammerID(ent);
if (entID == -1) {
entID = vmfmeta.getUID();
}
writer.start("entity");
writer.put("id", entID);
// get areaportal numbers
int portalNum = -1;
if (isAreaportal) {
String portalNumString = ent.getValue("portalnumber");
// extract portal number
if (portalNumString != null) {
try {
portalNum = Integer.valueOf(portalNumString);
} catch (NumberFormatException ex) {
portalNum = -1;
}
// keep the number when debugging
if (!config.isDebug()) {
ent.removeValue("portalnumber");
}
}
}
// get occluder numbers
int occluderNum = -1;
if (isOccluder) {
String occluderNumString = ent.getValue("occludernumber");
// extract occluder number
if (occluderNumString != null) {
try {
occluderNum = Integer.valueOf(occluderNumString);
} catch (NumberFormatException ex) {
occluderNum = -1;
}
// keep the number when debugging
if (!config.isDebug()) {
ent.removeValue("occludernumber");
}
}
}
int modelNum = ent.getModelNum();
for (Map.Entry<String, String> kv : ent.getEntrySet()) {
String key = kv.getKey();
String value = kv.getValue();
// skip angles for models and world brushes when fixing rotation
if (key.equals("angles") && modelNum >= 0 && fixRot) {
continue;
}
// skip origin for world brushes
if (key.equals("origin") && modelNum == 0) {
continue;
}
// skip model for brush entities
if (key.equals("model") && modelNum != -2) {
continue;
}
// skip hammerid
if (key.equals("hammerid")) {
continue;
}
// don't write angles and origin for portals and occluders
if ((isAreaportal || isOccluder) && (key.equals("angles") || key.equals("origin"))) {
continue;
}
writer.put(key, value);
}
writer.put("classname", className);
// write entity I/O
List<KeyValue> io = ent.getIO();
if (!io.isEmpty()) {
writer.start("connections");
for (KeyValue kv : io) {
writer.put(kv);
}
writer.end("connections");
}
// use origin for brush entities
Vector3f origin = ent.getOrigin();
// brush entities with angles values existed in an instance
// during compilation and need to be rotated manually so Hammer
// displays their correct rotation
Vector3f angles = fixRot ? ent.getAngles() : null;
// write model brushes
if (modelNum > 0) {
if (config.brushMode == BrushMode.BRUSHPLANES) {
brushsrc.writeModel(modelNum, origin, angles);
} else {
facesrc.writeModel(modelNum, origin, angles);
}
} else {
// try to find the areaportal brush
if (isAreaportal && portalNum != -1) {
int portalBrushNum = -1;
// find brushes in brush mode only
if (config.brushMode == BrushMode.BRUSHPLANES) {
portalBrushNum = findAreaportalBrush(portalNum);
}
if (portalBrushNum == -1) {
// no brush found, write areaportal polygon directly
facesrc.writeAreaportal(portalNum);
visgroups.add("Rebuild areaportals");
} else {
// don't rotate or move areaportal brushes, they're always
// positioned correctly
brushsrc.writeBrush(portalBrushNum);
visgroups.add("Reallocated areaportals");
}
}
// always write occluder polygons directly
if (isOccluder && occluderNum != -1) {
facesrc.writeOccluder(occluderNum);
visgroups.add("Rebuild occluders");
}
}
// find instance prefix and add it to a visgroup
if (instances && ent.getTargetName() != null) {
Matcher m = INSTANCE_PREFIX.matcher(ent.getTargetName());
if (m.find()) {
visgroups.add(m.group(1));
}
}
// add protection flags to visgroup
if (bspprot.isProtectedEntity(ent)) {
visgroups.add("VMEX flagged entities");
}
// write visgroup metadata if filled
if (!visgroups.isEmpty()) {
vmfmeta.writeMetaVisgroups(visgroups);
}
writer.end("entity");
}
}
/**
* Writes all func_detail entities
*/
public void writeDetails() {
L.info("Writing func_details");
if (config.detailMerge) {
Set<AABB> detailBounds = new HashSet<>();
Map<AABB, Integer> detailIndices = new HashMap<>();
// add all detail brushes to queue
for (int i = 0; i < bsp.brushes.size(); i++) {
DBrush brush = bsp.brushes.get(i);
// skip non-detail/non-solid brushes
if (!brush.isSolid() || !brush.isDetail()) {
continue;
}
// skip VMEX protector brushes
if (bspprot.isProtectedBrush(brush)) {
continue;
}
// get bounding box of the detail brush
AABB bounds = BrushUtils.getBounds(bsp, brush);
// writeBrush() expects brush indices, so map it to the AABB
detailBounds.add(bounds);
detailIndices.put(bounds, i);
}
while (!detailBounds.isEmpty()) {
// get next group of merged brush AABBs
Set<AABB> detailBoundsGroup = mergeNearestNeighborAABB(
detailBounds, config.detailMergeThresh);
// write brush group as func_detail to VMF
writer.start("entity");
writer.put("id", vmfmeta.getUID());
writer.put("classname", "func_detail");
for (AABB bounds : detailBoundsGroup) {
brushsrc.writeBrush(detailIndices.get(bounds));
}
writer.end("entity");
}
} else {
for (int i = 0; i < bsp.brushes.size(); i++) {
DBrush brush = bsp.brushes.get(i);
// skip non-detail/non-solid brushes
if (!brush.isSolid() || !brush.isDetail()) {
continue;
}
// skip VMEX protector brushes
if (bspprot.isProtectedBrush(brush)) {
continue;
}
writer.start("entity");
writer.put("id", vmfmeta.getUID());
writer.put("classname", "func_detail");
brushsrc.writeBrush(i);
writer.end("entity");
}
}
// write protector brushes separately
List<DBrush> protBrushes = bspprot.getProtectedBrushes();
if (!protBrushes.isEmpty()) {
writer.start("entity");
writer.put("id", vmfmeta.getUID());
writer.put("classname", "func_detail");
vmfmeta.writeMetaVisgroup("VMEX protector brushes");
for (DBrush protBrush : protBrushes) {
brushsrc.writeBrush(bsp.brushes.indexOf(protBrush));
}
writer.end("entity");
}
}
/**
* Transfers the next group of touching bounding volumes from a set of loose
* bounding volumes.
*
* @param src input bounding volumes
* @param thresh touching threshold
* @return set of bounding volumes that have been removed from src
*/
private Set<AABB> mergeNearestNeighborAABB(Set<AABB> src, float thresh) {
// pop next AABB from src
Iterator<AABB> iter = src.iterator();
List<AABB> first = Collections.singletonList(iter.next());
iter.remove();
Queue<AABB> pending = new ArrayDeque<>(first);
Set<AABB> group = new HashSet<>(first);
// do while there are pending AABBs
while (!pending.isEmpty()) {
// get next pending AABB
AABB current = pending.remove();
// expand AABB slightly so it can touch other AABBs more reliably
AABB currentTest = current.expand(thresh);
iter = src.iterator();
while (iter.hasNext()) {
// get next AABB
AABB other = iter.next();
// is it touching the target AABB?
if (other.intersectsWith(currentTest)) {
// add it as pending...
pending.add(other);
// ...and transfer to group
iter.remove();
group.add(other);
}
}
}
return group;
}
/**
* Writes all info_overlay entities
*/
public void writeOverlays() {
L.info("Writing info_overlays");
for (int i = 0; i < bsp.overlays.size(); i++) {
DOverlay o = bsp.overlays.get(i);
// calculate u/v bases
Vector3f ubasis = new Vector3f(o.uvpoints[0].z, o.uvpoints[1].z, o.uvpoints[2].z);
boolean vflip = o.uvpoints[3].z == 1;
for (int j = 0; j < 4; j++) {
o.uvpoints[j] = o.uvpoints[j].set(2, 0);
}
Vector3f vbasis = o.basisNormal.cross(ubasis).normalize();
if (vflip) {
vbasis = vbasis.scalar(-1);
}
// write VMF
writer.start("entity");
writer.put("id", vmfmeta.getUID());
writer.put("classname", "info_overlay");
writer.put("material", texsrc.getTextureName(o.texinfo));
writer.put("StartU", o.u[0]);
writer.put("EndU", o.u[1]);
writer.put("StartV", o.v[0]);
writer.put("EndV", o.v[1]);
writer.put("BasisOrigin", o.origin);
writer.put("BasisU", ubasis);
writer.put("BasisV", vbasis);
writer.put("BasisNormal", o.basisNormal);
writer.put("origin", o.origin);
// write fade distances
if (bsp.overlayFades != null && !bsp.overlayFades.isEmpty()) {
DOverlayFade of = bsp.overlayFades.get(i);
writer.put("fademindist", of.fadeDistMinSq);
writer.put("fademaxdist", of.fadeDistMaxSq);
}
// write system levels
if (bsp.overlaySysLevels != null && !bsp.overlaySysLevels.isEmpty()) {
DOverlaySystemLevel osl = bsp.overlaySysLevels.get(i);
writer.put("mincpulevel", osl.minCPULevel);
writer.put("maxcpulevel", osl.maxCPULevel);
writer.put("mingpulevel", osl.minGPULevel);
writer.put("maxgpulevel", osl.maxGPULevel);
}
for (int j = 0; j < 4; j++) {
writer.put("uv" + j, o.uvpoints[j]);
}
writer.put("RenderOrder", o.getRenderOrder());
Set<Integer> sides = new HashSet<>();
int faceCount = o.getFaceCount();
if (config.brushMode == BrushMode.BRUSHPLANES) {
Set<Integer> origFaces = new HashSet<>();
// collect original faces for this overlay
for (int j = 0; j < faceCount; j++) {
int iface = o.ofaces[j];
int ioface = bsp.faces.get(iface).origFace;
if (ioface > 0) {
origFaces.add(ioface);
}
}
// scan brush sides for the original faces
for (Integer ioface : origFaces) {
findOverlayFaces(i, ioface, sides);
}
} else {
for (int j = 0; j < faceCount; j++) {
int iface = o.ofaces[j];
int faceId = vmfmeta.getFaceUID(iface);
if (faceId != -1) {
sides.add(faceId);
}
}
}
// write brush side list
StringBuilder sb = new StringBuilder();
for (Integer side : sides) {
sb.append(side);
sb.append(" ");
}
writer.put("sides", sb.toString());
if (overlayNames.containsKey(o.id)) {
writer.put("targetname", overlayNames.get(o.id));
}
writer.end("entity");
}
}
/**
* Writes all prop_static entities
*/
public void writeStaticProps() {
L.info("Writing prop_statics");
Map<Vector3f, String> lightingOrigins = new LinkedHashMap<>();
for (DStaticProp pst : bsp.staticProps) {
DStaticPropV4 pst4 = (DStaticPropV4) pst;
writer.start("entity");
writer.put("id", vmfmeta.getUID());
writer.put("classname", "prop_static");
writer.put("origin", pst4.origin);
writer.put("angles", pst4.angles);
writer.put("skin", pst4.skin);
writer.put("fademindist", pst4.fademin == 0 ? -1 : pst4.fademin);
writer.put("fademaxdist", pst4.fademax);
writer.put("solid", pst4.solid);
writer.put("model", bsp.staticPropName.get(pst4.propType));
// store coordinates and targetname of the lighing origin for later
if (pst4.usesLightingOrigin()) {
String infoLightingName;
if (lightingOrigins.containsKey(pst4.lightingOrigin)) {
infoLightingName = lightingOrigins.get(pst4.lightingOrigin);
} else {
infoLightingName = "sprp_lighting_" + lightingOrigins.size();
lightingOrigins.put(pst4.lightingOrigin, infoLightingName);
}
writer.put("lightingorigin", infoLightingName);
}
writer.put("disableshadows", pst4.hasNoShadowing());
if (pst instanceof DStaticPropV5) {
DStaticPropV5 pst5 = (DStaticPropV5) pst;
writer.put("fadescale", pst5.forcedFadeScale);
writer.put("disableselfshadowing", pst5.hasNoSelfShadowing());
writer.put("disablevertexlighting", pst5.hasNoPerVertexLighting());
}
if (pst instanceof DStaticPropV6) {
DStaticPropV6 pst6 = (DStaticPropV6) pst;
writer.put("maxdxlevel", pst6.maxDXLevel);
writer.put("mindxlevel", pst6.minDXLevel);
writer.put("ignorenormals", pst6.hasIgnoreNormals());
}
// write that later; both v7 and v8 have it, but v8 extends v5
Color32 diffMod = null;
if (pst instanceof DStaticPropV7L4D) {
DStaticPropV7L4D pst7 = (DStaticPropV7L4D) pst;
diffMod = pst7.diffuseModulation;
}
if (pst instanceof DStaticPropV8) {
DStaticPropV8 pst8 = (DStaticPropV8) pst;
diffMod = pst8.diffuseModulation;
writer.put("maxcpulevel", pst8.maxCPULevel);
writer.put("mincpulevel", pst8.minCPULevel);
writer.put("maxgpulevel", pst8.maxGPULevel);
writer.put("mingpulevel", pst8.minGPULevel);
}
if (diffMod != null) {
writer.put("rendercolor", String.format("%d %d %d",
diffMod.r, diffMod.g, diffMod.b));
writer.put("renderamt", diffMod.a);
}
if (pst instanceof DStaticPropV9) {
DStaticPropV9 pst9 = (DStaticPropV9) pst;
writer.put("disableX360", pst9.disableX360);
}
if (pst instanceof DStaticPropV5Ship) {
writer.put("targetname", ((DStaticPropV5Ship) pst).targetname);
}
if (pst instanceof DStaticPropV10) {
DStaticPropV10 psttf2 = (DStaticPropV10) pst;
boolean genLightmaps = !psttf2.hasNoPerTexelLighting();
writer.put("generatelightmaps", genLightmaps);
if (genLightmaps) {
writer.put("lightmapresolutionx", psttf2.lightmapResolutionX);
writer.put("lightmapresolutiony", psttf2.lightmapResolutionY);
}
}
writer.end("entity");
}
// write lighting origins
for (Vector3f origin : lightingOrigins.keySet()) {
writer.start("entity");
writer.put("id", vmfmeta.getUID());
writer.put("classname", "info_lighting");
writer.put("targetname", lightingOrigins.get(origin));
writer.put("origin", origin);
writer.end("entity");
}
}
/**
* Writes all env_cubemap entities
*/
public void writeCubemaps() {
L.info("Writing env_cubemaps");
for (int i = 0; i < bsp.cubemaps.size(); i++) {
DCubemapSample cm = bsp.cubemaps.get(i);
writer.start("entity");
writer.put("id", vmfmeta.getUID());
writer.put("classname", "env_cubemap");
writer.put("origin", new Vector3f(cm.origin[0], cm.origin[1], cm.origin[2]));
writer.put("cubemapsize", cm.size);
// FIXME: results are too bad, find a better way
Set<Integer> sideList = texsrc.getBrushSidesForCubemap(i);
if (sideList != null) {
int cmSides = sideList.size();
if (cmSides > config.maxCubemapSides) {
L.log(Level.FINER, "Cubemap {0} has too many sides: {1}",
new Object[]{i, sideList});
}
// write list of brush sides that use this cubemap
if (cmSides > 0 && cmSides < config.maxCubemapSides) {
StringBuilder sb = new StringBuilder();
for (int sideId : sideList) {
sb.append(sideId);
sb.append(" ");
}
// delete last space
sb.deleteCharAt(sb.length() - 1);
writer.put("sides", sb.toString());
}
}
writer.end("entity");
}
}
/**
* Writes all func_ladder entities
*/
public void writeLadders() {
L.info("Writing func_ladders");
for (int i = 0; i < bsp.brushes.size(); i++) {
DBrush brush = bsp.brushes.get(i);
// skip non-ladder brushes
if (!brush.isLadder()) {
continue;
}
// write brush as func_ladder
writer.start("entity");
writer.put("id", vmfmeta.getUID());
writer.put("classname", "func_ladder");
brushsrc.writeBrush(i);
writer.end("entity");
}
}
private int findAreaportalBrush(int portalnum) {
// do we have areaportals at all?
if (bsp.areaportals.isEmpty()) {
return -1;
}
DAreaportal ap = null;
// each portal key has two dareaportal_t's, but their geometry always
// seems to be identical, so just pick the first one we get
for (DAreaportal areaportal : bsp.areaportals) {
if (areaportal.portalKey == portalnum) {
ap = areaportal;
break;
}
}
// have we found something for that key?
if (ap == null) {
L.log(Level.FINER, "No portal geometry for portal key {0}", portalnum);
return -1;
}
// create areaportal winding
Winding wp = WindingFactory.fromAreaportal(bsp, ap);
for (int i = 0; i < bsp.brushes.size(); i++) {
DBrush brush = bsp.brushes.get(i);
// considered brushes must be flagged as areaportal
if (!brush.isAreaportal()) {
continue;
}
// skip already assigned brushes
if (apBrushes.contains(i)) {
continue;
}
// compare each brush side with areaportal face
for (int j = 0; j < brush.numside; j++) {
// create brush side winding
Winding w = WindingFactory.fromSide(bsp, brush, j);
// compare windings
if (w.matches(wp)) {
L.log(Level.FINER, "Brush {0} for portal key {1}",
new Object[]{i, portalnum});
// add as assigned brush
apBrushes.add(i);
return i;
}
}
}
// nothing found :(
L.log(Level.FINER, "No brush for portal key {0}", portalnum);
return -1;
}
private void findOverlayFaces(int ioverlay, int ioface, Set<Integer> sides) {
// no original face information available? then we're done here...
if (bsp.origFaces.isEmpty()) {
return;
}
// don't add more if we already hit the maximum
if (sides.size() >= config.maxOverlaySides) {
return;
}
int sidesPrev = sides.size();
DFace origFace = bsp.origFaces.get(ioface);
// use sideid of displacement, if existing
if (origFace.dispInfo != -1) {
int side = vmfmeta.getDispInfoUID(origFace.dispInfo);
if (side != -1) {
L.log(Level.FINER, "O: {0} D: {1} id: {2}",
new Object[]{ioverlay, origFace.dispInfo, side});
sides.add(side);
}
return;
}
// create winding from original face
Winding wof = WindingFactory.fromFace(bsp, origFace);
for (int i = 0; i < bsp.brushes.size(); i++) {
DBrush brush = bsp.brushes.get(i);
for (int j = 0; j < brush.numside; j++) {
int ibs = brush.fstside + j;
int side = brushsrc.getBrushSideIDForIndex(ibs);
// skip unmapped brush sides
if (side == -1) {
continue;
}
DBrushSide bs = bsp.brushSides.get(ibs);
// create winding from brush side
Winding w = WindingFactory.fromSide(bsp, brush, j);
// check for valid face: same plane, same texinfo, same geometry
if (origFace.pnum != bs.pnum || origFace.texinfo != bs.texinfo
|| !w.matches(wof)) {
continue;
}
L.log(Level.FINER, "O: {0} OF: {1} B: {2} BS: {3} id: {4}",
new Object[]{ioverlay, ioface, i, ibs, side});
sides.add(side);
// make sure we won't have too many brush sides for that overlay
if (sides.size() >= config.maxOverlaySides) {
L.log(Level.WARNING, "Too many brush sides for overlay {0}", ioverlay);
break;
}
}
}
if (sides.size() == sidesPrev) {
L.log(Level.FINER, "O: {0} OF: {1} no match", new Object[]{ioverlay, ioface});
}
}
private void processEntities() {
for (Entity ent : bsp.entities) {
String className = ent.getClassName();
// fix worldspawn
if (className.equals("worldspawn")) {
// remove values that are unknown to Hammer
ent.removeValue("world_mins");
ent.removeValue("world_maxs");
ent.removeValue("hammerid");
// rebuild mapversion
if (!ent.hasKey("mapversion")) {
ent.setValue("mapversion", bspFile.getRevision());
}
}
// convert VMF format if requested
if (config.sourceFormat != SourceFormat.AUTO) {
char srcSep;
char dstSep;
if (config.sourceFormat == SourceFormat.NEW) {
srcSep = EntityIO.SEP_CHR_OLD;
dstSep = EntityIO.SEP_CHR_NEW;
} else {
srcSep = EntityIO.SEP_CHR_NEW;
dstSep = EntityIO.SEP_CHR_OLD;
}
for (KeyValue kv : ent.getIO()) {
String value = kv.getValue();
value = value.replace(srcSep, dstSep);
kv.setValue(value);
}
}
// replace escaped quotes for VTMB so they can be loaded with the
// inofficial SDK Hammer
if (bspFile.getSourceApp().getAppID() == SourceAppID.VAMPIRE_BLOODLINES) {
for (Map.Entry<String, String> kv : ent.getEntrySet()) {
String value = kv.getValue();
value = value.replace("\\\"", "");
kv.setValue(value);
}
for (KeyValue kv : ent.getIO()) {
String value = kv.getValue();
value = value.replace("\\\"", "");
kv.setValue(value);
}
}
// func_simpleladder entities are used by the engine only and won't
// work when re-compiling, so replace them with empty func_ladder's
// instead.
if (className.equals("func_simpleladder")) {
int modelNum = ent.getModelNum();
ent.clear();
ent.setClassName("func_ladder");
ent.setModelNum(modelNum);
}
// fix light entities (except for dynamic lights)
if (className.startsWith("light")
&& !className.equals("light_dynamic")) {
fixLightEntity(ent);
}
// add cameras based on info_player_* positions
if (className.startsWith("info_player_")) {
createCamera(ent);
}
// add hammerid to UID blacklist to make sure they're not generated
// for anything else
int hammerid = getHammerID(ent);
if (hammerid != -1) {
vmfmeta.getUIDBlackList().add(hammerid);
}
}
}
private int getHammerID(Entity ent) {
String keyName = "hammerid";
if (!ent.hasKey(keyName)) {
return -1;
}
String hammeridStr = ent.getValue(keyName);
int hammerid = -1;
try {
hammerid = Integer.parseInt(ent.getValue("hammerid"));
} catch (NumberFormatException ex) {
L.log(Level.WARNING, "Invalid hammerid format {0}", hammeridStr);
}
return hammerid;
}
private void fixLightEntity(Entity ent) {
String style = ent.getValue("style");
String defaultStyle = ent.getValue("defaultstyle");
if (style == null) {
// no style
return;
}
try {
// values below 32 = default presets
if (Integer.valueOf(style) < 32) {
return;
}
} catch (NumberFormatException ex) {
L.log(Level.WARNING, "Invalid light style number format: {0}", style);
}
// Use original preset style, if set. Empty the style otherwise.
if (defaultStyle != null) {
ent.setValue("style", defaultStyle);
ent.removeValue("defaultstyle");
} else {
ent.removeValue("style");
}
}
private void createCamera(Entity ent) {
Vector3f origin = ent.getOrigin();
Vector3f angles = ent.getAngles();
if (origin == null) {
return;
}
if (angles == null) {
angles = Vector3f.NULL;
}
// calculate position and look vectors
Vector3f pos, look;
// move 64 units up
pos = origin.add(new Vector3f(0, 0, 64));
// look 256 units forwards to entity facing direction
look = new Vector3f(192, 0, 0).rotate(angles).add(origin);
// move 64 units backwards to facing direction
pos = look.sub(pos).normalize().scalar(-64).add(pos);
vmfmeta.getCameras().add(new Camera(pos, look));
}
}