学校の夏のある夜、私はDayZ Mod(Arma 2 OA)のmaphackが必要になりました。 トピックに関する情報を検索した後、Battleyeアンチチートに関与してはならないことに気付きました。カーネル保護ドライバーをバイパスする知識も経験もないため、ゲームプロセスへのアクセスに大量のフックを配置したためです。
DayZは、ゲームプレイに重要なオブジェクトの場所が頻繁に変更されず、再起動後も残る数少ないゲームの1つです(ベースは移動せず、ほとんどの機器も長時間静止しています)。 この事実は、RAMダンプを通じて攻撃の可能性を開きます。
最後にすべてのリンク。
第1章ダンピム
アンチチートをバイパスせずにランタイムでメモリのスナップショットを取得することは成功しそうにありません。 したがって、他の戦術を探しています。 最初に見つけるのはhabrastatyaで 、そこからどこを掘るかが明らかになります。
試行1
記事に記載されているホットセットを介してメモリイメージを取得できず、Ubuntu CyberPack(IRF)から起動し、fmemを介してイメージを取得することができませんでした。
少しグーグル、同じLiME〜Linux Memory Extractor機能を持つ代替ツールを見つけます。 今では、livecdで組み立てて実行する必要がありました。
ディストリビューションの選択はTinyCore(TinyCorePure64、3 GB以上をダンプする必要がある場合)に依存していました。 それからダウンロードした後、パッケージをダウンロードしてインストールします。
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"
必要なプロセスのメモリを取得するために、誰かがこのファイルをフィードする必要があります。 これを行うには、memdumpプラグインを備えたVolatility Frameworkが必要です。
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フレームワーク
rekal -f F:\mem-image.lime pslist rekal -f F:\mem-image.lime memdump dump_dir="./output", pids=868
しかし、私がやらないこと、彼は始めたくなかったので、私は掘り続けました。
Windows 10でRekallを使用する場合、ダンプで何かを初めて検索すると、次のようなメッセージが表示される場合があります。
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
メモリの最も完全なスナップショットを取得するには、Windowsが休止状態に切り替わったときにすべてのメモリを保存するhiberfil.sysファイルを使用できます。
ここでも簡単です。休止状態モードに切り替え、任意のlivecd(私の場合は同じTinyCore)から起動し、システムディスク(読み取り専用)とUSBフラッシュドライブをマウントし、目的のファイルをコピーします
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
次に、このファイルにボラティリティを与えることができます(win7以前で休止状態ファイルをサポートします)。
vol.py imagecopy -f hiberfil.sys -O win7.img
私はwin10を持っているので、このオプションは私には適していない。
このファイルを、サンドマンフレームワークであったHibr2binプログラムに渡そうとしました。
HIBR2BIN /PLATFORM X64 /MAJOR 10 /MINOR 0 /INPUT hiberfil.sys /OUTPUT uncompressed.bin
しかし、彼女は理解できない出力を提供し、分析のためのフレームワークはそれを使用することを拒否しました。
Hibernation Reconは、無料バージョンで問題を解決しました。問題なくフレームワークを読み込めるようになりました。
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章Maphackの作成。
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
ここでは、3つのポインターすべてに関心があります。
- EntityVisualState-ロケーション情報。
- CfgVehicle-特性(名前、タイプ、最大速度など)。
- EntityInventory-インベントリ(インベントリの読み取りは実装されていません。私の目的のためにはこれは不要です)。
CfgVehicleから名前とタイプを読み取ります。
ArmaString* entityName; //0x0030 ArmaString* objectType; //0x006C
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から、3つの変数の構造である座標ベクトルを読み取ります。
D3DXVECTOR3 coordinates;
struct D3DXVECTOR3 { FLOAT x; FLOAT y; FLOAT z; };
ここではxとy(実際はz)だけが必要なので、次のように読みます。
coordX = mem.readFloat(pEntityVisualState + 0x28); coordY = mem.readFloat(pEntityVisualState + 0x30);
ところで、EntityDataにあるadditionalFieldsカードには、この段階で追加情報を書き込むことができます。 たとえば、インベントリの内容や移動速度。
これで、ゲームワールドのすべてのエンティティに関する情報を受け取って分類しました。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を使用します(それらの実装はgithubで表示できます)。 レンダリングを設定に接続するために、最初にベアの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(); };
マップを見やすくするために、カーソル(マウスホイール上)とシフト(LMBを押した状態)によるスケーリングを実装しました。 もう1つの重要な機能は、エンティティの完全な特性とゲーム座標を表示することです(目的のオブジェクトの領域をクリックしたとき)。
// 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); }
機能の表示は、MapReduceモデルを使用して、QtConcurrentフレームワークを使用して実装されます。
//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; }
以上で私の話は終わりです。 それが難しくない人には、コードを見て、可能性のある妨害を指摘してください。
興味のある方は、オブジェクトの読み取り特性を追加して、プルリクエストを投げることができます。
参照: