Дампим память и пишем maphack

в 10:34, , рубрики: dayz, forensics, maphack, qt, реверс-инжиниринг

image

В один из вечеров школьного лета, у меня появилась потребность в мапхаке для DayZ Mod (Arma 2 OA). Поискав информацию по теме, я понял, что связываться с античитом Battleye не стоит, ибо нет ни знаний, ни опыта для обхода защитного драйвера ядра, который вежливо расставил кучу хуков на доступ к процессу игры.

DayZ одна из немногих игр, где расположение важных для игрового процесса объектов, меняется не часто и сохраняется после перезахода(базы не двигаются, большинство техники тоже много времени стоит на месте). Этот факт открывает возможность атаки через дамп оперативной памяти.

Все ссылки в конце.

Глава 1. Дампим

Получить снимок памяти в рантайме без обхода античита, врядли получится. Поэтому ищем другие тактики. Первое, что находим это хабрастатья, из которой становится понятно куда копать.

Попытка 1

Получить образ памяти через описанный в статье хотресет, загрузку с Ubuntu CyberPack (IRF) и получение образа через fmem, мне не удалось, по невыясненным причинам fmem зависал.
Немного погуглив находим альтернативную тулзу, с таким же функционалом LiME ~ Linux Memory Extractor. Теперь её нужно было собрать и запустить на livecd.
Выбор дистрибутива пал на TinyCore(TinyCorePure64, если сдампить нужно более 3 ГБ). Загрузившись с него качаем и устанавливаем пакеты.

tce-load -iw linux-kernel-sources-env.tcz
cliorx linux-kernel-sources-env.sh

Далее монтируем флешку с сорцами, куда также будем скидывать дамп, собираем через make и получаем образ

insmod ./lime.ko "path=/path/mem-image.lime format=lime"

Теперь этот файл нужно кому-то скормить, чтобы на выходе получить память нужного нам процесса. Для этого нам должен был подойти Volatility Framework с плагином memdump.

vol.py -f F:mem-image.lime format=lime pslist
vol.py -f F:mem-image.lime format=lime memdump –dump-dir ./output –p 868

Или Rekall Framework, который является его форком и активно развивается, в отличии от самого volatility

rekal -f F:mem-image.lime pslist
rekal -f F:mem-image.lime memdump dump_dir="./output", pids=868

Однако что я бы не делал, заводиться он не захотел и я продолжил копать.

При работе с Rekall на windows 10, при первом поиске чего-либо по дампу может появиться сообщение вида:

WARNING:rekall.1:Profile nt/GUID/F6F4895554894B24B4DF942361F0730D1 fetched and built. Please consider reporting this profile to the Rekall team so we may add it to the public profile repository.

А в следующий раз, он может упасть с такой ошибкой:

CRITICAL:rekall.1:A DTB value was found but failed to verify. See logging messages for more information.

Если это произошло, при запуске вам нужно указать параметр --profile со значением профиля, который вам вывело в первый раз.

Попытка 2

Для получения наиболее полного снимка памяти можно воспользоваться файлом hiberfil.sys, в который сохраняется вся память при переходе винды в гибернацию.

Здесь всё ещё проще, переходим в режим гибернации, загружаемся с любого livecd (в моём случае, всё тот-же TinyCore), монтируем системный диск(Read-Only) и флешку, копируем нужный файл.

Для TinyCore не забываем установить пакет для поддержки ntfs.

tce-load -iw ntfs-3g

Через fdisk -l, находим нужные нам логические разделы и монтируем их

sudo ntfs-3g -o ro /dev/sda2 /tmp/a1  //Системный диск с Read-Only
sudo ntfs-3g /dev/sdc1 /tmp/a2

Копируем

cp /tmp/a1/hiberfil.sys /tmp/a2

Дальше этот файл можно было скормить volatility (поддерживает файл гибернации с win7 или ранее).

vol.py imagecopy -f hiberfil.sys -O win7.img

Поскольку у меня win10, мне этот вариант не подошёл.
Я попробовал отдать файл программе Hibr2bin, которая раньше была тем самым Sandman Framework.

HIBR2BIN /PLATFORM X64 /MAJOR 10 /MINOR 0 /INPUT hiberfil.sys /OUTPUT uncompressed.bin

Но та выдала непонятный output, с которым отказались работать фреймворки для анализа.
На помощь пришёл Hibernation Recon с Free версией, который без проблем дал читаемый для фреймвоков выхлоп.
На выходе с memdump мы получаем файл с самой памятью процесса и файл с соотношением виртуальных адресов к адресам в файле.

File Address      Length      Virtual Addr
-------------- -------------- --------------
0x000000000000 0x000000001000 0x000000010000
0x000000001000 0x000000001000 0x000000020000
0x000000002000 0x000000001000 0x000000021000
0x000000003000 0x000000001000 0x00000002f000
0x000000004000 0x000000001000 0x000000040000
0x000000005000 0x000000001000 0x000000050000
0x000000006000 0x000000001000 0x000000051000

Глава 2. Пишем мапхак.

Для GUI я выбрал Qt.

Для начала пишем удобную обёртку для обращения к виртуальной памяти в файле через таблицу.

class MemoryAPI
{
public:
    MemoryAPI(){}
    MemoryAPI(QString pathDump, QString pathIDX);
    //Функции чтения данных по виртуальным адресам
    quint32 readPtr      (const quint32 offset);
    qint32  readInt      (const quint32 offset);
    float   readFloat    (const quint32 offset);
    QString readStringAscii(const quint32 offset, const quint32 size);
    QString readArmaString(quint32 offset);
    //Инициализая обёртки
    void loadIDX        (QString path);
    void loadDump       (QString path);

private:
    //Массив с соотношениями виртуальных и физических адресов
    QVector <MemoryRange> memoryRelations;
    quint32 convertVirtToPhys(const quint32 virt) const;
    QByteArray readVirtMem(const quint32 baseAddr, const quint32 size);
    QFile dumpFile;
};

Каждую строчку idx-файла мы представляем в виде простой структуры.

class MemoryRange
{
private:
    quint32 baseVirtualAddress;
    quint32 basePhysicalAddress;
    quint32 size;
};

Все функции чтения данных по виртуальным адресам сводятся к вызову этой функции с нужными параметрами.

QByteArray MemoryAPI::readVirtMem(const quint32 baseAddr, const quint32 size)
{
    QByteArray result;
    //Конвертируем адрес
    quint32 addr = convertVirtToPhys(baseAddr);
    dumpFile.seek(addr);
    result = dumpFile.read(size);

    return result;
}

Конвертация адреса проходит простым поиском нужного смещения в массиве (можно было бы применить бинарный поиск, но нет).

quint32 MemoryAPI::convertVirtToPhys(const quint32 virt) const
{
    for(auto it = memoryRelations.begin(); it != memoryRelations.end(); ++it)
    {
        if((*it).inRange(virt))
        {
            const quint32& phBase = (*it).getPhysicalAddress(), vrBase = (*it).getVirtualAddress();
            //Защита от переполнения
            if(phBase>vrBase)
                return virt + (phBase - vrBase);
            else
                return virt - (vrBase - phBase);
        }
    }
    //Если не находим нужного адреса кидаем исключение
    throw 1;
}

Теперь сделаем структуру, в которой будем хранить данные каждого объекта в игре.

class EntityData
{
public:
    friend class WorldState;
    //Перечисление всех нужных нам типов объектов
    enum class type {airplane, car, motorcycle, ship, helicopter, parachute, tank,
                     tent, stash, fence, ammoBox, campFire, crashSite, animals,
                     players, zombies, stuff, hedgehog, invalid};

    type entityType;

    EntityData();
    EntityData(QString n, QPointF c, type t = type::stuff);

    QString shortDescription()const;
    QString fullDescription()const;
    QPointF getCoords() const {return coords;}
private:
    //Название объекта
    QString name;
    //Координаты объекта
    QPointF coords;
    //Дополнительная информация об объекте (для расширяемости)
    QMap<QString, QString> additionalFields;
};

Далее пишем класс, в котором будем хранить состояние мира (все объекты).

class WorldState
{
public:
    //Можно загрузиться из непосредственно дампа, и файла с адресами
    WorldState(const QString& dumpFile, const QString& idxFile);
    //или из xml-файла с состоянием мира
    WorldState(const QString& stateFile);
    //Этот xml-файл, можно сохранить и передать друзьям
    void saveState(const QString& stateFile);
    //Ассоциативный массив, в котором будем хранить итераторы на объекты каждого типа (полезная оптимизация)
    QMap <EntityData::type, EntityRange> entityRanges;
    QString worldName;
private:
    //Массив со всеми объектами
    QVector <EntityData> entityArray;
        //Смещения для получения нужных данных
    QVector<quint32> masterOffsets;
    QVector<quint32> tableOffsets;
    quint32          objTableAddress;
    void handleEntity   (quint32 entityAddress, MemoryAPI& mem);
        //Инициализации
    void initRanges();
    void initOffsets();

    QDomElement makeElement(QDomDocument& domDoc, const QString& name, const QString& strData = QString());
};

Здесь происходит вся работа с дампом памяти и загрузка информации о всех объектах.

WorldState::WorldState(const QString& dumpFile, const QString& idxFile)
{
    //Инициализируем смещения
    initOffsets();

    //Создаём простое диалоговое модальное окно прогресса
    QProgressDialog progress;
    progress.setCancelButton(nullptr);
    progress.setLabelText("Loading dump...");
    progress.setModal(true);
    progress.setMinimum(0);

    progress.setMaximum(masterOffsets.length()+2);
    progress.show();

    MemoryAPI mem(dumpFile,idxFile);
    progress.setValue(1);

    for(auto mO = masterOffsets.begin(); mO != masterOffsets.end(); ++mO)
    {
        quint32 entityTableBasePtr = mem.readPtr(objTableAddress) + (*mO);
        for(auto tO = tableOffsets.begin(); tO != tableOffsets.end(); ++tO)
        {
            qint32 size = mem.readInt(entityTableBasePtr + 0x4 +(*tO));

            for(qint32 i = 0; i!=size; ++i)
            {
                quint32 fPtr = mem.readPtr(entityTableBasePtr + (*tO));
                quint32 entityAddress = mem.readPtr(fPtr + 4 * i);

                //Обрабатываем сущность
                handleEntity(entityAddress, mem);

                //Не забываем обрабатывать события, чтобы не было зависаний графического интерфейса
                QCoreApplication::processEvents();
            }
        }
        progress.setValue(progress.value()+1);
    }
    initRanges();
    worldName = "chernarus";
    progress.setValue(progress.value()+1);
}

Инициализируем смещения

void WorldState::initOffsets()
{
    masterOffsets.append(0x880);
    masterOffsets.append(0xb24);
    masterOffsets.append(0xdc8);

    tableOffsets.append(0x8);
    tableOffsets.append(0xb0);
    tableOffsets.append(0x158);
    tableOffsets.append(0x200);

    objTableAddress = 0xDAD8C0;
}

Здесь остановимся поподробнее. Вся информация о мире игры хранится в примерно такой структуре (Основано на дампе, найденном на форуме).

Структура мира

class World
{
public:
char _0x0000[8];
    InGameUI* inGameUI; //0x0008 
char _0x000C[1520];
    EntityTablePointer* entityTablePointer; //0x05FC 
    VariableTableInfo* variableTableInfo; //0x0600 
char _0x0604[428];
    __int32 gameMode; //0x07B0 
char _0x07B4[4];
    float speedMultiplier; //0x07B8 
char _0x07BC[196];
        EntitiesDistributed table1; //0x0880
char _0x0B00[36];
    EntitiesDistributed table2; //0x0B24
char _0x0DA4[36];
    EntitiesDistributed table3; //0x0DC8
char _0x1048[849];
    BYTE artilleryEnabled; //0x1399 
    BYTE enableItemsDropping; //0x139A 
char _0x139B[13];
    UnitInfo* cameraOn; //0x13A8 
char _0x13AC[4];
    UnitInfo* cplayerOn; //0x13B0 
    UnitInfo* realPlayer; //0x13B4 
char _0x13B8[48];
    float actualOvercast; //0x13E8 
    float wantedOvercast; //0x13EC 
    __int32 nextWeatherChange; //0x13F0 
    float currentFogLevel; //0x13F4 
    float fogTarget; //0x13F8 
char _0x13FC[32];
    __int32 weatherTime; //0x141C 
char _0x1420[8];
    BYTE playerManual; //0x1428 
    BYTE playerSuspended; //0x1429 
char _0x142A[30];
    __int32 N0D09AD19; //0x1448 
char _0x144C[92];
    ArmaString* currentCampaign; //0x14A8 
char _0x14AC[4];
    __int32 N0D09B79F; //0x14B0 
char _0x14B4[52];
    float viewDistanceHard; //0x14E8 
    float viewDistanceMin; //0x14EC 
    float grass; //0x14F0 
char _0x14F4[36];
    __int32 initTableCount; //0x1518 
    __int32 initTableMaxCount; //0x151C 
char _0x1520[4];

};//Size=0x1524

