Skip to content

Instantly share code, notes, and snippets.

@bisqwit
Last active June 24, 2018 14:44
Show Gist options
  • Save bisqwit/9829680 to your computer and use it in GitHub Desktop.
Save bisqwit/9829680 to your computer and use it in GitHub Desktop.
Rewriting vbsp_styles (Portal 2 Puzzlemaker BEE2)
/* TO COMPILE:
*
i686-w64-mingw32-g++.exe -o vbsp_bisqwit.exe vbsp_bisqwit.cc -Wall -Wextra -pedantic -Ofast -static -flto -s -std=gnu++1y -fopenmp
OR:
x86_64-w64-mingw32-g++.exe -o vbsp_bisqwit.exe vbsp_bisqwit.cc -Wall -Wextra -pedantic -Ofast -static -flto -s -std=gnu++1y -fopenmp
OR
i686-pc-mingw32-g++.exe -o vbsp_bisqwit.exe vbsp_bisqwit.cc -Wall -Wextra -pedantic -O3 -static -flto -s -std=gnu++11 -fopenmp
(Requires GCC version 4.8 or newer; the libstdc++ that comes with older versions did not provide std::stoi)
*/
#include <cstdio>
#include <string>
#include <dirent.h>
#include <memory>
#include <vector>
#include <utility>
#include <cctype>
#include <algorithm>
#include <unordered_set>
#include <unordered_map>
#include <cstdlib>
#include <csignal>
#include <ctime>
#include <stdarg.h>
#ifdef __WIN32__
#include <process.h>
#include <io.h>
#else
#include <unistd.h>
#endif
#include <sys/stat.h>
// Split pathfilename into path and filename portions.
void SplitFN(const std::string& s, std::string& path, std::string& fn)
{
std::size_t f = s.rfind('/'), b = s.rfind('\\');
int p = std::max( f==s.npos ? -1 : int(f),
b==s.npos ? -1 : int(b) );
if(p == -1) { fn = s; path = "."; return; }
path = s.substr(0, p);
fn = s.substr(p+1);
}
// Replace the extension in a file name with new_ext. new_ext must include the leading ".".
std::string ChangeExt(const std::string& fn, const std::string& new_ext)
{
std::size_t p = fn.find('.');
if(p == fn.npos) return fn;
return fn.substr(0, p) + new_ext;
}
// Helper function used to create indentations. Returns a string with n tabs. Used by Dump().
std::string Indent(unsigned n)
{
return std::string(n, '\t');
}
// Converts the string to lowecase using std::tolower.
std::string LCase(const std::string& s)
{
std::string result{s};
for(char& c: result) c = std::tolower(c);
return result;
}
// Adds quotation marks if needed. Used in conjunction with spawn*().
// Do not use for Valve Trees. Adding or removing quotes there does not make the intended outcome.
std::string AddQuotes(const std::string& s)
{
if(s[0] == '"') return s;
bool need_quotes = false;
for(char c: s)
if(c == ' ')
{ need_quotes = true; break; }
if(!need_quotes) return s;
return "\"" + s + "\"";
}
#ifndef __WIN32__
enum { P_WAIT=0, P_NOWAIT=1 };
int spawnvp(int mode, const char* fn, char** args)
{
#error "spawnvp not implemented for this OS yet"
}
#endif
std::string log_fn;
FILE* log_fp = NULL;
void LogPrintf(const char* fmt, ...)
{
char Buf[8192];
va_list ap;
va_start(ap, fmt);
int len = vsnprintf(Buf, sizeof(Buf), fmt, ap);
va_end(ap);
std::fwrite(Buf, 1, len, stdout);
if(!log_fp)
{
log_fp = std::fopen(log_fn.c_str(), "at");
if(!log_fp) log_fp = std::fopen(log_fn.c_str(), "wt");
}
if(log_fp)
{
std::fwrite(Buf, 1, len, log_fp);
std::fflush(log_fp);
}
std::fflush(stdout);
}
struct ValveTree
{
std::string value_string;
std::vector< std::pair<std::string, std::shared_ptr<ValveTree>>> comment_keys;
std::vector< std::pair<std::string, std::shared_ptr<ValveTree>>> value_keys;
ValveTree() {}
ValveTree(ValveTree&& ) = default;
ValveTree(const ValveTree& ) = default;
ValveTree& operator=(const ValveTree&) = default;
ValveTree& operator=(ValveTree&& ) = default;
// Create tree by parsing VMF/P2C/editoritems.
// Parsing the tree is slightly more complicated than it should be
// because we are also paying special attention to tags that are put
// in comments, used by StylesMod/BEE2. At the same time, we must
// still ignore actual comments properly.
ValveTree(const std::string& filedata, std::size_t& p)
{
bool in_comment = false;
std::string key;
std::size_t begin_pos = p;
while(p < filedata.size() && !(!in_comment && filedata[p]=='}' && begin_pos!=0))
{
if(filedata[p] == '\r' || filedata[p] == '\n')
{
if(in_comment) key.clear();
//if(in_comment) { LogPrintf("Comment off\n"); }
in_comment = false;
}
if( std::isspace(filedata[p]) ) { ++p; continue; }
if(filedata[p]=='/' && filedata[p+1]=='/')
{
//LogPrintf("Comment on\n");
in_comment = true;
if(filedata[p+2]=='/') in_comment = false;
p += 2;
continue;
}
if(!key.empty() && !in_comment && filedata[p] == '{')
{
// We have a key, and we got a tree just now.
//LogPrintf("key: %s, tree:\n", key.c_str());
++p;
std::shared_ptr<ValveTree> subtree { new ValveTree(filedata, p) };
std::pair<std::string, std::shared_ptr<ValveTree>> keypair { key, std::move(subtree) };
(in_comment ? comment_keys : value_keys).push_back(keypair);
key.clear();
if(p < filedata.size() && filedata[p]=='}') ++p;
//LogPrintf("end tree (%s)\n", in_comment?"comment":"value");
continue;
}
// Ignore everything until a key or a value begins.
std::string stringdata;
if(std::isalpha(filedata[p]))
{
while(p < filedata.size() && (std::isalnum(filedata[p]) || filedata[p]=='_'))
stringdata += filedata[p++];
if(in_comment && stringdata.substr(0,7)=="packer_" && filedata[p]==':')
{
// It is a packer command. Parse the rest of it!
++p;
key.swap(stringdata);
while(p < filedata.size() && !std::isspace(filedata[p]))
stringdata += filedata[p++];
}
else if(in_comment) continue;
}
else if(filedata[p] == '"')
{
stringdata += filedata[p++];
while(p < filedata.size() && filedata[p] != '"')
{
if(filedata[p]=='\\') ++p;
stringdata += filedata[p++];
}
if(p < filedata.size() && filedata[p]=='"') stringdata += filedata[p++];
}
else
{ ++p; continue; }
if(key.empty())
{
// It was a key.
key = std::move(stringdata);
}
else
{
// We have a key, and we got a string value just now.
//LogPrintf("value(%s): %s (%s)\n", key.c_str(), stringdata.c_str(), in_comment?"comment":"value");
std::shared_ptr<ValveTree> subtree { new ValveTree };
subtree->value_string = std::move(stringdata);
std::pair<std::string, std::shared_ptr<ValveTree>> keypair { key, std::move(subtree) };
(in_comment ? comment_keys : value_keys).push_back(keypair);
key.clear();
}
}
}
// Create VMF/P2C/editoritems by dumping tree:
std::string Dump(unsigned indent = 0, bool nl_pending = false) const
{
std::string result;
if(nl_pending)
{
if(!value_string.empty())
{
result += ' ';
result += value_string;
}
result += '\n';
nl_pending = false;
}
else if(!value_string.empty())
{
result += Indent(indent);
result += value_string;
result += '\n';
}
if(indent != ~0u && (!comment_keys.empty() || !value_keys.empty() || value_string.empty()))
{
result += Indent(indent);
result += "{\n";
}
for(const auto& p: comment_keys)
{
result += Indent(indent+1);
result += "// ";
result += p.first;
result += p.second->Dump(indent+1, true);
}
for(const auto& p: value_keys)
{
result += Indent(indent+1);
result += p.first;
result += p.second->Dump(indent+1, true);
}
if(indent != ~0u && (!comment_keys.empty() || !value_keys.empty() || value_string.empty()))
{
result += Indent(indent);
result += "}\n";
}
return result;
}
};
// Reads a file and generates a ValveTree.
ValveTree ParseValveFile(const std::string& pathfn)
{
std::string filedata;
std::FILE* fp = std::fopen(pathfn.c_str(), "rb");
if(!fp)
{
std::perror(pathfn.c_str());
return {};
}
LogPrintf("vbsp_bisqwit: Parsing %s\n", pathfn.c_str());
for(;;)
{
char Buf[8192];
std::size_t r = std::fread(Buf, 1, sizeof(Buf), fp);
if(r == 0)
{
if(std::ferror(fp))
std::perror(pathfn.c_str());
break;
}
filedata.append(Buf, Buf+r);
}
std::fclose(fp);
std::size_t p = 0;
return ValveTree(filedata, p);
}
// Globals
namespace
{
std::string game_param;
std::string original_vmf_pathfn;
std::string changed_vmf_pathfn;
std::string original_vmf_path, original_vmf_fn;
std::string editoritems_pathfn;
std::string p2c_pathfn;
std::string style_path;
ValveTree original_vmf;
ValveTree original_p2c;
ValveTree editoritems;
ValveTree modified_vmf;
// Item VMFs:
// For each item "type" in editoritems.txt, list all Instance "names" (filenames)
std::unordered_map<std::string, std::vector<std::string>> itemtype_vmfs;
// Packer conditions:
// "instance" filename -> condition
std::unordered_multimap<std::string, std::string> instance_dependent_packer_conditions;
// Item "Type" dependent conditions
std::unordered_multimap<std::string, std::string> itemtype_dependent_packer_conditions;
// List of conditions enabled so far when parsing the .VMF and .P2C
// If the condition contains ';', it is a rename. Otherwise it's an "additem".
std::unordered_set<std::string> enabled_packer_conditions;
// For each item number, {itemtype and index to Instances}:
std::vector< std::pair<std::string, unsigned> > p2c_items;
bool debug = false;
int exit_code = 0;
}
// Parse editoritems.txt and find the packer conditions.
// Requires: editoritems has been created.
// Creates: itemtype_vmfs, instance_dependent_packer_conditions, itemtype_dependent_packer_conditions, enabled_packer_conditions
void FindPackerConditions(const ValveTree& tree = editoritems,
const std::string& type="",
bool instances=false)
{
// Packer rules:
//
// A rule that is together with "Name" in "Instances"
// must only be applied if an "instance" is found
// that references that particular file.
//
// A rule that is under "Instances", but not together
// with a "name" must be applied, if the "Type" of this
// item was included in the map.
//
// A rule that is not under "Instances" must be always applied.
//
// Rules:
//
// packer_additem: Adds the given file to the packing list.
// packer_rename: oldfilename;newfilename
// newfilename may include "mapnamehere".
//
// Does this tree have a "Name" entry?
std::string my_name;
for(const auto& p: tree.value_keys)
if(p.first == "\"Name\"" && !p.second->value_string.empty())
my_name = p.second->value_string;
// Does this tree have a "Type" entry?
std::string my_type;
for(const auto& p: tree.value_keys)
if(p.first == "\"Type\"" && !p.second->value_string.empty())
my_type = p.second->value_string;
for(const auto& p: tree.comment_keys)
{
if(p.first == "packer_additem" || p.first == "packer_rename")
{
if(instances && !my_name.empty())
{
instance_dependent_packer_conditions.insert( { my_name, p.second->value_string } );
if(debug) LogPrintf("Packing if instance %s: %s\n", my_name.c_str(), p.second->value_string.c_str());
}
else if(instances)
{
itemtype_dependent_packer_conditions.insert( { type, p.second->value_string } );
if(debug) LogPrintf("Packing if type %s: %s\n", type.c_str(), p.second->value_string.c_str());
}
else
{
enabled_packer_conditions.insert( p.second->value_string );
if(debug) LogPrintf("Always packing: %s\n", p.second->value_string.c_str());
}
}
}
if(instances && !my_name.empty() && !type.empty())
{
if(debug) LogPrintf("Type %s: %s\n", type.c_str(), my_name.c_str());
itemtype_vmfs[type].push_back(my_name);
}
for(const auto& p: tree.value_keys)
FindPackerConditions(*p.second,
type.empty() ? my_type : type,
instances || p.first == "\"Instances\"" );
}
// Parse the .p2c puzzle file and record the list of used items.
// Requires: FindPackerConditions() has been run.
// Creates: p2c_items
// Updates: enabled_packer_conditions
void FindUsedItemTypes()
{
const ValveTree* root = &original_p2c;
// In original_p2c, find "Items"
rescan:
for(const auto& p: root->value_keys)
if(p.first == "\"Items\"" || p.first == "\"portal2_puzzle\"")
{ root = &*p.second; goto rescan; }
// In "Items", find all "Item"
for(const auto& p: root->value_keys)
{
if(p.first == "\"Item\"")
{
std::unordered_map<std::string, std::string> props;
for(const auto& q: p.second->value_keys)
props[q.first] = q.second->value_string;
const auto& type_str = props.find("\"Type\"")->second;
const auto& item_no_str = props.find("\"Index\"")->second;
std::string index_str;
{ auto i = props.find("\"ITEM_PROPERTY_BARRIER_TYPE\"");
if(i != props.end()) index_str = i->second; }
{ auto i = props.find("\"ITEM_PROPERTY_BUTTON_TYPE\"");
if(i != props.end()) index_str = i->second; }
{ auto i = props.find("\"ITEM_PROPERTY_CUBE_TYPE\"");
if(i != props.end()) index_str = i->second; }
unsigned item_no = std::stoi(item_no_str.substr(1));
unsigned index_no = index_str.empty() ? 0 : std::stoi(index_str.substr(1));
if(debug) LogPrintf("Item %u: %s (%u)\n", item_no, type_str.c_str(), index_no);
// Save the item type
if(p2c_items.size() <= item_no) p2c_items.resize(item_no+1);
p2c_items[item_no].first = type_str;
p2c_items[item_no].second = index_no;
// Add the bspzip (packer) items that we need
auto r = itemtype_dependent_packer_conditions.equal_range(type_str);
while(r.first != r.second) enabled_packer_conditions.insert( r.first++->second );
}
}
}
// Parse through the Hammer map file (VMF) and make necessary changes.
// Requires: FindPackerConditions() and FindUsedItems() have been run.
// Creates: modified_vmf
// Updates: enabled_packer_conditions
void TranslateVMF()
{
/*
CHANGES TO DO:
func_instance "file":
instances/p2editor_clean/glass_128x128.vmf
INTO: instances/p2editor_clean/realglass_128x128.vmf
OR: instances/p2editor_clean/grating_128x128.vmf
"material" replaces:
BlackFloor
Matches metal/black_floor_metal_001c White:tile/white_wall_tile003f
BlackCeiling
Matches metal/black_floor_metal_001c White:tile/white_wall_tile003f or tile/white_wall_tile003a
BlackWall
Matches metal/black_wall_metal_002c White:tile/white_wall_tile003f or tile/white_wall_tile003a
"targetname" replaces:
"barrierhazardN_modelStartN" -> "barrierhazardN_modelStart"
"barrierhazardN_modelEndN" -> "barrierhazardN_modelEnd"
*/
// Gather the list of texture substitutions.
std::unordered_map<std::string, std::vector<std::string>> texture_substitutions;
for(const auto& p: editoritems.comment_keys)
{
if(p.first == "\"WhiteFloor\""
|| p.first == "\"WhiteCeiling\""
|| p.first == "\"WhiteWall\""
|| p.first == "\"BlackFloor\""
|| p.first == "\"BlackCeiling\""
|| p.first == "\"BlackWall\"")
{
texture_substitutions[p.first].push_back(p.second->value_string);
}
}
modified_vmf = original_vmf;
// Walk through the VMF file and see what to change.
//
// We walk through all of: world/solid/side/material
// And all of: entity/solid/side/material
for(const auto& a: modified_vmf.value_keys) if(a.first == "world" || a.first == "entity")
for(const auto& b: a.second->value_keys) if(b.first == "solid")
for(const auto& c: b.second->value_keys) if(c.first == "side")
{
double plane[3][3];
for(const auto& d: c.second->value_keys)
if(d.first == "\"plane\"")
std::sscanf(d.second->value_string.c_str(), "\"(%lf %lf %lf) (%lf %lf %lf) (%lf %lf %lf)\"",
&plane[0][0],&plane[0][1],&plane[0][2],
&plane[1][0],&plane[1][1],&plane[1][2],
&plane[2][0],&plane[2][1],&plane[2][2]);
for(auto& d: c.second->value_keys)
if(d.first == "\"material\"")
{
std::string& mat = d.second->value_string;
const std::string mat_l = LCase(mat);
/*LogPrintf("Material %-30s: %g %g %g, %g %g %g, %g %g %g\n",
mat.c_str(),
plane[0][0],plane[0][1],plane[0][2],
plane[1][0],plane[1][1],plane[1][2],
plane[2][0],plane[2][1],plane[2][2]);*/
std::string color;
if(mat_l == "\"metal/black_floor_metal_001c\""
|| mat_l == "\"metal/black_wall_metal_002c\"")
color = "Black";
if(mat_l == "\"tile/white_wall_tile003f\""
|| mat_l == "\"tile/white_wall_tile003a\"")
color = "White";
if(color.empty()) continue;
// Determine the orientation of this surface.
// Floors:
// PLANE: z0=z1=z2
// x,y: smaller larger
// larger larger
// larger smaller
// Ceilings:
// PLANE: z0=z1=z2
// x,y: smaller smaller
// larger smaller
// larger larger
// Walls: Everything else
//
bool not_wall = (plane[0][2] == plane[1][2] && plane[0][2] == plane[2][2]);
bool not_floor = plane[0][1] < plane[2][1];
// Choose a substitution texture from the right category.
std::string cat = "\"" + color + (not_wall ? (not_floor ? "Ceiling" : "Floor") : "Wall") + "\"";
auto v = texture_substitutions.find(cat);
if(v == texture_substitutions.end()) continue;
// Choose randomly from the selection.
std::size_t pos = std::rand() % v->second.size();
// Substitute the texture!
if(debug) LogPrintf("vbsp_bisqwit: Changed %s into %s\n", mat.c_str(), v->second[pos].c_str());
mat = v->second[pos];
}
}
// Check out all "func_instance"s.
for(const auto& a: modified_vmf.value_keys) if(a.first == "entity")
{
std::string classname, targetname;
std::size_t item_index = 0;
for(const auto& b: a.second->value_keys)
{
if(b.first == "\"classname\"") classname = b.second->value_string;
if(b.first == "\"targetname\"") targetname = b.second->value_string;
}
if(!targetname.empty() && targetname != "\"\"")
{
// Find the possible number in the end of the targetname.
// This describes the .p2c item number.
for(std::size_t p=targetname.size()-1, mul=1; p > 0 && std::isdigit(targetname[p-1]); mul *= 10)
item_index += mul * (targetname[--p] - '0');
}
if(classname == "\"func_instance\"")
for(auto& b: a.second->value_keys)
if(b.first == "\"file\"")
{
std::string& file = b.second->value_string;
/*LogPrintf("vbsp_bisqwit: Instance %s (%s) = %u\n",
file.c_str(),
targetname.c_str(),
item_index);*/
// Add the bspzip (packer) items that we need
auto r = instance_dependent_packer_conditions.equal_range(file);
while(r.first != r.second) enabled_packer_conditions.insert( r.first++->second );
// Check if it's a glass panel ("FixGlass" feature from StylesMod)
if(item_index < p2c_items.size())
{
const auto& t = p2c_items[item_index];
auto i = itemtype_vmfs.find(t.first);
if(t.first == "\"ITEM_BARRIER\"" && i != itemtype_vmfs.end() && file == i->second[0])
{
std::string path, fn;
SplitFN(file.substr(1), path, fn);
std::string newfile =
(t.second == 0) ? (path + "/realglass_128x128.vmf")
: (path + "/grating_128x128.vmf");
if(debug) LogPrintf("vbsp_bisqwit: Instance[%s] changed %s into %s\n", targetname.c_str(), file.c_str(), newfile.c_str());
file = "\"" + newfile + "\"";
}
}
}
}
}
// Save the modified VMF.
// Requires: TranslateVMF() has been run.
void MakeVMF()
{
std::string new_vmf_fn = original_vmf_path + "/styled/" + original_vmf_fn;
std::string new_vmf = modified_vmf.Dump(~0u);
LogPrintf("vbsp_bisqwit: Writing %s (%u bytes)\n", new_vmf_fn.c_str(), (unsigned)new_vmf.size());
FILE* fp = std::fopen(new_vmf_fn.c_str(), "wb");
if(!fp)
{
std::perror(new_vmf_fn.c_str());
exit_code = 1;
return;
}
int r = std::fwrite(new_vmf.data(), 1, new_vmf.size(), fp);
if(r != (int) new_vmf.size())
{
std::perror(new_vmf_fn.c_str());
exit_code = 2;
}
std::fclose(fp);
}
// Create the packing list for BSPZIP.
// Requires: enabled_packer_conditions
// Modified by FindPackerConditions, FindUsedItemTypes and TranslateVMF.
void MakePackerList()
{
std::string packlist_pathfn = original_vmf_path + "/" + ChangeExt(original_vmf_fn, ".filelist.txt");
LogPrintf("vbsp_bisqwit: Making packing list for bspzip: %s\n", packlist_pathfn.c_str());
// Build a list of renames:
std::unordered_map<std::string, std::string> renames;
std::vector<std::pair<std::string, std::string>> items;
for(const auto& s: enabled_packer_conditions)
{
std::size_t semicolon_pos = s.find(';');
if(semicolon_pos == s.npos)
items.push_back( {s,s} );
else
renames.insert( { s.substr(0,semicolon_pos),
s.substr(semicolon_pos+1) });
}
// Sort the items. This is completely unnecessary.
std::sort(items.begin(), items.end());
// For each item, apply the rename rules.
for(auto& s: items)
{
auto i = renames.find(s.first);
if(i != renames.end())
{
std::string substitution = i->second;
// TODO: Replace "mapnamehere" with something.
// Since I could not find an example where "particles.txt" actually gets
// generated, I did not have a testcase showing what should actually be done here.
s.second = substitution;
}
}
// Now put them into the file.
std::FILE* fp = /*fopen(packlist_pathfn.c_str(), "at");
if(!fp) std::fprintf(stderr, "vbsp_bisqwit: (Appending!)\n");
if(!fp && errno == ENOENT) fp = */fopen(packlist_pathfn.c_str(), "wt");
if(!fp)
{
std::perror(packlist_pathfn.c_str());
return;
}
for(const auto& s: items)
{
std::fprintf(fp, "%s\n", s.second.c_str());
// Now figure out where this file actually is.
std::string f = game_param + "/" + s.first;
if(access(f.c_str(), R_OK)) {
f = game_param + "/../portal2_dlc2/" + s.first;
if(access(f.c_str(), R_OK)) {
f = game_param + "/../portal2_dlc1/" + s.first;
if(access(f.c_str(), R_OK)) {
f = game_param + "/../sdk_content/" + s.first;
if(access(f.c_str(), R_OK)) {
LogPrintf("vbsp_bisqwit: ERROR: Could not figure out where %s is\n", s.first.c_str());
}}}}
std::fprintf(fp, "%s\n", f.c_str());
}
std::fclose(fp);
}
int running_pid = 0;
// Run VBSP.
// Requires: MakeVMF() has been run.
void RunVBSP()
{
std::string new_vmf_fn = original_vmf_path + "/styled/" + original_vmf_fn;
std::string g = AddQuotes(game_param);
std::string f = AddQuotes(new_vmf_fn);
std::vector<const char*> params { "vbsp", "-game", g.c_str(), f.c_str(), NULL };
LogPrintf("vbsp_bisqwit: Running vbsp_original:");
for(const char* s: params)
if(s)LogPrintf(" %s", s);
LogPrintf("\n");
/* TODO: Figure out how to make this work without spawning a spurious console window. */
running_pid = _spawnvp(P_NOWAIT, "vbsp_original.exe", &params[0]);
/* We use P_NOWAIT instead of P_WAIT because if our program
* is killed, we need to also kill the child and Windows does
* not do that automatically. Thus we need the PID.
*/
int s;
_cwait(&s, running_pid, 0/*NULL*/); running_pid = 0;
if(s != 0)
{
LogPrintf("vbsp_bisqwit: VBSP failed!\n");
exit_code = s;
return;
}
}
// Run BSPZIP.
// Requires: MakePackerList() and RUNVBSP() have been run.
void RunBSPZIP()
{
std::string packlist_pathfn = original_vmf_path + "/" + ChangeExt(original_vmf_fn, ".filelist.txt");
std::string new_bsp_fn = original_vmf_path + "/styled/" + ChangeExt(original_vmf_fn, ".bsp");
std::string goal_bsp_fn = original_vmf_path + "/" + ChangeExt(original_vmf_fn, ".bsp");
std::string g = AddQuotes(game_param);
std::string n = AddQuotes(new_bsp_fn), o = AddQuotes(goal_bsp_fn), l = AddQuotes(packlist_pathfn);
std::vector<const char*> params { "bspzip", "-addlist", n.c_str(), l.c_str(), o.c_str(),
"-game", g.c_str(), NULL };
LogPrintf("vbsp_bisqwit: Running bspzip:");
for(const char* s: params)
if(s)LogPrintf(" %s", s);
LogPrintf("\n");
/* TODO: Figure out how to make this work without spawning a spurious console window. */
running_pid = _spawnvp(P_NOWAIT, "bspzip.exe", &params[0]);
/* Again, using P_NOWAIT instead of P_WAIT
* for the same reason as in RunVBSP.
*/
int s;
_cwait(&s, running_pid, 0/*NULL*/); running_pid = 0;
if(s != 0)
{
LogPrintf("vbsp_bisqwit: BSPZIP failed!\n");
}
// VBSP generates a .PRT file in the same directory as the .BSP file.
// This PRT file is required by VVIS, so we also move it to the right place.
std::string new_prt_fn = original_vmf_path + "/styled/" + ChangeExt(original_vmf_fn, ".prt");
std::string goal_prt_fn = original_vmf_path + "/" + ChangeExt(original_vmf_fn, ".prt");
if(unlink(goal_prt_fn.c_str()) == -1 && errno != ENOENT)
std::perror(goal_prt_fn.c_str());
rename(new_prt_fn.c_str(), goal_prt_fn.c_str());
// Do the same to the LOG file. Because _we_ also log into that same file,
// we must close the log and reopen it afterwards.
std::string new_log_fn = original_vmf_path + "/styled/" + ChangeExt(original_vmf_fn, ".log");
std::string goal_log_fn = original_vmf_path + "/" + ChangeExt(original_vmf_fn, ".log");
if(log_fp) { std::fclose(log_fp); log_fp = NULL; }
log_fn = goal_log_fn;
if(unlink(goal_log_fn.c_str()) == -1 && errno != ENOENT)
std::perror(goal_log_fn.c_str());
rename(new_log_fn.c_str(), goal_log_fn.c_str());
}
// Some stuff to try and make Windows kill the child program
// when the parent dies; something which is simple in unix
// systems but seems overly complicated in Windows.
extern "C" { static void KillChildren(int); }
static void KillChildren(int n)
{
std::signal(SIGTERM, SIG_DFL);
if(n != SIGTERM)
LogPrintf("vbsp_bisqwit: Signal received, terminating\n");
if(running_pid)
{
LogPrintf("vbsp_bisqwit: Attempting to kill PID %d\n", running_pid);
/* Kill the child program. For some reason killing another program
* is really difficult in Windows -- at least I could not find any
* information in MSDN documentation on how to do it, so I'll
* externalize the task to this Cygwin program.
*/
char Buf[256];
std::sprintf(Buf, "c:/cygwin/bin/kill.exe -f %d", running_pid);
std::system(Buf);
}
if(n == -515) _exit(exit_code);
std::exit(exit_code);
}
static struct Terminator { ~Terminator() { KillChildren(-515); } } Terminator;
int main(int argc, char** argv)
{
std::string cmdline;
for(int a=0; a<argc; ++a) { cmdline += " " + AddQuotes(argv[a]); }
std::signal(SIGINT, KillChildren);
std::signal(SIGTERM, KillChildren);
for(int a=1; a<argc; ++a)
{
std::string s = argv[a];
if(s == "-game") game_param = argv[++a];
else if(s == "-debug") debug = true;
else original_vmf_pathfn = argv[a];
}
SplitFN(original_vmf_pathfn, original_vmf_path, original_vmf_fn);
log_fn = original_vmf_path + "/styled/" + ChangeExt(original_vmf_fn, ".log");
unlink(log_fn.c_str());
LogPrintf("vbsp_bisqwit: Commandline: %s\n", cmdline.c_str());
LogPrintf("vbsp_bisqwit: VMF file is %s in %s\n",
original_vmf_fn.c_str(),
original_vmf_path.c_str());
std::string p2c_path = game_param + "puzzles";
std::string p2c_code;
DIR* d = opendir(p2c_path.c_str());
if(!d)
{
std::perror(p2c_path.c_str());
std::exit(3);
}
while(dirent* e = readdir(d))
{
p2c_code = e->d_name;
if(p2c_code.empty() || p2c_code[0]=='.') continue;
p2c_path += '/';
p2c_path += p2c_code;
break;
}
closedir(d);
std::string p2c_fn = (original_vmf_fn == "preview.vmf")
? "autosave.p2c"
: ChangeExt(original_vmf_fn, ".p2c");
LogPrintf("vbsp_bisqwit: P2C file is %s in %s\n",
p2c_fn.c_str(), p2c_path.c_str());
std::string p2c_pathfn = p2c_path;
p2c_pathfn += '/';
p2c_pathfn += p2c_fn;
style_path = original_vmf_path + "/styled";
mkdir(style_path.c_str() /*, 0755 */);
LogPrintf("vbsp_bisqwit: Style path is %s\n", style_path.c_str());
editoritems_pathfn = game_param + "../portal2_dlc2/scripts/editoritems.txt";
LogPrintf("vbsp_bisqwit: EditorItems is %s\n", editoritems_pathfn.c_str());
#pragma omp parallel sections
{
#pragma omp section
{original_vmf = ParseValveFile(original_vmf_pathfn);}
#pragma omp section
{original_p2c = ParseValveFile(p2c_pathfn);}
#pragma omp section
{editoritems = ParseValveFile(editoritems_pathfn);
FindPackerConditions();
FindUsedItemTypes();
}
}
if(exit_code) std::exit(exit_code);
// LogPrintf("vbsp_bisqwit: Debug-dump of files\n");
// original_vmf.Dump();
// original_p2c.Dump();
// editoritems.Dump();
// TranslateVMF must be run in a single thread after both
// FindPackerConditions and FindUsedItemTypes have been run.
TranslateVMF();
#pragma omp parallel sections num_threads(3)
{
#pragma omp section
{
MakeVMF();
if(exit_code == 0) RunVBSP();
}
#pragma omp section
{
MakePackerList();
}
#pragma omp section
{
if(original_vmf_fn != "preview.vmf")
{
// Within {game_root}/screenshots, find out if there is a previewcomplete.tga (or previewcomplete%04d.tga)
// that is sufficiently new. If there is, find the newest "preview%04d.jpg", stamp it
// with PlayTestedCertified.png, and put it in {puzzle_root}/{puzzle_name}.jpg
std::string screenshot_dir = game_param + "/screenshots";
// Using _findfirst/_findnext/_findclose instead of opendir/readdir,
// because the former also gives timestamps without need to call stat().
struct _finddata_t cf;
intptr_t hf;
if((hf = _findfirst( (screenshot_dir + "/preview*.*").c_str(), &cf)) != -1L)
{
std::time_t time_now = std::time(NULL);
std::time_t time_threshold = time_now - 10*60;
bool completed = false;
std::string newest_shot;
time_t newest_shot_time = 0;
do {
if(!(cf.attrib & _A_SUBDIR))
{
if(cf.time_write < time_threshold) continue;
std::string n = cf.name;
if(n.substr(0, 15) == "previewcomplete")
completed = true;
else if(cf.time_write > newest_shot_time)
{
newest_shot_time = cf.time_write;
newest_shot = n;
}
}
} while(_findnext(hf, &cf) == 0);
_findclose(hf);
{ struct tm* tm = std::localtime(&newest_shot_time);
LogPrintf("vbsp_bisqwit: Newest shot is %s, %04d-%02d-%02d %02d:%02d:%02d\n",
newest_shot.c_str(),
tm->tm_year+1900,
tm->tm_mon+1,
tm->tm_mday,
tm->tm_hour,
tm->tm_min,
tm->tm_sec);
}
if(!completed || newest_shot.empty())
LogPrintf("vbsp_bisqwit: Screenshots were not recent enough or the map was not completed within last 10 minutes\n");
else
{
const std::string& cert_fn = game_param + "/../bin/PlayTestedCertified.png";
/* I could not be bothered to recompile libgd (and all its required
* libraries such as libpng and libjpeg) for the Mingw target,
* so I just use this very handy Imagemagick program to do the job.
* Requires Cygwin, though.
*/
std::string c = AddQuotes(cert_fn);
std::string m = AddQuotes(screenshot_dir + "/" + newest_shot);
std::string t = AddQuotes(p2c_path + "/" + ChangeExt(original_vmf_fn, ".jpg"));
std::vector<const char*> params { "composite",
"-geometry", "48x48+480+230", c.c_str(),
"-resize", "555x312", m.c_str(),
"-quality", "100", t.c_str(), NULL };
LogPrintf("vbsp_bisqwit: Running composite:");
for(const char* s: params)
if(s)LogPrintf(" %s", s);
LogPrintf("\n");
/* TODO: Figure out how to make this work without spawning a spurious console window. */
_spawnvp(P_WAIT, "c:/cygwin/bin/composite.exe", &params[0]);
}
}
}
} // section for screenshot manipulation
}//parallel sections
if(exit_code) std::exit(exit_code);
// RunBSPZIP must be run in a single thread after both
// RunVBSP() and MakePackerList() have been run.
RunBSPZIP();
std::signal(SIGTERM, SIG_DFL);
if(exit_code) std::exit(exit_code);
/* Why is this done by vbsp_styles? I don't know.
* When is the file then made non-writable? Again I don't know.
*/
std::string u = p2c_path + "/untitled.jpg";
LogPrintf("vbsp_bisqwit: Making %s writable\n", u.c_str());
_chmod(u.c_str(), _S_IREAD | _S_IWRITE );
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment