Replaced ascii-doc readme with one generated by sync

This commit is contained in:
acidicoala
2025-09-11 15:11:22 +05:00
parent 8ee2d77115
commit b2ab3c3116
15 changed files with 488 additions and 353 deletions

View File

@@ -0,0 +1,124 @@
#include <filesystem>
#include <iostream>
#include <random>
#include <regex>
#include <string>
#include <koalabox/http_client.hpp>
#include <koalabox/logger.hpp>
#include <koalabox/path.hpp>
#include <koalabox/str.hpp>
#include <koalabox/zip.hpp>
namespace {
namespace fs = std::filesystem;
namespace kb = koalabox;
std::string generate_random_string() {
static constexpr char charset[] = "0123456789"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz";
thread_local std::mt19937_64 rng{std::random_device{}()};
thread_local std::uniform_int_distribution<std::size_t> dist(0, sizeof(charset) - 2);
constexpr auto length = 16;
std::string result;
result.reserve(length);
for(std::size_t i = 0; i < length; ++i) {
result += charset[dist(rng)];
}
return result;
}
void print_help() {
std::cout << "Steamworks SDK downloader for SmokeAPI v1.0" << std::endl
<< "Usage: steamworks_downloader version1 version2 ... versionN" << std::endl
<< "Example: steamworks_downloader 100 158a 162" << std::endl
<< "Alternative usage: steamworks_downloader C:/path/to/downloaded_sdk/"
<< "SDK version list available at: "
<< "https://partner.steamgames.com/downloads/list" << std::endl;
}
void unzip_sdk(const fs::path& zip_file_path, const fs::path& unzip_dir) {
kb::zip::extract_files(
zip_file_path,
[&](const std::string& name, const bool) {
if(name.starts_with("sdk/public/steam/") && name.ends_with(".h")) {
return unzip_dir / "headers/steam" / fs::path(name).filename();
}
if(name.starts_with("sdk/redistributable_bin/") && name.ends_with(".dll") &&
name.find("steam_api") != std::string::npos) {
return unzip_dir / "binaries" / fs::path(name).filename();
}
return fs::path();
}
);
}
void download_sdk(const fs::path& steamworks_dir, const std::string_view& version) {
const auto download_url = std::format(
"https://github.com/acidicoala/cdn/raw/refs/heads/main/valve/steamworks_sdk_{}.zip",
version
);
const auto zip_file_path = fs::temp_directory_path() / (generate_random_string() + ".zip");
kb::http_client::download_file(download_url, zip_file_path);
try {
const auto unzip_dir = steamworks_dir / version;
unzip_sdk(zip_file_path, unzip_dir);
} catch(std::exception& e) {
std::cerr << "Unzip error: " << e.what() << std::endl;
}
fs::remove(zip_file_path);
}
} // namespace
/**
* A tool for downloading Steamworks SDK and unpacking its headers and binaries
* for further processing by other tools.
*/
int wmain(const int argc, const wchar_t** argv) { // NOLINT(*-use-internal-linkage)
if(argc == 1) {
print_help();
return 0;
}
const auto steamworks_dir = std::filesystem::current_path() / "steamworks";
// Special case. If there is a directory with a bunch of SDKs downloaded,
// then we can just provide it as a single argument
if(argc == 2) {
if(const auto cdn_dir = kb::str::to_str(argv[1]); fs::is_directory(cdn_dir)) {
for(const auto& entry : fs::directory_iterator(cdn_dir)) {
const auto filename = kb::path::to_str(entry.path().filename());
const std::regex re(R"(steamworks_sdk_(.+)\.zip)");
if(std::smatch match; std::regex_match(filename, match, re)) {
if(match.size() > 1) {
const auto& version = match[1].str();
unzip_sdk(entry.path(), steamworks_dir / version);
}
}
}
return 0;
}
}
for(auto i = 1; i < argc; i++) {
const auto version = kb::str::to_str(argv[i]);
try {
download_sdk(steamworks_dir, version);
} catch(const std::exception& e) {
LOG_ERROR("Error downloading SDK '{}'. Reason: {}", version, e.what());
}
}
}

View File