Получить доступ к этой структуре можно по указателю, который лежит по статическому для каждой версии игры смещению (смещение можно загуглить или самому найти через реверс, но это уже совсем другая история). Это смещение мы храним в переменной objTableAddress. В masterOffsets мы храним смещения на 3 таблицы, относительно этой структуры.

Таблица с таблицами

class EntitiesDistributed
{
public:
char _0x0000[8];
    Entity* table1; //0x0008
    __int32 table1Size; //0x000C 
char _0x0010[160];
    Entity* table2; //0x00B0
    __int32 table2Size; //0x00B4 
char _0x00B8[160];
    Entity* table3; //0x0158
    __int32 table3Size; //0x015C 
char _0x0160[160];
    Entity* table4; //0x0200
    __int32 table4Size; //0x0204 
char _0x0208[120];

};//Size=0x0280

В свою очередь каждая таблица хранит в себе ещё по 4 таблицы с длиной (смещения на эти таблицы мы храним в tableOffsets).
Теперь мы можем итерироваться по всем объектам в игре. Разберём функцию, которая обрабатывает каждую сущность.

void WorldState::handleEntity(quint32 entityAddress, MemoryAPI &mem)
{
    QString objType;
    QString objName;
    float coordX;
    float coordY;

    try{
        quint32 obj1 = entityAddress;
        quint32 pCfgVehicle = mem.readPtr(obj1 + 0x3C);
        quint32 obj3 = mem.readPtr(pCfgVehicle + 0x30);
        quint32 pObjType = mem.readPtr(pCfgVehicle + 0x6C);

        objType = mem.readArmaString(pObjType);
        objName = mem.readStringAscii(obj3 + 0x8, 25);

        quint32 pEntityVisualState = mem.readPtr(obj1 + 0x18);
        coordX = mem.readFloat(pEntityVisualState + 0x28);
        coordY = mem.readFloat(pEntityVisualState + 0x30);
    }catch(int a)
    {
        qDebug() << "Ошибка доступа к виртуальной памяти.";
        return;
    }

    //Создаём новую сущность
    EntityData ed(objName, QPointF(coordX, coordY));

    //Классифицируем сущность по категориям
    if(objType == "car")
        ed.entityType = EntityData::type::car;
    else if(objType == "motorcycle")
        ed.entityType = EntityData::type::motorcycle;
    else if(objType == "airplane")
        ed.entityType = EntityData::type::airplane;
    else if(objType == "helicopter")
        ed.entityType = EntityData::type::helicopter;
    else if(objType == "ship")
        ed.entityType = EntityData::type::ship;
    else if(objType == "tank")
        ed.entityType = EntityData::type::tank;
    else if(objType == "parachute")
        ed.entityType = EntityData::type::parachute;
    else if(objName.indexOf("TentStorage")!=-1)
        ed.entityType = EntityData::type::tent;
    else if(objName.indexOf("Stash")!=-1)
        ed.entityType = EntityData::type::stash;
    else if(objName.indexOf("WoodenGate")!=-1 || objName.indexOf("WoodenFence")!=-1)
        ed.entityType = EntityData::type::fence;
    else if(objName.indexOf("DZ_MedBox")!=-1 || objName.indexOf("DZ_AmmoBox")!=-1)
        ed.entityType = EntityData::type::ammoBox;
    else if(objName.indexOf("Hedgehog_DZ")!=-1)
        ed.entityType = EntityData::type::hedgehog;
    else if(objName.indexOf("Land_Camp_Fire_DZ")!= -1)
        ed.entityType = EntityData::type::campFire;
    else if(objName.indexOf("CrashSite")!= -1)
        ed.entityType = EntityData::type::crashSite;
    else if(objName.indexOf("WildBoar")== 0 || objName.indexOf("Rabbit")== 0 ||
            objName.indexOf("Cow")== 0 || objName.indexOf("Sheep")== 0 ||
            objName.indexOf("Goat")== 0 || objName.indexOf("Hen")== 0)
        ed.entityType = EntityData::type::animals;
    else if(objName.indexOf("Survivor2_DZ")!= -1 || objName.indexOf("Sniper1_DZ")!=-1 ||
            objName.indexOf("Camo1_DZ")!=-1 || objName.indexOf("Survivor3_DZ")!=-1 ||
            objName.indexOf("Bandit1_DZ")!= -1 || objName.indexOf("Soldier1_DZ")!= -1)
        ed.entityType = EntityData::type::players;
    else
        ed.entityType = EntityData::type::stuff;

    entityArray.append(ed);
}

