Гайд Продолжение. панорама - меню и кд абилок врагов

Участник
Статус
Оффлайн
Регистрация
23 Май 2019
Сообщения
781
Реакции[?]
331
Поинты[?]
63K
данный гайд основан на dylib'е и реверсе от джаваскриптовых хрефов(
Пожалуйста, авторизуйтесь для просмотра ссылки.
).
как реверсить джаваскриптовый хреф? берем с ссылки название нужной функции без класса(Game.GetGameTime превращается в GetGameTime), ищем хреф, должно найти несколько(на примере GetGameTime это GetScriptDesc<C_DOTAGameRules>::GetGameTime() или как то так называется, потом GetScriptDesc<ScriptBinding_PR>::GetGameTime() и GetScriptDesc<ScriptBinding_SF>::GetGameTime()). вам нужен GetScriptDesc<ScriptBinding_PR>::GetGameTime(). ну кароче на всякий случай тыкайте на каждый хреф и смотрите. ищите инструкцию типа lea r8, sub_blablabla. чекайте код этой sub_blablabla и если там есть какието очевидные зацепки(ну например если вы в дебагере сидите, чекайте всякие mov/lea адреса и если допустим вы там увидите mov rcx, [xxx] и потом этот xxx откроете в реклассе и увидите что судя по RTTI это C_DOTAGameRules/любой другой связаный по смыслу с функцией класс, то это то что вам надо), хрефы там или что-то еще. также можете брейкпоинт поставить на эти sub_blablabla и повызывать эти функции из джаваскрипта в демке(как это делать описано тута https://yougame.biz/threads/133895/ , но это долго и нудно на самом деле все ставить), что хорошенько ускорит ваш прогресс.
в общем нужные нам джаваскриптовые хрефы сегодня это CreatePanel , GetContextPanel , DeleteAsync и BLoadLayoutFromString.(особо не буду рассказывать как реверсить каждый из них, попробуйте сами, не сможете напишете помогу)
хреф "CreatePanel"(их две штуки) в panorama.dll:
1603642667744.png
нужная нам инструкция это lea r8, [xxx], переходим по этому xxx
1603642747571.png
видим тут еще один хреф(выделен синеньким), который убеждает нас что мы перешли туда куда надо

листаем ниже и видим mov qword:[xxx], чекаем что это за xxx такой(пкм->follow in dump->value), потом вводим этот же адрес(ctrl+g->ctrl+c) в реклассе и видим что это ничто иное как
panorama::CUIEngineSource2 : panorama::CUIEngine : panorama::IUIEngine : IToolsResourceListener : IRenderDeviceEventListener
дальше видим очевидный вызов виртуальной функции(выделен синеньким), после него test al,al, то есть эта функция возвращает булеану. что же за булеана? чекаем ниже и видим хреф "CreatePanelWithProperties second param (parent) must be a panel object". тут все понятно, функция чекает валидность второго параметра, она нам не нужна идем дальше видим похожие хрефы которые точно так же проверяют валидности параметров, идем дальше и находим хреф CreatePanelWithProperties - failed to create blablabla. такие хрефы(failed/error и т.д.) обычно идут уже после создания. то есть сначала попытка создать что-либо а потом если не получилось выдаем ошибку. так что смотрим что идет над этим хрефом

а идет там вызов panorama::CUIEngineSource2->функция на 0xf0. то есть вот сама наша функция которую мы так долго ждали. переходим на нее(берем из mov адрес panoram::CUIEngineSource2, берем вмт и прибавляем 0xf0 и переходим), ставим бп чекаем параметры с которыми она вызывается
параметры там такие(чтобы понять что за параметры вспоминаем первый хреф про type, parent, id, properties и все сразу становится на свои места)
rcx - this
rdx - указатель на type
r8 - айди панели(id)
r9 - указатель на panorama::CUIPanel, родитель создаваемой панели.(parent)
все понятно кроме указателя на type, смотрим на скрин видим там lea rdx,[rbp+590] и до этого mov word [rbp+590], ax и еще выше movzx edx, word:[rax] и еще выше вызов функции. тут тоже все понятно, вызывается функция и то что она возвращает это и есть type. засовываем этот type в локальную переменную(в стек) и передаем указатель на эту переменную в функцию создания панели. теперь посмотрим какие параметры у той функции которая возвращает тип. точно так же переходим ставим бп и видим что они такие:
rcx - указатель на word куда будет записан результат
rdx - строка которая из джаваскрипта с вызовом передается, судя по вызову $.CreatePanel( 'Panel', Game.GetMainHUD(), 'MyPanel' ), то нам нужна строка "Panel"

вот код этой функции. как видите(выделено синеньким) результат записывается в word указатель на который передан в rcx.
ну и вот кароче имеем в итоге
WORD PanelType;
GetPanelTypeF(&PanelType, "Panel");//можно инициализировать 1 раз за запуск чита, тип панели всегда одинаковый, вызывать каждый раз не надо
Pointer<CPanel2D> Panel2D = Panorama2->VirtualCall<30>(&PanelType, id, DOTAHud);//та самая +0xf0 .чекаем RTTI и видим что она возвращает panorama::CPanel2D. тыкаем туда сюда в реклассе/дебагере и описываем классы CUIPanel и CPanel2D:
C++:
    class CUIPanel{
    public:
u64 vmt;
        CPanel2D* _CPanel2D;
        const char* id;
        CUIPanel* parent;
        u64 TopLevelWindow;
        int numofchildren;
        int junk;
        CUIPanel** Children;
        CUIPanel* GetChild(int child) {
            if (child == -1) return this;
            if (child >= numofchildren) return 0;
            return Children[child];
        }
        template<typename... args> CUIPanel* GetChild(int child, args... pack) {
            if (child == -1) return this;
            if (child >= numofchildren) return 0;
            return Children[child]->GetChild(pack...);
        }
    };
    class CPanel2D{
    public:
u64 vmt;
        CUIPanel* CUIPanel;
точно так же реверсим остальные функции с хрефов.
GetMainHUD(то есть получение панельки куда мы будем пиндюрить свои панельки) сводится к FindPanelById("Hud"). код этой функции(который получен не помню как ):
C++:
    CUIPanel* FindPanelById(cc id) {
    //    struct PanelListItem {
    //    u64 junk[2];
    //    CUIPanel* panel1;
    //    u64 Junk[3];
    //    CUIPanel* panel2;
    //};
    //Pointer<PanelListItem> PanelList() {
    //        return Member(0x140);
    //    }
    //stringsmatch(x,y) это !strcmp(x,y)
        for (auto Item = Panorama2->PanelList();; Item += 64) {
            if (StringsMatch(Item->panel1->id, id)) {
                return Item->panel1;
            }
            if (StringsMatch(Item->panel2->id, id)) {
                return Item->panel2;
            }
        }
    }
DeleteAsync это
CPanel2D::DeleteAsyncPanel = GetAbsoluteAddress(Panel2D->VirtualMethod(42) + 0x735, 3, 7);

BLoadLayoutFromString это
void CUIPanel::LoadLayoutFromString(cc layout) {
VirtualCall<14>(layout, 0, 0);
}
стиль панельки реверсим от хрефа "CUIPanel::BSetProperty". он приведет вас к одноименной функции где вы ищете хреф "style" там чуть ниже будет mov word:[xxx],dx(оффсет 0x3f7) и этот xxx содержит индекс стиля. потом просто вызываете эту же самую функцию где передаете в rdx указатель на этот индекс стиля. то есть xxx можете просто передавать. эта функция имеет 307 индекс в вмт CUIPanel, что можно узнать поставив бп в самое начало данной функции и посмотреть стек перейти туда и посмотреть инструкцию выше(это будет call qword:[register+998]). применять это можно к любой функции которая вызвана через call(еще функции могут вызываться через jmp тогда в стеке не будет адреса который выполнил этот jmp), потому что инструкция call раскрывается в две инструкции: push RIP+sizeof(call); jmp FUNCTION; то есть перед вызовом в стек засовывается адрес возврата, то есть текущий адрес(RIP) + размер call, то есть следующая по счету инструкция. и когда вы ставите бп в начало функции вы можете просто заглянуть в стек и увидите кто вызвал эту функцию(исключение составляет вызов функции не через call а через jmp)

то есть,
void SetStyle(cc value) {
VirtualCall<307>(&StyleOffset, value, Panorama2->VirtualMethodsTable);//r9 не обязателен я туда просто так вмтху передаю, можете этого не делать
}
(это то что вы должны были получить в результате реверса, если у вас не получилось и вы не понимаете как это достать то пишите я разъясню)
все остальные функции берутся из dylib'ов - находите функцию в dylibе, вычисляете ее индекс, потом заходите в дебагер, находите этот же класс ставите брейкпоинты на функции рядом с этим индексом(ну допустим в dylibе индекс x, а я ставлю бпшки на x-2 -> x+2, потом расширяю радиус если надо), выполняете действие(ну допустим если я хукаю OnLeftMouseDown то я нажимаю левую кнопку мыши), где сработало та функция и есть искомая. берете ее индекс и воаля.

итак, алгоритм отрисовки на панораме у нас такой:
1. создаем панель с айди xxx(если что, может быть много панелек с одним и тем же айди, это разрешено)
2. загружаем в панель наш xml из строки
3. обрабатываем взаимодействие с панелькой(клики шлики наведение и т.д.)
4. по надобности(после клика например) изменяем свойства наших уже созданных элементов(изменяем текст надписи и т.д.)

сори не стал уже глубоко объяснять лень все подряд скринить и описывать. а так что-то конкретное могу объяснить пишите, будем вместе дополнять гайд по мере надобности.
код:(выпилил два неймспейса меню и абилитиесп, доделывайте сами)
Пожалуйста, авторизуйтесь для просмотра ссылки.
вбе тут нет сами добавите из прошлой части.
как выглядит:

по поводу влияния данной фигни на фпс много сказать не могу. играя против ботов в люксембурге(ну то есть как паб кароче) поставив игру на паузу когда все 5 врагов были засвечены замерив фпс за 2 минуты с читом а потом без через cl_showfps1, cl_resetfps, cl_printfps у меня было -2фпс с читом. но это на паузе. а по факту должно быть больше -фпсов. не замерял потому что смысла нет так как в доте фпс нестабильный
если нашли баги пишите блаблабла.
кстати забыл сказать, в панораме есть такое понятие как масштаб, формула масштаба это round(высота_экрана/1080). то есть размеры и координаты из вашего xml умножаются на этот масштаб и отрисовываются уже в полученных размерах. то есть если у меня ширина панельки 50 пикселей в xml, то на моем 1366x768 она становится 50*768/1080, а это 36 пикселей экрана
 
Последнее редактирование:
Участник
Статус
Оффлайн
Регистрация
23 Май 2019
Сообщения
781
Реакции[?]
331
Поинты[?]
63K
если что, xml в панораме имеет такой вид:
<root>
<Panel>//та самая CPanel2D которую возвращает CreatePanel
...
</Panel>
</root>
также почитайте
Пожалуйста, авторизуйтесь для просмотра ссылки.
,
Пожалуйста, авторизуйтесь для просмотра ссылки.
,
Пожалуйста, авторизуйтесь для просмотра ссылки.
,
Пожалуйста, авторизуйтесь для просмотра ссылки.

так как тутор получился немного говно, то буду дописывать.
начать вам стоит просто с создания панельки:
(кстати разница CUIPanel и CPanel2D в том, что CUIPanel это абстрактный элемент который мы просто называем "любая панелька", а CPanel2D это конкретный - кнопка, картинка, надпись и т.д.)
C++:
DOTAHud = FindPanelById("Hud");//при входе в матч

//после входа в матч когда уже игра более менее загрузилась(GameState > DOTA_GAMERULES_STATE_INIT)
GetPanelTypeF(&PanelType, "Panel");
Pointer<CPanel2D> Panel2D = Panorama2->VirtualCall<30>(&PanelType, "MyPanel", DOTAHud);
CUIPanel* PanelUI = Panel2D->CUIPanel;
PanelUI->LoadLayoutFromString("<root><Panel style=\"background-color:black;width:400px;height:400px;\"></Panel></root>");
и если все получилось то вам должно нарисовать черный квадрат малевича на экране.
дальше попробуйте изменить стиль уже созданной панельки:
C++:
WORD StyleOffset = 0;
StyleOffset = *(WORD*)(GetAbsoluteAddress(PanelUI->VirtualMethod(307) + 0x3f7, 3, 7));
//для StyleOffset PanelUI это любая панель, тот же DOTAHud, или же ваша только что созданная панель. в данном случае нам нужна лишь вмтха а на саму панельку пофигу.
//а в случае с изменением стиля нам естественно нужна сама изменяемая панель в rcx(this). она у меня передается под колпаком в VirtualCall
PanelUI->VirtualCall<307>(&StyleOffset, "background-color:white;");//квадрат стал белым. остальные параметры стиля(width,height) не изменились так как мы их не указали в строке, что очень удобно потому что мы можем изменить один параметр не парясь о других
дальше заходите в libclient.dylib и ищете OnMouseButtonDown/Up(это CPanel2D)

12 индекс в dylibе, значит ставим брейкпоинты в этом радиусе(10-14 для начала потом увеличиваем если не нашли) в функциях вмтхи нашей недавно созданной панельки, кликаем и смотрим кто из бпшек сработает. но тут одна проблемка, клики по панелькам(<Panel>) так почему-то не обрабатываются, поэтому я в своем xml везде впиндюрил Button style="width:100%;height:100%;". сделайте также, потом найдите вашу CButton(дочерний класс от CPanel2D) через GetChild(индексы для GetChild работают так:
<Panel>//вот панелька у которой хотим получить детей, Обозначим ее our_panel
<Button/>//our_panel->GetChild(0)
<Label/>//our_panel->GetChild(1)
<Image/>//our_panel->GetChild(2)
<Panel>//our_panel->GetChild(3)
<Label/>//our_panel->GetChild(3)->GetChild(0)//то есть нулевой ребенок третьего ребенка
</Panel>
</Panel>
)
и у этой CButton уже ставьте брейкпоинты и найдете что индексы такие:
CButtonVMT->Hook(14, CButton_MDOWN);
CButtonVMT->Hook(15, CButton_MUP);
 
Последнее редактирование:
Начинающий
Статус
Оффлайн
Регистрация
15 Дек 2018
Сообщения
146
Реакции[?]
9
Поинты[?]
0
Поставь в код защиту от пастеров какую ни будь, а то будут супер хаки дота2 как в кс го
Кста, пасхалка прикольная
 
Участник
Статус
Оффлайн
Регистрация
23 Май 2019
Сообщения
781
Реакции[?]
331
Поинты[?]
63K
Поставь в код защиту от пастеров какую ни будь, а то будут супер хаки дота2 как в кс го
Кста, пасхалка прикольная
выпилил меню и неймспейс AbilityESP, кому надо тот разберется, думаю достаточно защиты))
 
