Начинающий
- Статус
- Оффлайн
- Регистрация
- 3 Фев 2020
- Сообщения
- 36
- Реакции
- 8
Всем привет. Это гайд, как реализовать полноценный SVG icons parser двумя методами.
// credits ( no ad ):
//
//
// nano svg
Метод 1. External / Internal
Для начала, нам необходимо загрузить в память .vpk файл, в котором хранятся ресурсы игры (например иконки). Для этого воспользуемся небольшой библиотекой
Для иконок нас интересует pak01_dir.vpk, лежащий по пути Counter-Strike Global Offensive\game\csgo\pak01_dir.vpk
Один раз в инициализации вашего чита:
Теперь мы имеем загруженный в память .vpk архив. Все иконки внутри этого архива располагаются по пути panorama/images/icons/equipment/ . Примерный полный путь до иконки panorama/images/icons/equipment/some_weapon.vsvg_c . Для получения имени оружия (some_weapon) мы можем взять m_szName из CCSWeaponBaseVData и подогнать под наш формат ( m_szName имеет вид weapon_some_weapon )
Далее подгружает конкретную иконку в валвовском формате .vsvg_c, используя метод get_file c_vpk_archive
Теперь нам нужно спарсить из .vsvg_c, .svg иконку, используя этот метод
Далее парсим svg через библиотеку nano svg и растеризуем иконку в rgba формат
И в конце создаем dx11 текстуру для рендера
Готово! Теперь мы можем рендерить нашу иконку
Метод 2. Internal only
В этом методе мы будем использовать функцию из panorama.dll для растеризации .svg иконки в rgba формат:
Парсите иконку аналогично из первого метода, получая готовый svg файл. После чего растеризуете:
И получается dx11 текстуру
И также рендерите:
P.S
Важно отметить, что этап с ручным парсингом vpk файла и конвертацией валвовской vsvg_c в svg, можно заменить вызовом внутриигровой функцией из client.dll, однако найти ее у меня не получилось.
Спасибо за внимание!
// credits ( no ad ):
//
Пожалуйста, авторизуйтесь для просмотра ссылки.
//
Пожалуйста, авторизуйтесь для просмотра ссылки.
// nano svg
Метод 1. External / Internal
Для начала, нам необходимо загрузить в память .vpk файл, в котором хранятся ресурсы игры (например иконки). Для этого воспользуемся небольшой библиотекой
c_vpk.h:
#pragma once
#include <fstream>
#include <string>
#include <optional>
#include <unordered_map>
#include "../../include/creep/creep_str.hpp"
#pragma pack(push,1)
struct vpk_header_t
{
uint32_t signature;
uint32_t version;
uint32_t tree_size;
uint32_t file_data_section_size;
uint32_t archive_md5_section_size;
uint32_t other_md5_section_size;
uint32_t signature_section_size;
};
struct dir_entry_t
{
uint32_t crc;
uint16_t preload_bytes;
uint16_t archive_index;
uint32_t entry_offset;
uint32_t entry_length;
uint16_t terminator;
};
#pragma pack(pop)
class c_vpk_entry
{
public:
c_vpk_entry() = default;
c_vpk_entry(const std::string& base_file, const std::string& path, const dir_entry_t& entry) :_base_file(base_file), _path(path), _entry(entry) {}
std::optional<std::vector<uint8_t>> get_data()
{
// Didn't add error handling here, as for my usage there is no need to.
size_t [I]dir_pos = _base_file.find(creep[/I]("dir"));
std::string old_number_string = std::to_string(_entry.archive_index);
std::string vpk_file = _base_file.replace(_base_file.begin() + _dir_pos, _base_file.begin() + _dir_pos + 3, std::string(3 - old_number_string.length(), '0') + old_number_string);
std::ifstream stream(vpk_file, std::ios::binary);
if (!stream.is_open())
return {};
uint8_t* data = new uint8_t[_entry.entry_length];
stream.seekg(_entry.entry_offset, stream.beg);
stream.read((char*)data, _entry.entry_length);
stream.close();
std::vector<uint8_t> data_vector(data, data + _entry.entry_length);
delete[] data;
return data_vector;
}
std::vector<uint8_t> preload_bytes = {};
private:
std::string _base_file = {};
std::string _path = {};
dir_entry_t _entry = {};
};
class c_vpk_archive
{
public:
bool load(const std::string& name)
{
std::ifstream stream(name, std::ios::binary);
if (!stream.is_open())
return false;
vpk_header_t file_header = {};
stream.read((char*)&file_header, sizeof(file_header));
if (file_header.signature != 0x55aa1234)
{
stream.close();
return false;
}
if (file_header.version != 2)
{
stream.close();
return false;
}
while (true)
{
std::string extension;
// read from stream until it finds the end of the string "\x00"
std::getline(stream, extension, '\x00');
if (extension.empty())
break;
while (true)
{
std::string directory;
std::getline(stream, directory, '\x00');
if (directory.empty())
break;
while (true)
{
std::string file_name;
std::getline(stream, file_name, '\x00');
if (file_name.empty())
break;
std::string path = directory + '/' + file_name + '.' + extension;
dir_entry_t dir_entry;
stream.read((char*)&dir_entry, sizeof(dir_entry));
c_vpk_entry vpk_entry(name, path, dir_entry);
if (dir_entry.preload_bytes)
{
// I don't have a use for preload bytes, so you could skip over them
// by uncommenting the line below, and commenting the other lines inside of this if statement.
//stream.seekg(dir_entry.preload_bytes, stream.cur);
// If you do find a use for them however, here's how to read them.
uint8_t* bytes = new uint8_t[dir_entry.preload_bytes];
stream.read((char*)bytes, dir_entry.preload_bytes);
vpk_entry.preload_bytes = std::vector<uint8_t>(bytes, bytes + dir_entry.preload_bytes);
delete[] bytes;
}
if (!files.count(path))
files[path] = vpk_entry;
}
}
}
stream.close();
return true;
}
std::optional<c_vpk_entry> get_file(const std::string& name)
{
if (files.count(name))
return files[name];
return {};
}
private:
std::unordered_map<std::string, c_vpk_entry> files = {};
};
Для иконок нас интересует pak01_dir.vpk, лежащий по пути Counter-Strike Global Offensive\game\csgo\pak01_dir.vpk
Один раз в инициализации вашего чита:
C++:
inline c_vpk_archive m_archive {};
auto svg_parser::initialize() noexcept -> bool
{
m_base_game_path = utils::get_csgo_game_path( true );
if ( ! m_archive.load( m_base_game_path + creep_( "pak01_dir.vpk" ) ) )
{
std::cout << creep_("Failed to load VPK file!\n");
return false;
}
return true;
}
Теперь мы имеем загруженный в память .vpk архив. Все иконки внутри этого архива располагаются по пути panorama/images/icons/equipment/ . Примерный полный путь до иконки panorama/images/icons/equipment/some_weapon.vsvg_c . Для получения имени оружия (some_weapon) мы можем взять m_szName из CCSWeaponBaseVData и подогнать под наш формат ( m_szName имеет вид weapon_some_weapon )
C++:
auto make_svg_path = [](std::string& in)
{
const std::string prefix = creep_("weapon_");
const std::string base_path = creep_("panorama/images/icons/equipment/");
const std::string extension = creep_(".vsvg_c");
if ( in.rfind(prefix, 0) == 0 )
in.erase( 0, prefix.size( ) );
in = base_path + in + extension;
};
Далее подгружает конкретную иконку в валвовском формате .vsvg_c, используя метод get_file c_vpk_archive
Код:
std::string svg_icon_path{ base_name };
make_svg_path(svg_icon_path);
auto entry_opt = m_archive.get_file( svg_icon_path );
if ( ! entry_opt.has_value() )
{
std::cout << creep_("Failed to load svg: ") << svg_icon_path << creep_("\n");
return std::string{};
}
auto data_opt = entry_opt->get_data( );
if ( ! data_opt.has_value( ) )
{
std::cout << creep_("Failed to read svg data: ") << svg_icon_path << creep_("\n");
return std::string{};
}
Теперь нам нужно спарсить из .vsvg_c, .svg иконку, используя этот метод
C++:
inline std::string parse(const std::vector<uint8_t>& file_bytes)
{
const auto header = reinterpret_cast<const valve_svg_header_t*>(file_bytes.data());
const auto data = reinterpret_cast<const valve_svg_data_t*>(file_bytes.data() + header->file_offset_to_svg_data);
const auto total_header_size = header->file_offset_to_svg_data + offsetof(valve_svg_data_t, svg_string);
auto str = std::string(file_bytes.begin(), file_bytes.end());
str = str.substr(total_header_size);
return str;
}
C++:
auto parsed_data = svg::parse( * data_opt );
Далее парсим svg через библиотеку nano svg и растеризуем иконку в rgba формат
C++:
auto svg_parser::make_texture_external(ID3D11Device* device, ID3D11DeviceContext* context, const std::string& complited_name, float desired_height) noexcept -> texture_data
{
#ifdef SVG_ICONS_EXTERNAL
NSVGimage* image = nsvgParse( const_cast<char*>( complited_name.c_str( ) ), creep_("px"), 96.0f );
if ( ! image )
{
std::cout << creep_("Failed to make svg image!\n");
return {};
}
float scale = desired_height / image->height;
int img_w = int(image->width * scale);
int img_h = int(image->height * scale);
std::vector<unsigned char> rgba(img_w * img_h * 4);
NSVGrasterizer* rast = nsvgCreateRasterizer();
nsvgRasterize(rast, image, 0, 0, scale, rgba.data(), img_w, img_h, img_w * 4);
nsvgDeleteRasterizer(rast);
nsvgDelete(image);
texture_data m_data{};
if ( ! create_svg_texture( device, rgba.data(), img_w, img_h, m_data ) )
{
std::cout << creep_("Failed to create svg texture!\n");
return {};
}
return m_data;
#endif
return {};
}
И в конце создаем dx11 текстуру для рендера
C++:
auto svg_parser::create_svg_texture(ID3D11Device* device, const unsigned char* rgba, int width, int height, texture_data& ret_data) -> bool
{
D3D11_TEXTURE2D_DESC desc = {};
desc.Width = width;
desc.Height = height;
desc.MipLevels = 1;
desc.ArraySize = 1;
desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
desc.SampleDesc.Count = 1;
desc.Usage = D3D11_USAGE_DYNAMIC;
desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
D3D11_SUBRESOURCE_DATA initData = {};
initData.pSysMem = rgba;
initData.SysMemPitch = width * 4;
initData.SysMemSlicePitch = 0;
D3D11_SHADER_RESOURCE_VIEW_DESC r{};
r.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
r.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
r.Texture2D.MipLevels = 1;
r.Texture2D.MostDetailedMip = 0;
ID3D11Texture2D* svg_texture_ret{ };
ID3D11ShaderResourceView* svg_shader_resource_ret{ };
HRESULT hr = device->CreateTexture2D( &desc, &initData, &svg_texture_ret );
if ( FAILED( hr ) )
{
ret_data = { };
return false;
}
hr = device->CreateShaderResourceView( svg_texture_ret, &r, &svg_shader_resource_ret );
if ( FAILED( hr ) )
{
ret_data = { };
return false;
}
ret_data = { svg_texture_ret, svg_shader_resource_ret, static_cast<int>(width), static_cast<int>(height) };
}
Готово! Теперь мы можем рендерить нашу иконку
C++:
m_base.m_background_draw_list->AddImage(result.m_shader, vec3_t { pos.x - (result.width / 2.f), pos.y }.im(), vec3_t{pos.x + (result.width / 2.f), pos.y + result.height}.im(), color.im());
Метод 2. Internal only
В этом методе мы будем использовать функцию из panorama.dll для растеризации .svg иконки в rgba формат:
Парсите иконку аналогично из первого метода, получая готовый svg файл. После чего растеризуете:
C++:
auto complited_name = parse_weapon_icon(base_name);
if ( complited_name.empty( ) )
return false;
std::vector<uint8_t> texture ( 0xFFFFFF );
svg::image_data_t img ( texture );
uint32_t w{}, h{};
if ( ! img.load_svg( reinterpret_cast< uint8_t * >( complited_name.data( ) ), complited_name.size( ), &w, &h ) )
return false;
const auto width_to_height = static_cast<float>( w ) / static_cast<float>( h );
texture_data m_data{};
create_svg_texture( device, reinterpret_cast<uint8_t*>( complited_name.data( ) ), complited_name.size( ), desired_height ? static_cast<uint32_t>(width_to_height * desired_height) : 0, desired_height, m_data );
m_textures.emplace( fnv1a::hash_64(base_name) + static_cast<uint64_t>(desired_height), m_data );
return true;
C++:
struct image_data_t
{
image_data_t(std::vector<uint8_t>& buf)
{
reinterpret_cast<void* (__fastcall*)(void*, int, int, int)>(g_dumper->dump()[FNV32("utl_buffer_init")].as<void*>())(pad, 0, 0, 0);
*reinterpret_cast<uintptr_t*>(pad) = reinterpret_cast<uintptr_t>(buf.data());
*reinterpret_cast<uint32_t*>(reinterpret_cast<uintptr_t>(pad) + 8) = buf.size();
}
bool load_svg(uint8_t* img, size_t s, uint32_t* w, uint32_t* h)
{
uint16_t magic[] = { 0x33, 0x34, 0x35, 0x5c, 0x37, 0x33, 0x30, 0x5c, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x5c, 0x70, 0x61, 0x6e, 0x6f, 0x72, 0x61, 0x6d, 0x61, 0x00 };
return reinterpret_cast<bool(__fastcall*)(void*, uint32_t, void*, uint32_t*, uint32_t*, float, void*)>(g_dumper->dump()[FNV32("load_svg")].as<void*>())(img, s, pad, w, h, 1.f, magic);
}
char pad[0x3C];
};
И получается dx11 текстуру
C++:
auto svg_parser::create_svg_texture(ID3D11Device* device, uint8_t* rgba, size_t sz, uint32_t width, uint32_t height, texture_data& ret_data) -> bool
{
std::vector<uint8_t> texture( 0xFFFFFF );
svg::image_data_t img( texture );
if ( img.load_svg(rgba, sz, &width, &height) && width && height )
{
D3D11_TEXTURE2D_DESC desc = {};
desc.Width = width;
desc.Height = height;
desc.MipLevels = 1;
desc.ArraySize = 1;
desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
desc.SampleDesc.Count = 1;
desc.Usage = D3D11_USAGE_DYNAMIC;
desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
D3D11_SUBRESOURCE_DATA initData = {};
initData.pSysMem = texture.data();
initData.SysMemPitch = width * 4;
initData.SysMemSlicePitch = 0;
D3D11_SHADER_RESOURCE_VIEW_DESC r{};
r.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
r.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
r.Texture2D.MipLevels = 1;
r.Texture2D.MostDetailedMip = 0;
ID3D11Texture2D* svg_texture_ret{ };
ID3D11ShaderResourceView* svg_shader_resource_ret{ };
HRESULT hr = device->CreateTexture2D(&desc, &initData, &svg_texture_ret);
if ( FAILED( hr ) )
{
ret_data = { };
return false;
}
hr = device->CreateShaderResourceView(svg_texture_ret, &r, &svg_shader_resource_ret);
if ( FAILED( hr ) )
{
ret_data = { };
return false;
}
ret_data = { svg_texture_ret, svg_shader_resource_ret, (int)width, (int)height };
}
return true;
}
И также рендерите:
C++:
m_base.m_background_draw_list->AddImage(result.m_shader, vec3_t { pos.x - (result.width / 2.f), pos.y }.im(), vec3_t{pos.x + (result.width / 2.f), pos.y + result.height}.im(), color.im());
P.S
C++:
m_dump.emplace(FNV32("load_svg"), address_t{ creep_("panorama.dll"), creep_("48 8B C4 48 89 58 08 48 89 70 10 48 89 78 18 55 41 54 41 55 41 56 41 57 48 8D 6C 24 90") });
m_dump.emplace(FNV32("utl_buffer_init"), address_t{ shadow::dll_export("??0CUtlBuffer@@QEAA@HHH@Z", "tier0.dll").address().as<uintptr_t>() });
Важно отметить, что этап с ручным парсингом vpk файла и конвертацией валвовской vsvg_c в svg, можно заменить вызовом внутриигровой функцией из client.dll, однако найти ее у меня не получилось.
Спасибо за внимание!