Каждая сущность представляет собой примерно такую структуру

Структура сущности

class Entity
{
public:
char _0x0000[24];
    EntityVisualState* entityVisualState; //0x0018 
char _0x001C[32];
    CfgVehicle* cfgVehicle; //0x003C 
char _0x0040[476];
    EntityInventory* entityInventory; //0x021C 

};//Size=0x0220

Здесь нам интересны все три указателя.

  • EntityVisualState — информация о местоположении.
  • CfgVehicle — характеристики (название, тип, максимальная скорость и т.д.).
  • EntityInventory — инвентарь (чтение инвентаря не реализовано, т.к. для моих целей это излишне).

Из CfgVehicle мы читаем имя и тип.

ArmaString* entityName; //0x0030 
ArmaString* objectType; //0x006C 

EntityVisualState

class EntityVisualState
{
public:
char _0x0000[4];
    D3DXVECTOR3 dimension; //0x0004 
    D3DXVECTOR3 rotation1; //0x0010 
    D3DXVECTOR3 direction; //0x001C 
    D3DXVECTOR3 coordinates; //0x0028 
char _0x0034[20];
    D3DXVECTOR3 velocity; //0x0048 
    float angularVelocity; //0x0054 
    float zVelocity2; //0x0058 
    float Speed; //0x005C 
    D3DXVECTOR3 acceleration; //0x0060 
char _0x006C[16];
    D3DXVECTOR3 direction2; //0x007C 
    D3DXVECTOR3 rotation2; //0x0088 
    D3DXVECTOR3 direction3; //0x0094 
char _0x00A0[12];
    float fuelLevel; //0x00AC 
char _0x00B0[92];
    D3DXVECTOR3 headCoordinates; //0x010C 
    D3DXVECTOR3 torsoCoordinates; //0x0118 
char _0x0124[244];
    float N047F1D6C; //0x0218 
char _0x021C[200];

};//Size=0x02E4

Из EntityVisualState мы читаем вектор координат, который представляет собой структуру из трёх переменных.

D3DXVECTOR3 coordinates;

struct D3DXVECTOR3 {
  FLOAT x;
  FLOAT y;
  FLOAT z;
};

Здесь нам нужны только x и y(на самом деле z), поэтому читаем их так:

coordX = mem.readFloat(pEntityVisualState + 0x28);
coordY = mem.readFloat(pEntityVisualState + 0x30);

Кстати, в карту additionalFields, которая в EntityData, на этом этапе можно записать любую дополнительную информацию. Например, содержимое инвентаря или скорость перемещения.

Сейчас мы получили и классифицировали информацию о всех сущностях в игровом мире, теперь её нужно как-то отобразить, для этого я использовал QPainter.

Создаём класс виджета для рисования.

class InteractiveMap : public QWidget
{
    Q_OBJECT

public:
    InteractiveMap(QWidget* pwgt = nullptr);
    virtual ~InteractiveMap();

protected:
    virtual void paintEvent(QPaintEvent* pe);

private:
    //Константы масштабирования(на колёсико мыши)
    const float minScale = 1.0f;
    const float maxScale = 8.0f;
    const float scaleStep= 2.0f;

    void updateScale(const qreal value, const QPointF& dpos);
    void updateTranslate(const QPointF& value);

    bool getFilterValue(EntityData::type t);
    bool getFilterValue(QString t);

    void mousePressEvent  (QMouseEvent* pe);
    void mouseMoveEvent   (QMouseEvent* pe);
    void wheelEvent       (QWheelEvent *pe);

    void findCloseObjects(QPointF coords);
    QVector<CloseObjects>* input;

    QPainter*   painter;
    QPixmap*    image;
    WorldState* worldState;

    qreal scale;
    QPointF translate;
    QPoint startMove;

    //Кэшированная картинка
    QPixmap cache;

    QMutex renderMutex;

    //Асинхронный поиск объектов, близких к курсору
    QFutureWatcher<QString> closeObjWatcher;
    QFuture<QString> closeObjFuture;

public slots:
    //Загрузка состояния
    void loadState(QString stateFile);
    void loadDump(QString dumpFile, QString idxFile);