Начинающий
Статус
Оффлайн
Регистрация
15 Дек 2018
Сообщения
146
Реакции[?]
9
Поинты[?]
0
Участник
Статус
Оффлайн
Регистрация
23 Май 2019
Сообщения
781
Реакции[?]
331
Поинты[?]
63K
А вот смотри, как можно нарисовать манабары и что такое пэинтраверс?
пеинттраверс это как RunFrame только на другом движке отрисовки, vgui. это старый движок отрисовки интерфейса на сурс1 щас все на панораме в доте. пробовал на нем чето сделать и пососал когда иконки пытался нарисовать, потом не парился и перешел на панораму. манабары сделать можно точно так же. создаешь панель у которой в лейауте две панели одна черная шириной 100% другая синяя шириной (mana/MaxMana)% и воаля
 
Начинающий
Статус
Оффлайн
Регистрация
12 Сен 2020
Сообщения
42
Реакции[?]
3
Поинты[?]
0
а можешь поподробнее про вызовы функций на джаваскрипте для тех, кто не в теме. для локалхоста нужен денвер? и как этот скрипт работает в теории: возвращает в текстовом формате какую-то инфу на локалхост при исполнении бп, просматривая реплей (демка?)?), или я ваще не в ту степь ушел?
 
Последнее редактирование:
Участник
Статус
Оффлайн
Регистрация
23 Май 2019
Сообщения
781
Реакции[?]
331
Поинты[?]
63K
а можешь поподробнее про вызовы функций на джаваскрипте для тех, кто не в теме. для локалхоста нужен денвер? и как этот скрипт работает в теории: возвращает в текстовом формате какую-то инфу на локалхост при исполнении бп, просматривая реплей (демка?)?), или я ваще не в ту степь ушел?
xampp какой-нибудь подними для локалхоста.
при загрузке в матч(или когда нибудь еще, все зависит от того какой файл модифицируешь,в вышеупомянутом треде там через hud_reborn.xml) делается запрос на твой сервак.
ты с сервака возвращаешь плеинтекст джаваскриптовый а твой rd2js.xml вызывает на него eval и в итоге твой джаваскриптовый текст выполняется.
можно без сервака но это не эффективно. с серваком удобнее менять и перезагружать скрипты. а без сервака тебе рекомпилить впк файл надо будет...
половина функций не работает в пабе, вальвы пофиксили, а вот в демке все работает можешь там реверсить
Пожалуйста, авторизуйтесь для просмотра ссылки.
находишь функцию с хрефа в client.dll ставишь там бп смотришь че как
 
