wip blitzapi, load runes, set runes

This commit is contained in:
mrbesen 2022-07-09 01:01:51 +02:00
parent e893852bc1
commit d3379ab794
Signed by untrusted user: MrBesen
GPG Key ID: 596B2350DCD67504
20 changed files with 513 additions and 13 deletions

27
include/blitzapi.h Normal file
View File

@ -0,0 +1,27 @@
#pragma once
#include <vector>
#include <QJsonObject>
#include "restclient.h"
#include "position.h"
class BlitzAPI : public RestClient {
public:
BlitzAPI();
struct ChampionInfo {
std::vector<uint32_t> skillorder;
std::vector<uint32_t> runes;
uint32_t primaryRune = 0;
// items?
ChampionInfo();
explicit ChampionInfo(const QJsonObject&);
};
ChampionInfo getChampionInfo(uint32_t championID, Position p, uint32_t enemyChampionID = 0); // TODO: add more parameters: Queue (Ranked 5x5)
private:
};

View File

@ -3,6 +3,7 @@
#include "clientaccess.h"
#include "restclient.h"
#include "position.h"
#include "runeaspekt.h"
class ClientAPI : public RestClient {
public:
@ -128,6 +129,24 @@ public:
operator bool();
};
struct RunePage {
uint64_t id = 0;
uint64_t lastmodified = 0;
uint32_t primaryStyleID = 0;
uint32_t subStyleID = 0;
std::string name;
bool isDeleteable = true;
bool isEditable = true;
bool isActive = false; // what is the difference between active and current????
bool isCurrent = false;
bool isValid = true;
uint32_t order = 0; // position in the ui
std::vector<uint32_t> selectedPerkIDs;
RunePage();
explicit RunePage(const QJsonObject& json);
};
ClientAPI(const ClientAccess& access);
~ClientAPI();
@ -145,6 +164,14 @@ public:
TimerInfo getTimerInfo();
// rune stuff
RunePage getCurrentRunePage();
std::vector<RunePage> getAllRunePages();
bool selectRunePage(uint64_t id);
bool editRunePage(const RunePage& page);
std::vector<RuneAspekt> getAllRuneAspekts();
protected:

View File

@ -12,9 +12,15 @@ T convert(const QJsonValue& val) {
template<>
int convert(const QJsonValue& val);
template<>
uint32_t convert(const QJsonValue& val);
template<>
int64_t convert(const QJsonValue& val);
template<>
uint64_t convert(const QJsonValue& val);
template<>
std::string convert(const QJsonValue& val);

View File

@ -4,6 +4,7 @@
#include <memory>
#include <mutex>
#include "blitzapi.h"
#include "clientapi.h"
#include "config.h"
#include "datadragon.h"
@ -34,6 +35,9 @@ protected:
std::shared_ptr<ClientAPI> clientapi;
BlitzAPI blitzapi;
std::vector<RuneAspekt> runeaspekts;
public:
enum class State {
LOBBY = 0,
@ -53,6 +57,9 @@ public:
void reload(); // reload the config, when something was changed
const std::vector<RuneAspekt>& getRuneAspekts();
void applyRunes();
private:
void stopJoinThread();
void innerRun();

View File

@ -30,12 +30,15 @@ private slots:
void tabtoggled(Position, LolAutoAccept::State, bool);
void tabchanged(Position, LolAutoAccept::State);
void applyRunes();
signals:
void requestTabChange(int tabindex);
private:
// returns empty string on no match
void onPosChange(Position newpos); // to trigger the signal from a QObject
void onRuneChanged(const std::vector<uint32_t>& runes, uint32_t prim, uint32_t sec);
Ui::MainWindow *ui;
std::thread lolaathread;

View File

@ -7,6 +7,7 @@
class RestClient {
public:
RestClient(const std::string& base);
RestClient(const RestClient&) = delete;
virtual ~RestClient();
enum class Method {
@ -21,6 +22,7 @@ protected:
QByteArray requestRaw(const std::string& url, Method m = Method::GET, const std::string& data = {});
QJsonDocument request(const std::string& url, Method m = Method::GET, const std::string& data = {});
void enableDebugging(bool enabled = true);
std::string escape(const std::string& in) const;
std::string baseurl;

18
include/runeaspekt.h Normal file
View File

@ -0,0 +1,18 @@
#pragma once
#include <cstdint>
#include <string>
class QJsonObject;
struct RuneAspekt {
uint32_t id = 0;
std::string name;
std::string shortDesc;
std::string longDesc;
std::string tooltip;
std::string iconPath;
RuneAspekt();
explicit RuneAspekt(const QJsonObject& json);
};

30
include/runedisplay.h Normal file
View File

@ -0,0 +1,30 @@
#pragma once
#include <QWidget>
#include "runeaspekt.h"
namespace Ui {
class RuneDisplay;
}
class RuneDisplay : public QWidget {
Q_OBJECT
public:
explicit RuneDisplay(QWidget *parent = nullptr);
~RuneDisplay();
void setRuneMeta(const std::vector<RuneAspekt>& runeinfo);
void setRunes(std::vector<uint32_t> ids, uint32_t primary, uint32_t secondary);
private:
void updateText();
std::string getRuneText(uint32_t id);
Ui::RuneDisplay *ui;
std::vector<uint32_t> runes;
uint32_t primary;
uint32_t secondary;
std::vector<RuneAspekt> runeinfo;
};

View File

@ -28,6 +28,7 @@ defineReplace(prependAll) {
SOURCES += \
src/arg.cpp \
src/blitzapi.cpp \
src/champcache.cpp \
src/championsearch.cpp \
src/champrow.cpp \
@ -44,6 +45,7 @@ SOURCES += \
src/mainwindow.cpp \
src/memoryimagecache.cpp \
src/restclient.cpp \
src/runedisplay.cpp \
src/settingstab.cpp \
src/stagesettings.cpp \
thirdparty/Log/Log.cpp
@ -52,6 +54,7 @@ SOURCES += \
HEADERS += \
include/arg.h \
include/blitzapi.h \
include/champcache.h \
include/championsearch.h \
include/champrow.h \
@ -67,6 +70,7 @@ HEADERS += \
include/mainwindow.h \
include/memoryimagecache.h \
include/restclient.h \
include/runedisplay.h \
include/settingstab.h \
include/stagesettings.h \
thirdparty/Log/Log.h
@ -80,6 +84,7 @@ OBJECTS_DIR = build/
FORMS += \
ui/championsearch.ui \
ui/mainwindow.ui \
ui/runedisplay.ui \
ui/settingstab.ui \
ui/stagesettings.ui

113
src/blitzapi.cpp Normal file
View File

@ -0,0 +1,113 @@
#include "blitzapi.h"
#include <QJsonArray>
#include <QJsonDocument>
#include <Log.h>
#include "json.h"
// curl 'https://league-champion-aggregate.iesdev.com/graphql?query=query%20ChampionBuilds%28%24championId%3AInt%21%2C%24queue%3AQueue%21%2C%24role%3ARole%2C%24opponentChampionId%3AInt%2C%24key%3AChampionBuildKey%29%7BchampionBuildStats%28championId%3A%24championId%2Cqueue%3A%24queue%2Crole%3A%24role%2CopponentChampionId%3A%24opponentChampionId%2Ckey%3A%24key%29%7BchampionId%20opponentChampionId%20queue%20role%20builds%7BcompletedItems%7Bgames%20index%20averageIndex%20itemId%20wins%7Dgames%20mythicId%20mythicAverageIndex%20primaryRune%20runes%7Bgames%20index%20runeId%20wins%20treeId%7DskillOrders%7Bgames%20skillOrder%20wins%7DstartingItems%7Bgames%20startingItemIds%20wins%7DsummonerSpells%7Bgames%20summonerSpellIds%20wins%7Dwins%7D%7D%7D&variables=%7B%22championId%22%3A25%2C%22role%22%3A%22SUPPORT%22%2C%22queue%22%3A%22RANKED_SOLO_5X5%22%2C%22opponentChampionId%22%3Anull%2C%22key%22%3A%22PUBLIC%22%7D' -H 'Accept: application/json'
// query=query ChampionBuilds($championId:Int!,$queue:Queue!,$role:Role,$opponentChampionId:Int,$key:ChampionBuildKey){championBuildStats(championId:$championId,queue:$queue,role:$role,opponentChampionId:$opponentChampionId,key:$key){championId opponentChampionId queue role builds{completedItems{games index averageIndex itemId wins}games mythicId mythicAverageIndex primaryRune runes{games index runeId wins treeId}skillOrders{games skillOrder wins}startingItems{games startingItemIds wins}summonerSpells{games summonerSpellIds wins}wins}}}
// &variables={"championId":25,"role":"SUPPORT","queue":"RANKED_SOLO_5X5","opponentChampionId":null,"key":"PUBLIC"}
static const std::string POSITIONNAMES[] = {"INVALID", "TOP", "JUNGLE", "MIDDLE", "BOTTOM", "SUPPORT"};
BlitzAPI::BlitzAPI() : RestClient("https://league-champion-aggregate.iesdev.com/graphql?") {}
BlitzAPI::ChampionInfo::ChampionInfo() {}
BlitzAPI::ChampionInfo::ChampionInfo(const QJsonObject& json) {
primaryRune = getValue<uint32_t>(json, "primaryRune");
// kill order stuff
auto skillordersref = json["skillOrders"];
if(skillordersref.isArray()) {
QJsonArray arr = skillordersref.toArray();
if(!arr.empty()) {
auto skillorderref = arr.at(0);
if(skillorderref.isObject()) {
QJsonObject skillorder = skillorderref.toObject();
QJsonValueRef realorderref = skillorder["skillOrder"];
if(realorderref.isArray()) {
QJsonArray realorder = realorderref.toArray();
this->skillorder.reserve(realorder.size());
for(auto it : realorder) {
if(it.isDouble()) {
this->skillorder.push_back(it.toDouble());
}
}
}
}
}
}
// runes
QJsonValue runesarrref = json["runes"];
if(runesarrref.isArray()) {
QJsonArray runesarr = runesarrref.toArray();
runes.reserve(runesarr.size());
for(auto it : runesarr) {
if(!it.isObject()) continue;
QJsonObject rune = it.toObject();
auto runeid = rune["runeid"];
if(runeid.isDouble()) {
runes.push_back(runeid.toDouble());
}
}
}
}
BlitzAPI::ChampionInfo BlitzAPI::getChampionInfo(uint32_t championID, Position p, uint32_t enemyChampionID) {
QJsonObject vars;
vars["championId"] = (int) championID;
vars["role"] = QString::fromStdString(POSITIONNAMES[(int) p]);
vars["queue"] = "RANKED_SOLO_5X5";
if(enemyChampionID == 0)
vars["opponentChampionId"] = QJsonValue::Null;
else
vars["opponentChampionId"] = (int) enemyChampionID;
vars["key"] = "PUBLIC"; // ? what does this do?
QJsonDocument jvars(vars);
const std::string variables = jvars.toJson().toStdString();
const std::string query = "query ChampionBuilds($championId:Int!,$queue:Queue!,$role:Role,$opponentChampionId:Int,$key:ChampionBuildKey){championBuildStats(championId:$championId,queue:$queue,role:$role,opponentChampionId:$opponentChampionId,key:$key){championId opponentChampionId queue role builds{completedItems{games index averageIndex itemId wins}games mythicId mythicAverageIndex primaryRune runes{games index runeId wins treeId}skillOrders{games skillOrder wins}startingItems{games startingItemIds wins}summonerSpells{games summonerSpellIds wins}wins}}}";
const std::string requeststr = "query=" + escape(query) + "&variables=" + escape(variables);
QJsonDocument doc = request(requeststr);
if(!doc.isObject()) {
// error
return {};
}
// Log::info << "returned: " << doc.toJson().toStdString();
QJsonObject obj = doc.object();
QJsonValueRef dataref = obj["data"];
if(!dataref.isObject()) return {};
QJsonObject data = dataref.toObject();
QJsonValueRef buildstatsref = data["championBuildStats"];
if(!buildstatsref.isObject()) return{};
QJsonObject buildstats = buildstatsref.toObject();
QJsonValueRef buildsref = buildstats["builds"];
if(!buildsref.isArray()) return {};
QJsonArray builds = buildsref.toArray();
if(builds.size() > 0) {
// just take the first
QJsonValue buildval = builds.at(0);
if(buildval.isObject()) {
return (ChampionInfo) buildval.toObject();
}
}
return {};
}

View File

@ -94,6 +94,7 @@ ClientAPI::PlayerInfo ClientAPI::getSelf() {
info.gameName = getValue<std::string>(obj, "gameName");
info.name = getValue<std::string>(obj, "name");
info.statusMessage = getValue<std::string>(obj, "statusMessage", "");
info.summonerid = getValue<uint64_t>(obj, "summonerId");
auto lolref = obj["lol"];
if(lolref.isObject()) {
@ -165,3 +166,83 @@ ClientAPI::TimerInfo ClientAPI::getTimerInfo() {
return (TimerInfo) obj;
}
ClientAPI::RunePage ClientAPI::getCurrentRunePage() {
QJsonDocument doc = request("lol-perks/v1/currentpage");
if(!doc.isObject()) return {};
return (RunePage) doc.object();
}
std::vector<ClientAPI::RunePage> ClientAPI::getAllRunePages() {
QJsonDocument doc = request("lol-perks/v1/pages");
if(!doc.isArray()) return {};
QJsonArray arr = doc.array();
std::vector<RunePage> out;
out.reserve(arr.size());
for(auto it : arr) {
if(!it.isObject()) {
out.push_back((RunePage) it.toObject());
}
}
return out;
}
bool ClientAPI::selectRunePage(uint64_t id) {
QJsonDocument doc = request("lol-perks/v1/currentpage", Method::PUT, std::to_string(id));
if(doc.isEmpty()) return true; // ok
// error
Log::warn << "error selecting runepage: " << id << " " << doc.toJson().toStdString();
return false;
}
bool ClientAPI::editRunePage(const RunePage& page) {
QJsonObject pagereq;
pagereq["id"] = (int) page.id;
pagereq["name"] = QString::fromStdString(page.name);
pagereq["primaryStyleId"] = (int) page.primaryStyleID;
pagereq["subStyleId"] = (int) page.subStyleID;
QJsonArray selected;
for(uint32_t sel : page.selectedPerkIDs) {
selected.push_back((int) sel);
}
pagereq["selectedPerkIds"] = selected;
QJsonDocument reqdoc(pagereq);
QJsonDocument doc = request("lol-perks/v1/pages", Method::POST, reqdoc.toJson().toStdString());
if(doc.isEmpty()) return true; // ok
// error
Log::warn << "error editing runepage: " << page.id << " " << doc.toJson().toStdString();
return false;
}
std::vector<RuneAspekt> ClientAPI::getAllRuneAspekts() {
QJsonDocument doc = request("lol-perks/v1/perks");
if(!doc.isArray()) {
Log::warn << __PRETTY_FUNCTION__ << " doc is not array";
return {};
}
QJsonArray arr = doc.array();
std::vector<RuneAspekt> out;
out.reserve(arr.size());
for(auto it : arr) {
if(it.isObject()) {
out.push_back((RuneAspekt) it.toObject());
}
}
return out;
}

View File

@ -36,6 +36,18 @@ static T mapEnum(const std::string& input, const std::string* names, uint32_t co
#define MAPENUM(VAR, ENUM, DEFAULT) \
mapEnum(VAR, ENUM ## Names, ENUM ## NamesCount, ClientAPI:: ENUM :: DEFAULT)
template<typename T>
static std::vector<T> readVector(QJsonArray arr) {
std::vector<T> out;
out.reserve(arr.size());
for(auto it : arr) {
out.push_back(convert<T>((QJsonValue) it));
}
return out;
}
ClientAPI::ReadyCheckState ClientAPI::toReadyCheckState(const QJsonObject& obj) {
std::string searchState = getValue<std::string>(obj, "state", "Invalid");
std::string playerresponse = getValue<std::string>(obj, "playerResponse", "None");
@ -170,6 +182,35 @@ ClientAPI::ChampSelectSession::operator bool() {
return gameid != 0;
}
ClientAPI::RunePage::RunePage() {}
ClientAPI::RunePage::RunePage(const QJsonObject& json) {
id = getValue<int32_t>(json, "id", 0);
lastmodified = getValue<int64_t>(json, "lastModified", 0);
primaryStyleID = getValue<int32_t>(json, "primaryStyleId", 0);
subStyleID = getValue<int32_t>(json, "subStyleId", 0);
name = getValue<std::string>(json, "name");
isDeleteable = getValue<bool>(json, "isDeletable", false);
isEditable = getValue<bool>(json, "isEditable", false);
isActive = getValue<bool>(json, "isActive", false);
isCurrent = getValue<bool>(json, "current", false);
isValid = getValue<bool>(json, "isValid", false);
order = getValue<int32_t>(json, "order", 0);
auto selectedref = json["selectedPerkIds"];
if(selectedref.isArray()) {
selectedPerkIDs = readVector<uint32_t>(selectedref.toArray());
}
}
RuneAspekt::RuneAspekt() {}
RuneAspekt::RuneAspekt(const QJsonObject& json) {
id = getValue<int32_t>(json, "id", 0);
name = getValue<std::string>(json, "name");
shortDesc = getValue<std::string>(json, "shortDesc");
longDesc = getValue<std::string>(json, "longDesc");
tooltip = getValue<std::string>(json, "tooltip");
iconPath = getValue<std::string>(json, "iconPath");
}
#define PRINTENUM(ENUMNAME) \
std::ostream& operator<<(std::ostream& str, const ClientAPI:: ENUMNAME & state) { \

View File

@ -7,6 +7,13 @@ int convert(const QJsonValue& val) {
return val.toInt();
}
template<>
uint32_t convert(const QJsonValue& val) {
if(val.isString())
return val.toString().toUInt();
return (uint32_t) val.toDouble();
}
template<>
int64_t convert(const QJsonValue& val) {
if(val.isString())
@ -14,6 +21,13 @@ int64_t convert(const QJsonValue& val) {
return (int64_t) val.toDouble();
}
template<>
uint64_t convert(const QJsonValue& val) {
if(val.isString())
return val.toString().toULongLong();
return (uint64_t) val.toDouble();
}
template<>
std::string convert(const QJsonValue& val) {
return val.toString().toStdString();

View File

@ -70,6 +70,22 @@ void LolAutoAccept::reload() {
loadPosition(currentPosition);
}
const std::vector<RuneAspekt>& LolAutoAccept::getRuneAspekts() {
if(runeaspekts.empty()) {
if(clientapi) {
runeaspekts = clientapi->getAllRuneAspekts();
Log::info << "Loaded " << runeaspekts.size() << " rune aspekts";
}
}
return runeaspekts;
}
void LolAutoAccept::applyRunes() {
// TODO
Log::warn << "LolAutoAccept::applyRunes() not implemented";
}
void LolAutoAccept::stopJoinThread() {
stop();

View File

@ -52,8 +52,12 @@ void MainWindow::toggleMainswitch(bool state) {
return;
}
ui->runedisplay->setRuneMeta(lolaa.getRuneAspekts());
lolaa.run();
ui->statusbar->showMessage(tr("Auto-Acceptor started!"));
} else {
lolaa.stop();
ui->statusbar->showMessage(tr("Auto-Acceptor stoped!"));
@ -83,8 +87,18 @@ void MainWindow::tabchanged(Position p, LolAutoAccept::State s) {
lolaa.reload();
}
void MainWindow::applyRunes() {
Log::info << "applyRunes pressed";
lolaa.applyRunes();
}
void MainWindow::onPosChange(Position newpos) {
if(newpos != Position::INVALID) {
emit requestTabChange((int) newpos -1);
}
}
void MainWindow::onRuneChanged(const std::vector<uint32_t>& runes, uint32_t prim, uint32_t sec) {
ui->runedisplay->setRunes(runes, prim, sec);
}

View File

@ -77,21 +77,19 @@ QByteArray RestClient::requestRaw(const std::string& url, Method m, const std::s
if (!curl) return {};
std::string requrl = baseurl + url;
QByteArray ba; //buffer
// std::cout << "[DEBUG] requrl is: " << requrl << std::endl;
curl_easy_setopt(curl, CURLOPT_URL, requrl.c_str());
// curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1); //Prevent "longjmp causes uninitialized stack frame" bug
// set callback data
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &ba);
curl_easy_setopt(curl, CURLOPT_USERPWD, basicauth.c_str());
curl_easy_setopt(curl, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
if (disableCertCheck) {
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
}
// restore default HTTP Options
curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L);
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, NULL);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, NULL);
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, NULL);
// curl header
struct curl_slist* headerlist = NULL;
@ -121,9 +119,11 @@ QByteArray RestClient::requestRaw(const std::string& url, Method m, const std::s
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE"); break;
}
if (headerlist) {
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headerlist);
}
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headerlist);
QByteArray ba; //buffer
// set callback data
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &ba);
CURLcode res = curl_easy_perform(curl);
if (headerlist) {
@ -168,3 +168,10 @@ void RestClient::enableDebugging(bool enabled) {
curl_easy_setopt(curl, CURLOPT_VERBOSE, 0L);
}
}
std::string RestClient::escape(const std::string& in) const {
char* e = curl_easy_escape(curl, in.c_str(), in.length());
std::string esc(e);
curl_free(e);
return esc;
}

45
src/runedisplay.cpp Normal file
View File

@ -0,0 +1,45 @@
#include "runedisplay.h"
#include "ui_runedisplay.h"
#include <sstream>
RuneDisplay::RuneDisplay(QWidget *parent) : QWidget(parent), ui(new Ui::RuneDisplay) {
ui->setupUi(this);
}
RuneDisplay::~RuneDisplay() {
delete ui;
}
void RuneDisplay::setRuneMeta(const std::vector<RuneAspekt>& ri) {
runeinfo = ri;
}
void RuneDisplay::setRunes(std::vector<uint32_t> ids, uint32_t primary, uint32_t secondary) {
runes = ids;
this->primary = primary;
this->secondary = secondary;
updateText();
}
void RuneDisplay::updateText() {
std::ostringstream out;
out << "primary: " << getRuneText(primary) << " secondary: " << getRuneText(secondary) << " ";
for(uint32_t rune : runes) {
out << getRuneText(rune);
}
ui->runetext->setText(QString::fromStdString(out.str()));
}
std::string RuneDisplay::getRuneText(uint32_t id) {
for(const RuneAspekt& ra : runeinfo) {
if(ra.id == id) {
return ra.name;
}
}
return "(" + std::to_string(id) + ")";
}

View File

@ -83,4 +83,4 @@ StageSettings* SettingsTab::getStage(LolAutoAccept::State s) const {
}
assert(false); // "invalid" stage (Lobby or Game)
return nullptr;
}
}

View File

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>432</width>
<height>630</height>
<width>433</width>
<height>638</height>
</rect>
</property>
<property name="sizePolicy">
@ -115,6 +115,16 @@
</widget>
</widget>
</item>
<item>
<widget class="QPushButton" name="applyRunesBtn">
<property name="text">
<string>Apply Runes</string>
</property>
</widget>
</item>
<item>
<widget class="RuneDisplay" name="runedisplay" native="true"/>
</item>
</layout>
</widget>
</widget>
@ -123,7 +133,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>432</width>
<width>433</width>
<height>24</height>
</rect>
</property>
@ -137,6 +147,12 @@
<header>settingstab.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>RuneDisplay</class>
<extends>QWidget</extends>
<header>runedisplay.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>mainswitch</tabstop>

28
ui/runedisplay.ui Normal file
View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>RuneDisplay</class>
<widget class="QWidget" name="RuneDisplay">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="runetext">
<property name="text">
<string>Runes: </string>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>