Last active
June 24, 2018 14:44
-
-
Save bisqwit/9829680 to your computer and use it in GitHub Desktop.
Rewriting vbsp_styles (Portal 2 Puzzlemaker BEE2)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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", ¶ms[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", ¶ms[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", ¶ms[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