Начинающий
Статус
Оффлайн
Регистрация
12 Сен 2020
Сообщения
42
Реакции[?]
3
Поинты[?]
0
да я читал, но все равно не могу понять какой я хочу результат получить, или в чем заключается упрощение поиска нужного поинтера в функции через данный метод, какая взаимосвязь бп и выполняющегося скрипта js) 1) что такое демка (разные значения видел)? 2) сейчас все xml в dota_addons, можно оттуда любой файл использовать?
 
Последнее редактирование:
Участник
Статус
Оффлайн
Регистрация
23 Май 2019
Сообщения
781
Реакции[?]
331
Поинты[?]
63K
да я читал, но все равно не могу понять какой я хочу результат получить, или в чем заключается упрощение поиска нужного поинтера в функции через данный метод, какая взаимосвязь бп и выполняющегося скрипта js) 1) что такое демка (разные значения видел)? 2) сейчас все xml в dota_addons, можно оттуда любой файл использовать?
1) демка - DEMO HERO. опробовать героя или как то так хз как в русской версии. ну кароче из главного меню в таблице героев кнопка там есть
2) джаваскриптовые функции из API представляют собой c++шные функции завернутые в джаваскриптовый движок. ставишь бп на эту с++шную функцию и когда ты вызываешь эту функцию из джаваскрипта то твой бп срабатывает и ты смотришь где там че какие значения(динамический анализ)
3) не парься и юзай hud_reborn как в том треде
ты ищешь хреф в client.dll, получаешь 2-3 хрефа, чекаешь каждый на смысловое соответствие(если я хочу GetGameTime то у меня должно быть что-то связанное с временем, например какие-то float значения и т.д., должен быть указатель на класс+оффсет), находишь нужный, ставишь там бп вызываешь функцию из джаваскрипта реверсишь(ну либо без бп тоже можно но сложно, статический анализ)
 