@@ -0,0 +1,258 @@
#include <chrono>
#include <deque>
#include <filesystem>
#include <fstream>
#include <functional>
#include <set>
#include <BS_thread_pool.hpp>
#include <cpp-tree-sitter.h>
#include <nlohmann/json.hpp>
#include <koalabox/io.hpp>
#include <koalabox/logger.hpp>
#include <koalabox/parser.hpp>
#include <koalabox/path.hpp>
#include <koalabox/str.hpp>
namespace {
namespace fs = std::filesystem;
namespace kb = koalabox;
std::string_view unquote_if_quoted(const std::string_view& s) {
if(s.size() >= 2 && s.front() == '"' && s.back() == '"') {
return s.substr(1, s.size() - 2);
}
// Not a plain quoted string (might be raw/user-defined). Return as-is.
return s;
}
void parse_header(const std::string_view& source, nlohmann::ordered_json& lookup) {
const auto tree = kb::parser::parse_source(source);
const auto root = tree.getRootNode();
nlohmann::ordered_json current_lookup = {};
std::string interface_version;
kb::parser::walk(
root,
[&](const auto& current_node) {
const auto current_type = current_node.getType();
const auto current_value = current_node.getSourceRange(source);
const auto current_sexpr = current_node.getSExpr();
if(current_type == "class_specifier") {
std::string interface_name;
[[maybe_unused]] int vt_idx = 0;
kb::parser::walk(
current_node,
[&](const ts::Node& class_node) {
const auto type = class_node.getType();
const auto value = class_node.getSourceRange(source);
if(type == "type_identifier" && interface_name.empty()) {
interface_name = value;
LOG_DEBUG("Found interface: {}", interface_name);
return kb::parser::visit_result::Continue;
}
if(type == "field_declaration" && value.starts_with("virtual ")) {
if(value.starts_with("virtual ")) {
kb::parser::walk(
class_node,
[&](const ts::Node& decl_node) {
if(decl_node.getType() == "field_identifier") {
const auto function_name = decl_node.getSourceRange(
source
);
// Note: This doesn't take into account overloaded functions.
// However, so far this project hasn't had any need to hook such
// functions. Hence, no fixes have been implemented so far.
current_lookup[function_name] = vt_idx++;
return kb::parser::visit_result::Stop;
}
return kb::parser::visit_result::Continue;
}
);
}
return kb::parser::visit_result::SkipChildren;
}
return kb::parser::visit_result::Continue;
}
);
} else if(current_type == "preproc_def") {
kb::parser::walk(
current_node,
[&](const ts::Node& preproc_node) {
if(preproc_node.getType() == "identifier") {
const auto identifier = preproc_node.getSourceRange(source);
return identifier.ends_with("INTERFACE_VERSION")
? kb::parser::visit_result::Continue
: kb::parser::visit_result::Stop;
}
if(preproc_node.getType() == "preproc_arg") {
const auto quoted_version = preproc_node.getSourceRange(source);
const auto trimmed_version = koalabox::str::trim(quoted_version);
interface_version = unquote_if_quoted(trimmed_version);
LOG_DEBUG("Interface version: {}", interface_version);
return kb::parser::visit_result::Stop;
}
return kb::parser::visit_result::Continue;
}
);
} else if(current_type == "translation_unit" || current_type == "preproc_ifdef") {
return kb::parser::visit_result::Continue;
}
return kb::parser::visit_result::SkipChildren;
}
);
// Save the findings
static std::mutex section;
if(not
interface_version.empty()
) {
const std::lock_guard lock(section);
lookup[interface_version] = current_lookup;
}
}
/**
* Certain Steam macros break C++ AST parser, if left unprocessed.
* This function does that in a very naive manner. Stupid, but works.
*/
std::string manually_preprocess_header(const fs::path& header_path) {
const auto header_contents = kb::io::read_file(header_path);
// language=RegExp
const std::regex re(R"(STEAM_PRIVATE_API\s*\(\s*([^)]+)\s*\))");
const auto processed_contents = std::regex_replace(header_contents, re, "$1");
return processed_contents;
}
void parse_sdk(
const fs::path& sdk_path,
nlohmann::ordered_json& lookup,
BS::thread_pool<>& pool
) {
const auto headers_dir = sdk_path / "headers\\steam";
const auto headers_dir_str = kb::path::to_str(headers_dir);
if(not fs::exists(headers_dir)) {
LOG_WARN("Warning: SDK missing 'headers/steam' directory: {}", headers_dir_str);
return;
}
LOG_INFO("Parsing SDK: {}", headers_dir_str);
// Go over each file in headers directory
for(const auto& entry : fs::directory_iterator(headers_dir)) {
if(const auto& header_path = entry.path(); header_path.extension() == ".h") {
const auto task = pool.submit_task(
[&, header_path] {
try {
LOG_DEBUG("Parsing header: {}", kb::path::to_str(header_path));
const auto processed_header = manually_preprocess_header(header_path);
parse_header(processed_header, lookup);
} catch(std::exception& e) {
LOG_CRITICAL(e.what());
exit(-1);
}
}
);
}
}
}
void generate_lookup_json(
const fs::path& steamworks_dir,
//
const std::set<std::string>& sdk_filter
) {
nlohmann::ordered_json lookup;
// The thread pool noticeably speeds up the overall parsing.
// Thread count of 4 seems to yield most optimal performance benefits.
constexpr auto thread_count = 4;
LOG_INFO("Creating task pool with {} threads", thread_count);
BS::thread_pool pool(thread_count);
// Go over each steamworks sdk version
for(const auto& entry : fs::directory_iterator(steamworks_dir)) {
if(not entry.is_directory()) {
continue;
}
if(
not sdk_filter.empty() and
not sdk_filter.contains(kb::path::to_str(entry.path().filename()))
) {
continue;
}
parse_sdk(entry.path(), lookup, pool);
}
// Wait for all tasks to finish
pool.wait();
const auto interface_lookup_path = fs::path("interface_lookup.json");
std::ofstream lookup_output(interface_lookup_path);
lookup_output << std::setw(4) << lookup;
LOG_INFO(
"Interface lookup generated at: {}",
kb::path::to_str(fs::absolute(interface_lookup_path))
);
}
}
/**
* A tool for parsing Steamworks headers and generating a lookup map of its interfaces.
* Optionally accepts a list of folder names that filters which sdk versions will be parsed.
* No list means all versions will be parsed.
*/
int wmain(const int argc, const wchar_t* argv[]) { // NOLINT(*-use-internal-linkage)
try {
koalabox::logger::init_console_logger();
std::set<std::string> sdk_filter;
if(argc > 1) {
for(auto i = 1; i < argc; i++) {
const auto version = koalabox::str::to_str(argv[i]);
sdk_filter.insert(version);
}
}
const auto steamworks_dir = fs::path("steamworks");
if(!fs::exists(steamworks_dir)) {
throw std::exception("Expected to find 'steamworks' in current working directory.");
}
const auto start = std::chrono::steady_clock::now();
generate_lookup_json(steamworks_dir, sdk_filter);
const auto end = std::chrono::steady_clock::now();
const auto elapsed = duration_cast<std::chrono::seconds>(end - start);
LOG_INFO("Finished parsing steamworks in {} seconds", elapsed.count());
} catch(std::exception& e) {
LOG_CRITICAL("Error: {}", e.what());
return 1;
}
}