diff --git a/src/client/component/gui/asset_list.hpp b/src/client/component/gui/asset_list.hpp index 6d87154f..30a62e10 100644 --- a/src/client/component/gui/asset_list.hpp +++ b/src/client/component/gui/asset_list.hpp @@ -9,7 +9,7 @@ namespace gui::asset_list void add_view_button(int id, game::XAssetType type, const char* name); template - void add_asset_view(game::XAssetType type, const std::function& draw_callback) + void add_asset_view(game::XAssetType type, const std::function& draw_callback) { static std::unordered_set opened_assets; add_asset_view_callback(type, [](const std::string& name) @@ -32,7 +32,10 @@ namespace gui::asset_list auto is_open = true; if (ImGui::Begin(name.data(), &is_open)) { - draw_callback(header); + if (!draw_callback(header)) + { + is_open = false; + } } ImGui::End(); diff --git a/src/client/component/gui/assets/mapents.cpp b/src/client/component/gui/assets/mapents.cpp new file mode 100644 index 00000000..44de595b --- /dev/null +++ b/src/client/component/gui/assets/mapents.cpp @@ -0,0 +1,146 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/game.hpp" +#include "game/dvars.hpp" + +#include "component/scheduler.hpp" +#include "component/command.hpp" +#include "component/fastfiles.hpp" +#include "component/gsc/script_loading.hpp" +#include "../gui.hpp" +#include "../asset_list.hpp" + +#include "utils/mapents.hpp" + +#include +#include +#include +#include + +namespace gui::asset_list::mapents +{ + namespace + { + using entity_t = std::vector>; + + struct mapents_t + { + game::MapEnts* asset; + std::string converted_mapents; + std::thread parse_thread; + std::atomic_bool is_parsing = false; + std::atomic_bool done_parsing = false; + }; + + std::unordered_set temp_files; + + utils::concurrency::container mapents; + + void parse_mapents(game::MapEnts* asset, mapents_t& data) + { + data.is_parsing = true; + data.asset = asset; + const auto str_data = std::string{asset->entityString, static_cast(asset->numEntityChars)}; + data.parse_thread = std::thread([=] + { + const auto new_data = ::mapents::parse(str_data, [](const std::uint16_t id) + { + return gsc::gsc_ctx->token_name(id); + }); + + std::string converted_mapents; + for (const auto& entity : new_data.entities) + { + converted_mapents.append("{\n"); + const auto var_list = entity.get_var_list(); + for (const auto& var : var_list) + { + if (var.sl_string) + { + converted_mapents.append( + utils::string::va("0 \"%s\" \"%s\"\n", var.key.data(), var.value.data())); + } + else + { + converted_mapents.append( + utils::string::va("\"%s\" \"%s\"\n", var.key.data(), var.value.data())); + } + } + converted_mapents.append("}\n"); + } + + mapents.access([=](mapents_t& data) + { + data.is_parsing = false; + data.done_parsing = true; + data.converted_mapents = converted_mapents; + }); + }); + } + + void draw_entity_string(game::MapEnts* asset) + { + ImGui::InputTextMultiline("entity_string", asset->entityString, asset->numEntityChars, + ImGui::GetWindowSize(), ImGuiInputTextFlags_ReadOnly); + } + + bool create_and_open_mapents_file(mapents_t& data) + { + const auto current_path = std::filesystem::current_path().generic_string(); + const std::string path = utils::string::va("%s\\h2-mod\\tmp\\%s", current_path.data(), data.asset->name); + temp_files.insert(path); + + utils::io::write_file(path, data.converted_mapents, false); + ShellExecuteA(nullptr, nullptr, path.data(), nullptr, nullptr, SW_SHOWNORMAL); + + return false; + } + + bool draw_asset(game::MapEnts* asset) + { + return mapents.access([&](mapents_t& data) + { + if (!data.is_parsing && data.done_parsing) + { + if (data.parse_thread.joinable()) + { + data.parse_thread.join(); + } + else + { + create_and_open_mapents_file(data); + return false; + } + } + + if (!data.is_parsing && !data.done_parsing) + { + parse_mapents(asset, data); + } + + ImGui::Text("Parsing mapents..."); + return true; + }); + } + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + gui::asset_list::add_asset_view(game::ASSET_TYPE_MAP_ENTS, draw_asset); + } + + void pre_destroy() override + { + for (const auto& file : temp_files) + { + utils::io::remove_file(file); + } + } + }; +} + +REGISTER_COMPONENT(gui::asset_list::mapents::component) diff --git a/src/client/component/gui/assets/material.cpp b/src/client/component/gui/assets/material.cpp index f029bf85..43ddd3b0 100644 --- a/src/client/component/gui/assets/material.cpp +++ b/src/client/component/gui/assets/material.cpp @@ -46,7 +46,7 @@ namespace gui::asset_list::material return image_type_names[type]; } - void draw_material_window(game::Material* asset) + bool draw_material_window(game::Material* asset) { ImGui::SetNextItemOpen(true, ImGuiCond_FirstUseEver); if (ImGui::TreeNode("textures")) @@ -99,6 +99,8 @@ namespace gui::asset_list::material DRAW_ASSET_PROPERTY(materialType, "%i"); DRAW_ASSET_PROPERTY(layerCount, "%i"); DRAW_ASSET_PROPERTY(assetFlags, "%X"); + + return true; } } diff --git a/src/client/component/gui/assets/xmodel.cpp b/src/client/component/gui/assets/xmodel.cpp index 51831353..bfbf7921 100644 --- a/src/client/component/gui/assets/xmodel.cpp +++ b/src/client/component/gui/assets/xmodel.cpp @@ -177,7 +177,7 @@ namespace gui::asset_list::xmodel } } - void draw_xmodel_window(game::XModel* asset) + bool draw_xmodel_window(game::XModel* asset) { static bool flip_axis = false; ImGui::Checkbox("flip axis", &flip_axis); @@ -252,8 +252,17 @@ namespace gui::asset_list::xmodel if (ImGui::TreeNode("surface materials")) { + game::Material* prev_material = nullptr; + for (auto i = 0; i < asset->numsurfs; i++) { + if (prev_material == asset->materialHandles[i]) + { + continue; + } + + prev_material = asset->materialHandles[i]; + if (ImGui::Button(asset->materialHandles[i]->name)) { gui::copy_to_clipboard(asset->materialHandles[i]->name); @@ -275,11 +284,15 @@ namespace gui::asset_list::xmodel { gui::copy_to_clipboard(asset->compositeModels[i]->name); } + + gui::asset_list::add_view_button(i, game::ASSET_TYPE_XMODEL, asset->compositeModels[i]->name); } ImGui::TreePop(); } } + + return true; } } diff --git a/src/client/utils/mapents.cpp b/src/client/utils/mapents.cpp new file mode 100644 index 00000000..549b088a --- /dev/null +++ b/src/client/utils/mapents.cpp @@ -0,0 +1,163 @@ +#include + +#include "mapents.hpp" + +#include + +namespace mapents +{ + void mapents_entity::add_var(const spawn_var& var) + { + this->vars.push_back(var); + } + + std::string mapents_entity::get(const std::string& key) const + { + for (const auto& var : this->vars) + { + if (var.key == key) + { + return var.value; + } + } + + return ""; + } + + std::vector mapents_entity::get_var_list() const + { + return this->vars; + } + + void mapents_entity::clear() + { + this->vars.clear(); + } + + mapents_list parse(const std::string& data, const token_name_callback& get_token_name) + { + mapents_list list; + mapents_entity current_entity; + + const auto lines = utils::string::split(data, '\n'); + auto in_map_ent = false; + auto in_comment = false; + + for (auto i = 0; i < lines.size(); i++) + { + auto line = lines[i]; + if (line.ends_with('\r')) + { + line.pop_back(); + } + + if (line.starts_with("/*") || line.ends_with("/*")) + { + in_comment = true; + continue; + } + + if (line.starts_with("*/") || line.ends_with("*/")) + { + in_comment = false; + continue; + } + + if (in_comment || line.starts_with("//") || line.empty()) + { + continue; + } + + if (line[0] == '{' && !in_map_ent) + { + current_entity.clear(); + in_map_ent = true; + continue; + } + + if (line[0] == '{' && in_map_ent) + { + throw std::runtime_error(utils::string::va("Unexpected '{' on line %i", i)); + } + + if (line[0] == '}' && in_map_ent) + { + list.entities.push_back(current_entity); + in_map_ent = false; + continue; + } + + if (line[0] == '}' && !in_map_ent) + { + throw std::runtime_error(utils::string::va("Unexpected '}' on line %i", i)); + } + + if (line[0] == '\n' || line[0] == '\0' || line[0] == '\n') + { + continue; + } + + spawn_var var{}; + + if (line.starts_with("0 \"")) + { + std::regex expr(R"~(0 "(.+)" "(.*)")~"); + std::smatch match{}; + if (!std::regex_search(line, match, expr)) + { + throw std::runtime_error(utils::string::va("Failed to parse line %i (%s)", i, line.data())); + continue; + } + + var.key = utils::string::to_lower(match[1].str()); + var.value = match[2].str(); + var.sl_string = true; + } + else + { + std::regex expr(R"~((.+) "(.*)")~"); + std::smatch match{}; + if (!std::regex_search(line, match, expr)) + { + throw std::runtime_error( + utils::string::va("Failed to parse line %i (%s)", i, line.data())); + continue; + } + + var.key = utils::string::to_lower(match[1].str()); + var.value = match[2].str(); + + if (utils::string::is_numeric(var.key) && !var.key.starts_with("\"") && !var.key.ends_with("\"")) + { + var.key = get_token_name(static_cast(std::atoi(var.key.data()))); + } + else if (var.key.starts_with("\"") && var.key.ends_with("\"") && var.key.size() >= 3) + { + var.key = var.key.substr(1, var.key.size() - 2); + } + else + { + throw std::runtime_error( + utils::string::va("Invalid key ('%s') on line %i (%s)", var.key.data(), i, line.data())); + continue; + } + } + + if (var.key.size() <= 0) + { + throw std::runtime_error( + utils::string::va("Invalid key ('%s') on line %i (%s)", var.key.data(), i, line.data())); + continue; + } + + if (var.value.size() <= 0) + { + continue; + } + + current_entity.add_var(var); + } + + return list; + } +} diff --git a/src/client/utils/mapents.hpp b/src/client/utils/mapents.hpp new file mode 100644 index 00000000..55e62dc3 --- /dev/null +++ b/src/client/utils/mapents.hpp @@ -0,0 +1,34 @@ +#pragma once + +namespace mapents +{ + using token_name_callback = std::function; + using token_id_callback = std::function; + + struct spawn_var + { + std::string key; + std::string value; + bool sl_string; + }; + + class mapents_entity + { + public: + void clear(); + void add_var(const spawn_var& var); + + std::string get(const std::string& key) const; + std::vector get_var_list() const; + + private: + std::vector vars; + }; + + struct mapents_list + { + std::vector entities; + }; + + mapents_list parse(const std::string& data, const token_name_callback& token_name); +} diff --git a/src/common/utils/string.cpp b/src/common/utils/string.cpp index 67a03bf4..43c37e3f 100644 --- a/src/common/utils/string.cpp +++ b/src/common/utils/string.cpp @@ -242,4 +242,9 @@ namespace utils::string return *b_ == '\0'; } + + bool is_numeric(const std::string& text) + { + return std::to_string(atoi(text.data())) == text; + } } diff --git a/src/common/utils/string.hpp b/src/common/utils/string.hpp index 447a6960..7033e161 100644 --- a/src/common/utils/string.hpp +++ b/src/common/utils/string.hpp @@ -105,4 +105,6 @@ namespace utils::string std::string truncate(const std::string& text, const size_t length, const std::string& end); bool strstr_lower(const char* a, const char* b); + + bool is_numeric(const std::string& text); }