Последнее редактирование:
Начинающий
Статус
Оффлайн
Регистрация
12 Сен 2020
Сообщения
42
Реакции[?]
3
Поинты[?]
0
как правильно интерпретировать значения в джс функции (
Пожалуйста, авторизуйтесь для просмотра ссылки.
), это интуитивно-практический момент, или догадка при декомпайле функции из иды или еще есть какие-то особенности?
 
Последнее редактирование:
Участник
Статус
Оффлайн
Регистрация
23 Май 2019
Сообщения
781
Реакции[?]
331
Поинты[?]
63K
как правильно интерпретировать значения в джс функции (
Пожалуйста, авторизуйтесь для просмотра ссылки.
), это интуитивно-практический момент, или догадка при декомпайле функции из иды или еще есть какие-то особенности?
документация вальвов кусок говна. если знаешь джаваскрипт еще можно догадаться более менее,а так земля пухом. я тоже на джаваскрипте гонял через эту апи со скриптами, поэтому это практический момент. + еще D2JS с гитхаба в качестве примера есть.

Particles.SetParticleControl( integer iIndex, integer iPoint, js_value vPosVal )
Particles это Object
SetParticleControl это function которая принадлежит объекту Particles
iIndex это индекс партикли(ну логично что это результат вызова CreateParticle, то есть
var particle = Particles.CreateParticle(...)
Particles.SetParticleControl(particle,...)
)
iPoint это индекс поинта который ты выставляешь(поинт это этакий параметр который вроде для каждой партикли разный. например 2 это цвет 3 это кординаты 4 это размер 5 это еще что-то и т.д.)
vPosVal это массив из 3 floatов в который ты будешь выставлять данный поинт.(то есть xyz коорды, rgb цвет и т.д.)
 
