/*
** 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;
import info.ata4.bsplib.BspFileReader;
import info.ata4.bsplib.entity.Entity;
import info.ata4.bsplib.struct.DBrush;
import info.ata4.bsplib.struct.DBrushSide;
import info.ata4.bsplib.struct.DPlane;
import info.ata4.bsplib.vector.Vector3f;
import info.ata4.bspsrc.modules.texture.TextureSource;
import info.ata4.bspsrc.modules.texture.ToolTexture;
import info.ata4.bspsrc.util.AABB;
import info.ata4.bspsrc.modules.geom.BrushUtils;
import info.ata4.bspsrc.util.WindingFactory;
import info.ata4.log.LogUtils;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
/**
* A module to check if the map has been protected by the mapper with at least
* one of these methods:
* - "no_decomp" entity property
* - "tools/locked" texture
* - protection prefab
* - encrypted entities by BSPProtect
* - obfuscated entities by IID
* - texinfo hack by IID_BSP
*
* @author Nico Bergemann <barracuda415 at yahoo.de>
*/
public class BspProtection extends ModuleRead {
// constants
public static final String BSPPROTECT_FILE = "entities.dat";
public static final String VMEX_LOCKED_TEX = "tools/locked";
public static final String VMEX_LOCKED_ENT = "no_decomp";
private static final float EPS_SIZE = 0.01f;
private static final float ALIGNED_ALPHA = 0.99f;
private static final float NODRAW_RATIO_LIMIT = 0.9f;
private static final Vector3f PB1 = new Vector3f(1, 4, 9);
private static final Vector3f PB2 = new Vector3f(4, 9, 1);
private static final Vector3f PB3 = new Vector3f(9, 1, 4);
// logger
private static final Logger L = LogUtils.getLogger();
// sub-modules
private final TextureSource texsrc;
// flags
private boolean flaggedEnt;
private boolean flaggedTex;
private boolean flaggedBrush;
private boolean encryptedEnt;
private boolean obfuscatedEnt;
private boolean modifedTexinfo;
// lists of protecting elements
private List<DBrush> protBrushes = new ArrayList<>();
private List<Entity> protEntities = new ArrayList<>();
public BspProtection(BspFileReader reader, TextureSource texsrc) {
super(reader);
reader.loadEntities();
reader.loadPlanes();
reader.loadBrushes();
reader.loadBrushSides();
this.texsrc = texsrc;
}
public boolean check() {
flaggedEnt = false;
flaggedTex = false;
flaggedBrush = false;
encryptedEnt = false;
obfuscatedEnt = false;
modifedTexinfo = false;
checkBrushes();
checkBrushSides();
checkEntities();
checkTextures();
checkPakfile();
boolean prot = isProtected();
if (!prot) {
L.fine("Nothing found");
}
return prot;
}
public boolean isProtected() {
return flaggedEnt || flaggedTex || flaggedBrush || encryptedEnt || obfuscatedEnt || modifedTexinfo;
}
public boolean hasEntityFlag() {
return flaggedEnt;
}
public boolean hasTextureFlag() {
return flaggedTex;
}
public boolean hasBrushFlag() {
return flaggedBrush;
}
public boolean hasEncryptedEntities() {
return encryptedEnt;
}
public boolean hasObfuscatedEntities() {
return obfuscatedEnt;
}
public boolean hasModifiedTexinfo() {
return modifedTexinfo;
}
/**
* Returns all found protection methods in string form.
*
* @return list of method strings
*/
public List<String> getProtectionMethods() {
List<String> methods = new ArrayList<>();
if (hasEntityFlag()) {
methods.add("VMEX entity flag (no_decomp)");
}
if (hasTextureFlag()) {
methods.add("VMEX texture flag (tools/locked)");
}
if (hasBrushFlag()) {
methods.add("VMEX protector brush flag");
}
if (hasEncryptedEntities()) {
methods.add("BSPProtect entity encryption");
}
if (hasObfuscatedEntities()) {
methods.add("IID entity obfuscation");
}
if (hasModifiedTexinfo()) {
methods.add("IID nodraw texture hack");
}
return methods;
}
/**
* Returns all found protector brushes.
*
* @return list of protector brushes
*/
public List<DBrush> getProtectedBrushes() {
List<DBrush> list = new ArrayList<>();
list.addAll(protBrushes);
return list;
}
/**
* Checks if the given brush is a protector brush.
*
* @param brush
* @return true if the brush is part of the protection prefab.
*/
public boolean isProtectedBrush(DBrush brush) {
return protBrushes.contains(brush);
}
/**
* Returns all found protector entities.
*
* @return list of protector entities
*/
public List<Entity> getProtectedEntities() {
List<Entity> list = new ArrayList<>();
list.addAll(protEntities);
return list;
}
/**
* Checks if an entitiy contains protection keyvalues.
*
* @param entity
* @return true if the entity contains protection keyvalues
*/
public boolean isProtectedEntity(Entity entity) {
return protEntities.contains(entity);
}
private void checkBrushes() {
L.fine("Checking for protector prefab");
DBrush b1 = null;
DBrush b2 = null;
DBrush b3 = null;
// check every brush
for (DBrush b : bsp.brushes) {
// ignore brushes that don't fit
if (!isAlignedBrush(b) || !isSameTexBrush(b)) {
continue;
}
// get brush dimensions
Vector3f bsize = BrushUtils.getBounds(bsp, b).getSize();
// check brush dimensions with prefab constants
if (PB1.sub(bsize).length() < EPS_SIZE) {
b1 = b;
}
if (PB2.sub(bsize).length() < EPS_SIZE) {
b2 = b;
}
if (PB3.sub(bsize).length() < EPS_SIZE) {
b3 = b;
}
// check if all three brushes exists
if (b1 != null && b2 != null && b3 != null) {
L.fine("Found protector prefab!");
flaggedBrush = true;
protBrushes.add(b1);
protBrushes.add(b2);
protBrushes.add(b3);
b1 = null;
b2 = null;
b3 = null;
}
}
}
private void checkBrushSides() {
L.log(Level.FINE, "Checking for nodraw brush sides (ratio limit: {0})", NODRAW_RATIO_LIMIT);
double nodrawSides = 0;
for (DBrushSide bs : bsp.brushSides) {
if (bs.texinfo == 0) {
nodrawSides++;
}
}
double nodrawRatio = nodrawSides / bsp.brushSides.size();
// check if there're too many nodraw brush sides
modifedTexinfo = nodrawRatio > NODRAW_RATIO_LIMIT;
if (modifedTexinfo) {
L.fine("Found nodraw hack!");
}
}
private void checkEntities() {
L.fine("Checking for entity lock key \"" + VMEX_LOCKED_ENT + "\" and obfuscated targetnames");
int targetnames = 0;
int targetnamesObfs = 0;
for (Entity ent : bsp.entities) {
String targetName = ent.getTargetName();
// check for obfuscated target names
if (targetName != null) {
targetnames++;
if (targetName.matches("^[0-9]+$")) {
targetnamesObfs++;
}
}
// search for no_decomp entity property
for (String key : ent.getKeys()) {
if (key.equals(VMEX_LOCKED_ENT)) {
L.fine("Found lock key!");
protEntities.add(ent);
flaggedEnt = true;
}
}
}
// all targetnames are numeric?
obfuscatedEnt = targetnames > 0 && targetnames == targetnamesObfs;
if (obfuscatedEnt) {
L.fine("Found obfuscation!");
}
}
/**
* Checks for the lock-texture
*/
private void checkTextures() {
L.fine("Checking for lock texture \"" + VMEX_LOCKED_TEX + "\"");
// search for tools/locked texture
for (String texname : bsp.texnames) {
if (texname.equalsIgnoreCase(VMEX_LOCKED_TEX)) {
L.fine("Found lock texture!");
flaggedTex = true;
return;
}
}
}
/**
* Searches for the "entities.dat" file in the pakfile, which contains the
* ICE encrypted entity lump created by BSPProtect.
* The visible entitly lump will contain the worldspawn only if the
* map file has been encrypted with this tool.
*/
private void checkPakfile() {
L.fine("Checking for encrypted entities inside pakfile (file: \"" + BSPPROTECT_FILE + "\")");
// BSPProtect currently works with Source 2007/2009 aka Orange Box only
if (bspFile.getVersion() != 20) {
encryptedEnt = false;
return;
}
try (ZipArchiveInputStream zis = bspFile.getPakFile().getArchiveInputStream()) {
ZipArchiveEntry ze;
while ((ze = zis.getNextZipEntry()) != null) {
if (ze.getName().equals(BSPPROTECT_FILE)) {
L.fine("Found encrypted entities!");
encryptedEnt = true;
break;
}
}
} catch (IOException ex) {
L.log(Level.WARNING, "Couldn't read pakfile", ex);
// pakfile broken or missing?
encryptedEnt = false;
}
}
/**
* Checks if a brush is aligned(?)
*
* @param brush a brush
* @return true, if the brush is aligned
*/
private boolean isAlignedBrush(DBrush brush) {
if (brush.numside != 6) {
return false;
}
for (int i = 0; i < 6; i++) {
DBrushSide bs = bsp.brushSides.get(brush.fstside + i);
DPlane bpl = bsp.planes.get(bs.pnum);
for (float value : bpl.normal) {
if (Math.abs(value) > ALIGNED_ALPHA) {
return true;
}
}
}
return false;
}
/**
* Checks if a brush shares the same texture string on all sides
*
* @param brush a brush
* @return true if all brush sides share the same texture
*/
private boolean isSameTexBrush(DBrush brush) {
DBrushSide bs = bsp.brushSides.get(brush.fstside);
String texname = texsrc.getTextureName(bs.texinfo);
if (texname.equals(ToolTexture.SKIP)) {
// this side has no valid texture
return false;
}
for (int i = 1; i < brush.numside; i++) {
bs = bsp.brushSides.get(brush.fstside + i);
String nexttexname = texsrc.getTextureName(bs.texinfo);
if (!texname.equalsIgnoreCase(nexttexname)) {
return false;
}
}
return true;
}
}