    void closeState();
    void saveState(QString stateFile);
    void updateCache();
    void sendCloseObjects();

signals:
    void showCloseObjects(QString str);
    void saveStateChanged(bool state);
};

Метки с техникой я рисую поверх картинки с картой. Метки на карте для текущего масштаба я кэширую в QPixmap (дорого рисовать заново несколько сотен или тысяч объектов при каждом сдвиге камеры).

void InteractiveMap::paintEvent(QPaintEvent *pe)
{
    renderMutex.lock();
    painter->begin(this);
//////////////////////////////////////////////////
    QTransform mat;
    painter->setTransform(mat);
    painter->scale(scale, scale);
    painter->translate(translate);
    painter->drawPixmap(0,0, *image);

    if(cache.isNull())
    {
        //Важно увеличить DPR, иначе метки будут смазаны при сильном увеличении
        cache = QPixmap(image->size()*4);
        cache.setDevicePixelRatio(4);
        cache.fill(Qt::transparent);
        QPainter cachePaint(&cache);
        //Бежим по всем типам объектов
        for(QMap<EntityData::type, EntityRange>::const_iterator it = worldState->entityRanges.cbegin(); it!=worldState->entityRanges.cend();++it)
        {
            //Проверяем нужно ли отображать этот тип
            if(getFilterValue(it.key()))
            {
                for(QVector<EntityData>::const_iterator i = it.value().start; i!= it.value().end; ++i)
                {
                    float x = i->getCoords().x();
                    float y = i->getCoords().y();

                    //Преобразуем координаты по магической формуле
                    x = (((x) / (15360.0f / 975.0f)));
                    y = (((15360.0f - y) / (15360.0f / 970.0f)) - 4.0f);

                    //Рисуем точку
                    QFont font("Arial");
                    QPen  pen;
                    pen.setWidthF(4.0f/scale);
                    pen.setStyle(Qt::SolidLine);
                    font.setPointSizeF(qMax(float(8.0f*1.0f/scale),2.0f));
                    cachePaint.setFont(font);
                    cachePaint.setPen(pen);
                    cachePaint.drawPoint(x,y);

                    //Рисуем название объекта, если нужно
                    if(getFilterValue(QString("name")))
                        cachePaint.drawText(x,y,i->shortDescription());

                }
            }
        }
    }
    painter->drawPixmap(0,0,cache);
//////////////////////////////////////////////////
    painter->end();
    renderMutex.unlock();
}

Для выбора типов сущностей, которые нужно отображать и других настроек, я использую QCheckBox-ы на боковой панели (их реализацию можно будет глянуть на гитхабе). Для связи отрисовки с настройками, я сначала использовал голый QSettings, но оказалось, что он не кэширует настройки в памяти, а напрямую работает с реестром, поэтому мне пришлось написать обёрточный синглтон с кэшем, который также при обновлении параметров посылает сигнал на перерисовку.

class SettingsManager : public QObject
{
    Q_OBJECT
public:
    SettingsManager();
    ~SettingsManager();
    static SettingsManager& instance();
    QVariant value(const QString &key, const QVariant &defaultValue = QVariant());
    void setValue(const QString &key, const QVariant &value);

    SettingsManager(SettingsManager const&) = delete;
    SettingsManager& operator= (SettingsManager const&) = delete;
private:
    QMap<QString, QVariant> data;
    QSettings settings;
signals:
    void updateMap();
};

Для удобного просмотра карты я реализовал масштабирование на курсор (на колёсико мыши) и сдвиг (с зажатым лкм-ом). Ещё одна важная фича — просмотр полных характеристик и игровых координат сущностей (при нажатии скм в район нужных объектов).

//Устанавливаем новый масштаб
void InteractiveMap::updateScale(qreal value, const QPointF& dpos)
{
    qreal newScale = scale * value;
    if(newScale >= minScale && newScale <= maxScale)
    {
        scale = newScale;
        //Добавляем смещение для масштабирования в точку курсора
        translate += dpos/scale;
        updateCache();
    }
}

//Устанавливаем новое смещение
void InteractiveMap::updateTranslate(const QPointF& value)
{
    QPointF newV = translate + (value * 1/scale);
    translate = newV;
    update();
}

//Обработка нажатия кнопок мыши
void InteractiveMap::mousePressEvent(QMouseEvent *pe)
{
    //Сдвиг при зажатии лкм
    if(pe->buttons() & Qt::LeftButton)
        startMove = pe->pos();
    //Поиск близких к курсору объектов на скм
    else if(pe->buttons() & Qt::MidButton)
    {
        if(worldState)
        {
            //Определяем координаты на карте, с учётом масштаба
            QPointF pos = pe->pos()/scale - translate;
            if(pos.x() >= 0.0f && pos.x() <= image->width() && pos.y() >= 0.0f && pos.y() <= image->height())
            {
                //Переводим координаты во внутренние игровые
                pos.rx() = pos.x() * (15360.0f / 975.0f);
                pos.ry() = -((15360.0f/970.0f)*(pos.y()+4.0f)-15360.0f);
                //Вызываем асинхронный поиск
                findCloseObjects(pos);
            }
        }

    }
}

void InteractiveMap::mouseMoveEvent(QMouseEvent *pe)
{
    //Сдвиг при зажатии лкм
    if(pe->buttons() & Qt::LeftButton)
    {
        updateTranslate(pe->pos() - startMove);
        startMove = pe->pos();
    }
}

void InteractiveMap::wheelEvent(QWheelEvent *pe)
{
    //Обработка масштабирования
    float dScale = (pe->angleDelta().y() < 0) ? 1/scaleStep : scaleStep;

    QPointF nPos = pe->pos() * (dScale);
    QPointF dPos = pe->pos() - nPos;

    updateScale(dScale,dPos);
}

Просмотр характеристик реализован с помощью фреймворка QtConcurrent, посредством модели MapReduce.

//Reduce функция
void addToAnswer(QString& result, const QString& interm)
{
    if(!interm.isEmpty())
        result += interm;
}

void InteractiveMap::findCloseObjects(QPointF coords)
{
    if(!closeObjWatcher.isRunning())
    {
        //Собираем входные данные
        input = new QVector<CloseObjects>;
        for(QMap<EntityData::type, EntityRange>::iterator it = worldState->entityRanges.begin(); it!=worldState->entityRanges.end();++it)
        {
            if(getFilterValue(it.key()))
            {
                //Создаём входной объект
                CloseObjects obj(&it.value(), coords);
                input->append(obj);
            }
        }
        closeObjFuture = QtConcurrent::mappedReduced(*input, &CloseObjects::findCloseObjects, addToAnswer);
        //После завершения вычислений посылаем сигнал
        connect(&closeObjWatcher, &QFutureWatcher<QString>::finished, this, &InteractiveMap::sendCloseObjects);
        //Запускаем вычисления
        closeObjWatcher.setFuture(closeObjFuture);
    }
}

void InteractiveMap::sendCloseObjects()
{
    //Отправляем результаты для отображения
    emit showCloseObjects(closeObjWatcher.result());
    //Не забываем очистить входной массив
    delete input;
    input = nullptr;
}

Входной класс состоит из указателя на категорию сущностей и точки для поиска.

class CloseObjects
{
public:
    CloseObjects() {}
    CloseObjects(EntityRange *r, QPointF p): range(r), coords(p) {}
    QString findCloseObjects() const;
private:
    EntityRange*    range;
    QPointF         coords;
};

В Map функции, мы проходим по всем объектам в категории, если сущность находится в константном радиусе от позиции курсора, то возвращаем полное описание объекта (название + дополнительные поля) и игровые координаты.

QString CloseObjects::findCloseObjects() const
{
    QString result;
    QTextStream stream(&result);
    //Устанавливаем точность в выводе до 2 цифр после запятой
    stream.setRealNumberNotation(QTextStream::FixedNotation);
    stream.setRealNumberPrecision(2);
    for(QVector<EntityData>::const_iterator it = range->start; it != range->end; ++it)
    {
        float len = qSqrt(qPow((it->getCoords().x() - coords.x()),2) + qPow((it->getCoords().y() - coords.y()),2));
        if(len <= 350)
        {
            stream << it->fullDescription() << "n" << QVariant(it->getCoords().x()/100).toFloat() << " " << QVariant((15360 - it->getCoords().y())/100).toFloat() << "n";
        }
    }
    return result;
}

На этом я заканчиваю свой рассказ. Кому не трудно, гляньте код, укажите на возможные косяки.
Заинтересованные могут добавить чтение дополнительных характеристик объектов и кинуть pull-request.

Ссылки:

Автор: EvilWind

Источник

Поделиться

* - обязательные к заполнению поля