Начинающий
Статус
Оффлайн
Регистрация
12 Сен 2020
Сообщения
42
Реакции[?]
3
Поинты[?]
0
можешь расшифровать на бытовом уровне структуру шаблона, а то отсылки в инете к sfinae еще больше запутывают)
template<typename T, typename std::enable_if<!std::is_same<T, AssignResolution>::value>::type* = nullptr> Pointer(T t)
 
Участник
Статус
Оффлайн
Регистрация
23 Май 2019
Сообщения
781
Реакции[?]
331
Поинты[?]
63K
можешь расшифровать на бытовом уровне структуру шаблона, а то отсылки в инете к sfinae еще больше запутывают)
template<typename T, typename std::enable_if<!std::is_same<T, AssignResolution>::value>::type* = nullptr> Pointer(T t)
AnyPointer это просто любой указатель который можно кастовать автоматом без ругани компилятора и без скобочек

AssignResolution это указатель с которого производится считывание нужного типа, например
float x = Member(123);//Member возвращает AssignResolution, а потом у AssignResolution вызывается operator float(), который считывает *(float*)(AssignResolution::pointer)
то есть это замена такого выражения:
float x = Member<float>(123);//конкретно говорю что нужно считать float. а с AssignResolution он сам понимает что нужно считать float так как я присваиваю переменной float x
AssignResolution основан на той особенности, что когда есть функция которая возвращает какой-то тип(int a(){}) или есть присвоение(a = b), то возврат функции/правая сторона присвоения пытаются кастануться в тип возврата/тип левой стороны присвоения. если каст невозможен то ошибка, если возможен то все кастуется.
то есть условно:
char x(){
u64 a = 123456;// это 0x1e240
return a;//u64::operator char(a) вызывается и получаем первый в памяти(последний от самого числа из-за литл ендиана) байтик а это 0x40, то есть число 64
}
или:
int a =12345;
float x = a;//int::operator float(a) вызывается и получаем на выходе 12345.0f

AnyPointer X(){//смысл функции в том чтобы с указателя This+123 считать еще один указатель(ну то есть 8 байт)
return Member(123);//функция Member возвращает класс AssignResolution
}
здесь так называемый implicit conversion, то есть автоматический каст.
я говорю компилятору что нужно сделать из AssignResolution AnyPointer. а он видит два способа: либо Pointer<u64>::Pointer(AssignResolution t) либо AssignResolution::operator Pointer<u64>(), поэтому он выдает ошибку о неоднозначности каста.
решением проблемы будет либо explicit каст, то есть
AnyPointer X(){
return (AnyPointer)Member(123);//AssignResolution::operator Pointer<u64>
//либо return AnyPointer{Member(123);};//Pointer<u64>::Pointer(AssignResolution t)
}
либо ликвидация неоднозначности каста.
у Pointer конструктор
template<typename T>
Pointer(T t)
то есть его можно из любого говна построить.
а у AssignResolution оверлоад тайпкаста
template<typename T>
operator T()
то есть его тоже можно кастануть в любое говно.
я либо исключаю из множества T в конструкторе Pointer::Pointer класс AssignResolution, либо в operator T исключаю из T класс AnyPointer.
но между конструктором Pointer и оператором T есть качественная разница(не в общем смысле,а конкретно в данном примере, потому что класс AssignResolution это просто указатель с которого надо потом считать нужный тип) - конструктор просто присваивает, а оператор считывает с ячейки. поэтому в функции AnyPointer X() мне нужно именно считать а не просто присвоить. поэтому я исключаю в Pointer::Pointer из множества T класс AssignResolution
template<typename T, typename std::enable_if<!std::is_same<T, AssignResolution>::value>::type* = nullptr> Pointer(T t)
typename T - любой тип данных
std::enable_if - булеана которая отключает функцию(не на совсем а просто для данного случая) если условие не выполнено
std::is_same - проверяю является ли T классом AssignResolution
в итоге получается, что Pointer::Pointer(T t) работает на любой T кроме AssignResolution. в этом случае функция просто не работает, и тем самым остается только AssignResolution::operator Pointer(), тем самым я ликвидирую неоднозначность каста и компилятор не ругается.
 
Последнее редактирование:
Начинающий
Статус
Оффлайн
Регистрация
30 Мар 2020
Сообщения
326
Реакции[?]
24
Поинты[?]
12K
У кого-то получалось находить BLoadLayOutFromString? Или знает ли ктото альтернативы?
 
Сверху Снизу