mirror of
https://github.com/melonDS-emu/melonDS.git
synced 2025-03-06 21:00:31 +01:00
Merge 36a54cb8b2
into a9cce557d2
This commit is contained in:
commit
3113d3a3d5
19 changed files with 1333 additions and 8 deletions
6
.github/workflows/build-ubuntu.yml
vendored
6
.github/workflows/build-ubuntu.yml
vendored
|
@ -27,7 +27,7 @@ jobs:
|
|||
sudo rm -f /etc/apt/sources.list.d/dotnetdev.list /etc/apt/sources.list.d/microsoft-prod.list
|
||||
sudo apt update
|
||||
sudo apt install --allow-downgrades cmake ninja-build extra-cmake-modules libpcap0.8-dev libsdl2-dev libenet-dev \
|
||||
qt6-{base,base-private,multimedia}-dev libqt6svg6-dev libarchive-dev libzstd-dev libfuse2
|
||||
qt6-{base,base-private,multimedia}-dev libqt6svg6-dev libarchive-dev libzstd-dev libfuse2 liblua5.4-dev
|
||||
- name: Configure
|
||||
run: cmake -B build -G Ninja -DCMAKE_INSTALL_PREFIX=/usr -DMELONDS_EMBED_BUILD_INFO=ON
|
||||
- name: Build
|
||||
|
@ -69,8 +69,8 @@ jobs:
|
|||
apt update
|
||||
apt -y full-upgrade
|
||||
apt -y install git {gcc-12,g++-12}-aarch64-linux-gnu cmake ninja-build extra-cmake-modules \
|
||||
{libsdl2,qt6-{base,base-private,multimedia},libqt6svg6,libarchive,libzstd,libenet}-dev:arm64 \
|
||||
pkg-config dpkg-dev
|
||||
{libsdl2,qt6-{base,base-private,multimedia},libqt6svg6,libarchive,libzstd,libenet,liblua5.4}-dev:arm64 \
|
||||
pkg-config dpkg-dev
|
||||
- name: Check out source
|
||||
uses: actions/checkout@v4
|
||||
- name: Configure
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,4 +1,5 @@
|
|||
build*/
|
||||
debug*/
|
||||
bin
|
||||
obj
|
||||
*.depend
|
||||
|
|
|
@ -35,6 +35,7 @@
|
|||
qt6.qtbase
|
||||
qt6.qtmultimedia
|
||||
SDL2
|
||||
lua
|
||||
zstd
|
||||
libarchive
|
||||
libGL
|
||||
|
|
|
@ -57,6 +57,8 @@ set(SOURCES_QT_SDL
|
|||
|
||||
LANDialog.cpp
|
||||
NetplayDialog.cpp
|
||||
|
||||
LuaMain.cpp
|
||||
)
|
||||
|
||||
option(USE_QT6 "Use Qt 6 instead of Qt 5" ON)
|
||||
|
@ -75,6 +77,7 @@ set(CMAKE_AUTORCC ON)
|
|||
|
||||
find_package(Threads REQUIRED)
|
||||
find_package(PkgConfig REQUIRED)
|
||||
find_package(Lua REQUIRED)
|
||||
|
||||
if (BUILD_STATIC)
|
||||
list(APPEND PKG_CONFIG_EXECUTABLE "--static")
|
||||
|
@ -83,6 +86,7 @@ endif()
|
|||
pkg_check_modules(SDL2 REQUIRED IMPORTED_TARGET sdl2)
|
||||
pkg_check_modules(LibArchive REQUIRED IMPORTED_TARGET libarchive)
|
||||
pkg_check_modules(Zstd REQUIRED IMPORTED_TARGET libzstd)
|
||||
## pkg_search_module(Lua REQUIRED IMPORTED_TARGET lua)
|
||||
|
||||
fix_interface_includes(PkgConfig::SDL2 PkgConfig::LibArchive)
|
||||
|
||||
|
@ -173,6 +177,7 @@ endif()
|
|||
target_include_directories(melonDS PUBLIC
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/..")
|
||||
target_include_directories(melonDS PRIVATE ${LUA_INCLUDE_DIR})
|
||||
|
||||
if (USE_QT6)
|
||||
target_include_directories(melonDS PUBLIC ${Qt6Gui_PRIVATE_INCLUDE_DIRS})
|
||||
|
@ -180,7 +185,7 @@ else()
|
|||
target_include_directories(melonDS PUBLIC ${Qt5Gui_PRIVATE_INCLUDE_DIRS})
|
||||
endif()
|
||||
target_link_libraries(melonDS PRIVATE core)
|
||||
target_link_libraries(melonDS PRIVATE PkgConfig::SDL2 PkgConfig::LibArchive PkgConfig::Zstd)
|
||||
target_link_libraries(melonDS PRIVATE PkgConfig::SDL2 PkgConfig::LibArchive PkgConfig::Zstd ${LUA_LIBRARIES})
|
||||
target_link_libraries(melonDS PRIVATE ${QT_LINK_LIBS} ${CMAKE_DL_LIBS})
|
||||
|
||||
if (WIN32)
|
||||
|
|
|
@ -146,6 +146,8 @@ public:
|
|||
void setJoystick(int id);
|
||||
int getJoystickID() { return joystickID; }
|
||||
SDL_Joystick* getJoystick() { return joystick; }
|
||||
std::vector<int> heldKeys;
|
||||
std::vector<int> keyStrokes;
|
||||
|
||||
void touchScreen(int x, int y);
|
||||
void releaseScreen();
|
||||
|
@ -285,6 +287,8 @@ public:
|
|||
bool fastForwardToggled;
|
||||
bool slowmoToggled;
|
||||
bool doAudioSync;
|
||||
|
||||
melonDS::u32 getInputMask(){return inputMask;}
|
||||
private:
|
||||
|
||||
std::unique_ptr<melonDS::Savestate> backupState;
|
||||
|
|
|
@ -225,8 +225,10 @@ int getEventKeyVal(QKeyEvent* event)
|
|||
|
||||
void EmuInstance::onKeyPress(QKeyEvent* event)
|
||||
{
|
||||
heldKeys.push_back(event->key());
|
||||
int keyHK = getEventKeyVal(event);
|
||||
int keyKP = keyHK;
|
||||
keyStrokes.push_back(keyHK);
|
||||
if (event->modifiers() != Qt::KeypadModifier)
|
||||
keyKP &= ~event->modifiers();
|
||||
|
||||
|
@ -241,6 +243,7 @@ void EmuInstance::onKeyPress(QKeyEvent* event)
|
|||
|
||||
void EmuInstance::onKeyRelease(QKeyEvent* event)
|
||||
{
|
||||
heldKeys.erase(std::find(heldKeys.begin(),heldKeys.end(),event->key()));
|
||||
int keyHK = getEventKeyVal(event);
|
||||
int keyKP = keyHK;
|
||||
if (event->modifiers() != Qt::KeypadModifier)
|
||||
|
|
|
@ -460,6 +460,9 @@ void EmuThread::run()
|
|||
}
|
||||
|
||||
handleMessages();
|
||||
|
||||
//Lua Script Stuff (-for now happens at the end of each frame regardless of emuStatus)
|
||||
emit signalLuaUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -156,6 +156,8 @@ signals:
|
|||
|
||||
void syncVolumeLevel();
|
||||
|
||||
void signalLuaUpdate();
|
||||
|
||||
private:
|
||||
void handleMessages();
|
||||
|
||||
|
|
633
src/frontend/qt_sdl/LuaMain.cpp
Normal file
633
src/frontend/qt_sdl/LuaMain.cpp
Normal file
|
@ -0,0 +1,633 @@
|
|||
#include "LuaMain.h"
|
||||
#include <filesystem>
|
||||
#include <QDir>
|
||||
#include <QFileDialog>
|
||||
#include <QPushButton>
|
||||
#include <QGuiApplication>
|
||||
#include <QScrollBar>
|
||||
#include <QPainter>
|
||||
#include "types.h"
|
||||
#include "NDS.h"
|
||||
#include <SDL_joystick.h>
|
||||
#include <NDS_Header.h>
|
||||
#include "main.h"
|
||||
|
||||
LuaBundle::LuaBundle(LuaConsoleDialog* dialog, EmuInstance* inst)
|
||||
{
|
||||
emuInstance = inst;
|
||||
emuThread = emuInstance->getEmuThread();
|
||||
luaDialog = dialog;
|
||||
overlays = new std::vector<OverlayCanvas>;
|
||||
imageHash = new QHash<QString, QImage>;
|
||||
}
|
||||
|
||||
LuaConsoleDialog::LuaConsoleDialog(QWidget* parent) : QDialog(parent)
|
||||
{
|
||||
QWidget* w = parent;
|
||||
MainWindow* mainWindow;
|
||||
for (;;) //copied from ScreenPanel in Screen.cpp
|
||||
{
|
||||
mainWindow = qobject_cast<MainWindow*>(w);
|
||||
if (mainWindow) break;
|
||||
w = w->parentWidget();
|
||||
if (!w) break;
|
||||
}
|
||||
bundle = new LuaBundle(this,mainWindow->getEmuInstance());
|
||||
console = new LuaConsole(this);
|
||||
console->setGeometry(0,20,302,80);
|
||||
bar = console->verticalScrollBar();
|
||||
buttonPausePlay = new QPushButton("Pause/UnPause",this);
|
||||
buttonPausePlay->setGeometry(0,0,100,20);
|
||||
buttonStartStop = new QPushButton("Stop",this);
|
||||
buttonStartStop->setGeometry(101,0,100,20);
|
||||
buttonOpenScript = new QPushButton("OpenLuaFile",this);
|
||||
buttonOpenScript->setGeometry(202,0,100,20);
|
||||
connect(buttonOpenScript,&QPushButton::clicked,this,&LuaConsoleDialog::onOpenScript);
|
||||
connect(buttonStartStop,&QPushButton::clicked,this,&LuaConsoleDialog::onStop);
|
||||
connect(buttonPausePlay,&QPushButton::clicked,this,&LuaConsoleDialog::onPausePlay);
|
||||
this->setWindowTitle("Lua Script");
|
||||
}
|
||||
|
||||
void LuaConsoleDialog::closeEvent(QCloseEvent *event)
|
||||
{
|
||||
onStop();
|
||||
bundle->overlays->clear();
|
||||
flagClosed = true;
|
||||
event->accept();
|
||||
}
|
||||
|
||||
void LuaConsoleDialog::onOpenScript()
|
||||
{
|
||||
QFileInfo file = QFileInfo(QFileDialog::getOpenFileName(this, "Load Lua Script",QDir::currentPath()));
|
||||
if (!file.exists()) return;
|
||||
currentScript = file;
|
||||
bundle->flagNewLua = true;
|
||||
}
|
||||
|
||||
LuaConsole::LuaConsole(QWidget* parent)
|
||||
{
|
||||
this->setParent(parent);
|
||||
}
|
||||
|
||||
void LuaConsole::onGetText(const QString& string)
|
||||
{
|
||||
this->appendPlainText(string);
|
||||
QScrollBar* bar = verticalScrollBar();
|
||||
bar->setValue(bar->maximum());
|
||||
}
|
||||
|
||||
void LuaBundle::printText(QString string)
|
||||
{
|
||||
this->luaDialog->console->onGetText(string);
|
||||
}
|
||||
|
||||
void LuaConsoleDialog::onLuaSaveState(QString string)
|
||||
{
|
||||
emit signalLuaSaveState(string);
|
||||
}
|
||||
|
||||
void LuaConsoleDialog::onLuaLoadState(QString string)
|
||||
{
|
||||
emit signalLuaLoadState(string);
|
||||
}
|
||||
|
||||
void LuaConsole::onClear()
|
||||
{
|
||||
this->clear();
|
||||
}
|
||||
|
||||
LuaFunction::LuaFunction(luaFunctionPointer cf,const char* n,std::vector<LuaFunction*>* container)
|
||||
{
|
||||
this->cfunction = cf;
|
||||
this->name = n;
|
||||
container->push_back(this);
|
||||
}
|
||||
|
||||
static_assert(sizeof(LuaBundle*) <= LUA_EXTRASPACE,"LUA_EXTRASPACE too small");
|
||||
|
||||
LuaBundle* get_bundle(lua_State * L)
|
||||
{
|
||||
LuaBundle* pBundle;
|
||||
std::memcpy(&pBundle, lua_getextraspace(L), sizeof(LuaBundle*));
|
||||
return pBundle;
|
||||
}
|
||||
|
||||
#define MELON_LUA_HOOK_INSTRUCTION_COUNT 50 //number of vm instructions between hook calls
|
||||
void luaHookFunction(lua_State* L, lua_Debug *arg)
|
||||
{
|
||||
LuaBundle* bundle = get_bundle(L);
|
||||
if (bundle->flagStop and (arg->event == LUA_HOOKCOUNT))
|
||||
luaL_error(L, "Force Stopped");
|
||||
}
|
||||
|
||||
std::vector<LuaFunction*> definedLuaFunctions;//List of all defined lua functions
|
||||
|
||||
void LuaBundle::createLuaState()
|
||||
{
|
||||
if (!flagNewLua) return;
|
||||
overlays->clear();
|
||||
flagNewLua = false;
|
||||
luaState = nullptr;
|
||||
QByteArray fileName = luaDialog->currentScript.fileName().toLocal8Bit();
|
||||
QString filedir = luaDialog->currentScript.dir().path();
|
||||
lua_State* L = luaL_newstate();
|
||||
LuaBundle* pBundle = this;
|
||||
std::memcpy(lua_getextraspace(L), &pBundle, sizeof(LuaBundle*)); //Write a pointer to this LuaBundle into the extra space of the new lua_State
|
||||
luaL_openlibs(L);
|
||||
for (LuaFunction* function : definedLuaFunctions)
|
||||
lua_register(L,function->name,function->cfunction);
|
||||
QDir::setCurrent(filedir);
|
||||
lua_sethook(L,&luaHookFunction,LUA_MASKCOUNT,MELON_LUA_HOOK_INSTRUCTION_COUNT);
|
||||
if (luaL_dofile(L,fileName.data())==LUA_OK)
|
||||
{
|
||||
luaState = L;
|
||||
}
|
||||
else //Error loading script
|
||||
{
|
||||
printText(lua_tostring(L,-1));
|
||||
}
|
||||
}
|
||||
|
||||
void LuaConsoleDialog::onStop()
|
||||
{
|
||||
if (bundle->getLuaState())
|
||||
bundle->flagStop = true;
|
||||
}
|
||||
|
||||
void LuaConsoleDialog::onPausePlay()
|
||||
{
|
||||
bundle->flagPause = !bundle->flagPause;
|
||||
}
|
||||
|
||||
void LuaConsoleDialog::onLuaUpdate()
|
||||
{
|
||||
bundle->createLuaState();
|
||||
bundle->luaUpdate();
|
||||
}
|
||||
|
||||
//Gets Called once a frame
|
||||
void LuaBundle::luaUpdate()
|
||||
{
|
||||
if (!luaState || flagPause) return;
|
||||
if (lua_getglobal(luaState,"_Update")!=LUA_TFUNCTION)
|
||||
{
|
||||
printText("No \"_Update\" Function found, pausing script...");
|
||||
flagPause = true;
|
||||
return;
|
||||
}
|
||||
if (lua_pcall(luaState,0,0,0)!=0)
|
||||
{
|
||||
//Handel Errors
|
||||
printText(lua_tostring(luaState,-1));
|
||||
luaState = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
OverlayCanvas::OverlayCanvas(int x,int y,int width,int height,LuaCanvasTarget t)
|
||||
{
|
||||
target = t;
|
||||
buffer1 = new QImage(width,height,QImage::Format_ARGB32_Premultiplied);
|
||||
buffer2 = new QImage(width,height,QImage::Format_ARGB32_Premultiplied);
|
||||
buffer1->fill(0xffffff00); //initializes buffer with yellow pixels (probably should change this to transparent black pixels at some point...)
|
||||
buffer2->fill(0xffffff00);
|
||||
imageBuffer = buffer1;
|
||||
displayBuffer = buffer2;
|
||||
rectangle = QRect(x,y,width,height);
|
||||
flipped = false;
|
||||
GLTextureLoaded = false;
|
||||
}
|
||||
|
||||
void OverlayCanvas::flip()
|
||||
{
|
||||
if (imageBuffer == buffer1)
|
||||
{
|
||||
imageBuffer = buffer2;
|
||||
displayBuffer = buffer1;
|
||||
}
|
||||
else
|
||||
{
|
||||
imageBuffer = buffer1;
|
||||
displayBuffer = buffer2;
|
||||
}
|
||||
flipped = true;
|
||||
}
|
||||
|
||||
void LuaBundle::luaResetOSD()
|
||||
{
|
||||
for (auto lo = overlays->begin(); lo != overlays->end();)
|
||||
{
|
||||
OverlayCanvas& overlay = *lo;
|
||||
overlay.GLTextureLoaded = false;
|
||||
lo++;
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: Organize lua functions into different named tables similar to bizhawk.
|
||||
/*--------------------------------------------------------------------------------------------------
|
||||
Start of lua function definitions
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
|
||||
namespace luaDefinitions
|
||||
{
|
||||
|
||||
#define AddLuaFunction(functPointer,name)LuaFunction name(functPointer,#name,&definedLuaFunctions)
|
||||
|
||||
int lua_MelonPrint(lua_State* L)
|
||||
{
|
||||
LuaBundle* bundle = get_bundle(L);
|
||||
const char* string = luaL_checkstring(L,1);
|
||||
bundle->printText((QString)string);
|
||||
return 0;
|
||||
}
|
||||
AddLuaFunction(lua_MelonPrint,print);
|
||||
|
||||
int lua_MelonClear(lua_State* L)
|
||||
{
|
||||
LuaBundle* bundle = get_bundle(L);
|
||||
bundle->getluaDialog()->console->clear();
|
||||
return 0;
|
||||
}
|
||||
AddLuaFunction(lua_MelonClear,MelonClear);
|
||||
|
||||
enum ramInfo_ByteType
|
||||
{
|
||||
ramInfo_OneByte = 1,
|
||||
ramInfo_TwoBytes = 2,
|
||||
ramInfo_FourBytes = 4
|
||||
};
|
||||
|
||||
melonDS::u32 GetMainRAMValueU(const melonDS::u32& addr, const ramInfo_ByteType& byteType,LuaBundle* bundle)
|
||||
{
|
||||
|
||||
melonDS::NDS* nds = bundle->getEmuInstance()->getNDS();
|
||||
switch (byteType)
|
||||
{
|
||||
case ramInfo_OneByte:
|
||||
return *(melonDS::u8*)(nds->MainRAM + (addr&nds->MainRAMMask));
|
||||
case ramInfo_TwoBytes:
|
||||
return *(melonDS::u16*)(nds->MainRAM + (addr&nds->MainRAMMask));
|
||||
case ramInfo_FourBytes:
|
||||
return *(melonDS::u32*)(nds->MainRAM + (addr&nds->MainRAMMask));
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
melonDS::s32 GetMainRAMValueS(const melonDS::u32& addr, const ramInfo_ByteType& byteType,LuaBundle* bundle)
|
||||
{
|
||||
melonDS::NDS* nds = bundle->getEmuInstance()->getNDS();
|
||||
switch (byteType)
|
||||
{
|
||||
case ramInfo_OneByte:
|
||||
return *(melonDS::s8*)(nds->MainRAM + (addr&nds->MainRAMMask));
|
||||
case ramInfo_TwoBytes:
|
||||
return *(melonDS::s16*)(nds->MainRAM + (addr&nds->MainRAMMask));
|
||||
case ramInfo_FourBytes:
|
||||
return *(melonDS::s32*)(nds->MainRAM + (addr&nds->MainRAMMask));
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
int Lua_ReadDatau(lua_State* L,ramInfo_ByteType byteType)
|
||||
{
|
||||
LuaBundle* bundle = get_bundle(L);
|
||||
melonDS::u32 address = luaL_checkinteger(L,1);
|
||||
melonDS::u32 value = GetMainRAMValueU(address,byteType,bundle);
|
||||
lua_pushinteger(L, value);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int Lua_ReadDatas(lua_State* L,ramInfo_ByteType byteType)
|
||||
{
|
||||
LuaBundle* bundle = get_bundle(L);
|
||||
melonDS::u32 address = luaL_checkinteger(L,1);
|
||||
melonDS::s32 value = GetMainRAMValueS(address,byteType,bundle);
|
||||
lua_pushinteger(L, value);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int Lua_Readu8(lua_State* L)
|
||||
{
|
||||
return Lua_ReadDatau(L,ramInfo_OneByte);
|
||||
}
|
||||
AddLuaFunction(Lua_Readu8,Readu8);
|
||||
|
||||
int Lua_Readu16(lua_State* L)
|
||||
{
|
||||
return Lua_ReadDatau(L,ramInfo_TwoBytes);
|
||||
}
|
||||
AddLuaFunction(Lua_Readu16,Readu16);
|
||||
|
||||
int Lua_Readu32(lua_State* L)
|
||||
{
|
||||
return Lua_ReadDatau(L,ramInfo_FourBytes);
|
||||
}
|
||||
AddLuaFunction(Lua_Readu32,Readu32);
|
||||
|
||||
int Lua_Reads8(lua_State* L)
|
||||
{
|
||||
return Lua_ReadDatas(L,ramInfo_OneByte);
|
||||
}
|
||||
AddLuaFunction(Lua_Reads8,Reads8);
|
||||
|
||||
int Lua_Reads16(lua_State* L)
|
||||
{
|
||||
return Lua_ReadDatas(L,ramInfo_TwoBytes);
|
||||
}
|
||||
AddLuaFunction(Lua_Reads16,Reads16);
|
||||
|
||||
int Lua_Reads32(lua_State* L)
|
||||
{
|
||||
return Lua_ReadDatas(L,ramInfo_FourBytes);
|
||||
}
|
||||
AddLuaFunction(Lua_Reads32,Reads32);
|
||||
|
||||
int Lua_NDSTapDown(lua_State* L)
|
||||
{
|
||||
LuaBundle* bundle = get_bundle(L);
|
||||
melonDS::NDS* nds = bundle->getEmuInstance()->getNDS();
|
||||
int x = luaL_checkinteger(L,1);
|
||||
int y = luaL_checkinteger(L,2);
|
||||
nds->TouchScreen(x,y);
|
||||
return 0;
|
||||
}
|
||||
AddLuaFunction(Lua_NDSTapDown,NDSTapDown);
|
||||
|
||||
int Lua_NDSTapUp(lua_State* L)
|
||||
{
|
||||
LuaBundle* bundle = get_bundle(L);
|
||||
melonDS::NDS* nds = bundle->getEmuInstance()->getNDS();
|
||||
nds->ReleaseScreen();
|
||||
return 0;
|
||||
}
|
||||
AddLuaFunction(Lua_NDSTapUp,NDSTapUp);
|
||||
|
||||
int Lua_StateSave(lua_State* L)
|
||||
{
|
||||
LuaBundle* bundle = get_bundle(L);
|
||||
const char* filename = luaL_checkstring(L,1);
|
||||
bundle->getluaDialog()->onLuaSaveState((QString)filename);
|
||||
return 0;
|
||||
}
|
||||
AddLuaFunction(Lua_StateSave,StateSave);
|
||||
|
||||
int Lua_StateLoad(lua_State* L)
|
||||
{
|
||||
LuaBundle* bundle = get_bundle(L);
|
||||
const char* filename = luaL_checkstring(L,1);
|
||||
|
||||
bundle->getluaDialog()->onLuaLoadState((QString)filename);
|
||||
return 0;
|
||||
}
|
||||
AddLuaFunction(Lua_StateLoad,StateLoad);
|
||||
|
||||
int Lua_getMouse(lua_State* L)
|
||||
{
|
||||
Qt::MouseButtons btns = QGuiApplication::mouseButtons();
|
||||
LuaBundle* bundle = get_bundle(L);
|
||||
QPoint pos = bundle->getEmuInstance()->getMainWindow()->panel->mapFromGlobal(QCursor::pos(QGuiApplication::primaryScreen()));
|
||||
const char* keys[6] = {"Left","Middle","Right","XButton1","XButton2","Wheel"};
|
||||
bool vals[6] =
|
||||
{
|
||||
btns.testFlag(Qt::LeftButton),
|
||||
btns.testFlag(Qt::MiddleButton),
|
||||
btns.testFlag(Qt::RightButton),
|
||||
btns.testFlag(Qt::XButton1),
|
||||
btns.testFlag(Qt::XButton2),
|
||||
false //TODO: add mouse wheel support
|
||||
};
|
||||
lua_createtable(L, 0, 8);
|
||||
lua_pushinteger(L, pos.x());
|
||||
lua_setfield(L, -2, "X");
|
||||
lua_pushinteger(L, pos.y());
|
||||
lua_setfield(L, -2, "Y");
|
||||
for (int i=0;i<6;i++)
|
||||
{
|
||||
lua_pushboolean(L,vals[i]);
|
||||
lua_setfield(L,-2,keys[i]);
|
||||
}
|
||||
return 1;//returns table describing the current pos and state of the mouse
|
||||
}
|
||||
AddLuaFunction(Lua_getMouse,GetMouse);
|
||||
|
||||
int Lua_HeldKeys(lua_State* L)
|
||||
{
|
||||
LuaBundle* bundle = get_bundle(L);
|
||||
lua_createtable(L,0,bundle->getEmuInstance()->heldKeys.size());
|
||||
for (int key : bundle->getEmuInstance()->heldKeys)
|
||||
{
|
||||
lua_pushboolean(L,true);
|
||||
lua_seti(L,-2,key);
|
||||
}
|
||||
return 1;//returns table of currently held keys.
|
||||
}
|
||||
AddLuaFunction(Lua_HeldKeys,HeldKeys);
|
||||
|
||||
/*--------------------------------------------------------------------------------------------------
|
||||
Front-end lua function definitions
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
//TODO: Lua Colors
|
||||
|
||||
//MakeCanvas(int x,int y,int width,int height,[int target,topScreen=0,bottomScreen=1,OSD(default)>=2)],[bool active = true])
|
||||
int Lua_MakeCanvas(lua_State* L)
|
||||
{
|
||||
LuaBundle* bundle = get_bundle(L);
|
||||
int x = luaL_checknumber(L,1);
|
||||
int y = luaL_checknumber(L,2);
|
||||
int w = luaL_checknumber(L,3);
|
||||
int h = luaL_checknumber(L,4);
|
||||
int t = luaL_optinteger(L,5,2);
|
||||
bool a = 0 != luaL_optinteger(L,6,1);
|
||||
|
||||
OverlayCanvas canvas(x,y,w,h,(LuaCanvasTarget)t);
|
||||
canvas.isActive = a;
|
||||
lua_pushinteger(L,bundle->overlays->size());
|
||||
bundle->overlays->push_back(canvas);
|
||||
return 1; //returns index of the new overlay
|
||||
}
|
||||
AddLuaFunction(Lua_MakeCanvas,MakeCanvas);
|
||||
|
||||
int Lua_SetCanvas(lua_State* L) //SetCanvas(int index)
|
||||
{
|
||||
LuaBundle* bundle = get_bundle(L);
|
||||
int index = luaL_checknumber(L,1);
|
||||
|
||||
bundle->luaCanvas = &bundle->overlays->at(index);
|
||||
return 0;
|
||||
}
|
||||
AddLuaFunction(Lua_SetCanvas,SetCanvas);
|
||||
int Lua_ClearOverlay(lua_State* L)
|
||||
{
|
||||
LuaBundle* bundle = get_bundle(L);
|
||||
bundle->luaCanvas->imageBuffer->fill(0x00000000);
|
||||
return 0;
|
||||
}
|
||||
AddLuaFunction(Lua_ClearOverlay,ClearOverlay);
|
||||
|
||||
int Lua_Flip(lua_State* L)
|
||||
{
|
||||
LuaBundle* bundle = get_bundle(L);
|
||||
bundle->luaCanvas->flip();
|
||||
return 0;
|
||||
}
|
||||
AddLuaFunction(Lua_Flip,Flip);
|
||||
|
||||
//Text(int x, int y, string message, [u32 color = 'black'], [int fontsize = 9], [string fontfamily = Helvetica])
|
||||
int Lua_text(lua_State* L)
|
||||
{
|
||||
LuaBundle* bundle = get_bundle(L);
|
||||
int x = luaL_checknumber(L,1);
|
||||
int y = luaL_checknumber(L,2);
|
||||
const char* message = luaL_checklstring(L,3,NULL);
|
||||
melonDS::u32 color = luaL_optnumber(L,4,0x00000000);
|
||||
const char* FontFamily = luaL_optlstring(L,6,"Helvetica",NULL);
|
||||
int size = luaL_optnumber(L,5,9);
|
||||
|
||||
QPainter painter(bundle->luaCanvas->imageBuffer);
|
||||
QFont font(FontFamily,size,0,false);
|
||||
//font.setStyleStrategy(QFont::NoAntialias);
|
||||
//font.setLetterSpacing(QFont::AbsoluteSpacing,-1);
|
||||
painter.setFont(font);
|
||||
painter.setPen(color);
|
||||
painter.drawText(x,y,message);
|
||||
return 0;
|
||||
}
|
||||
AddLuaFunction(Lua_text,Text);
|
||||
|
||||
int Lua_line(lua_State* L)
|
||||
{
|
||||
LuaBundle* bundle = get_bundle(L);
|
||||
int x1 = luaL_checknumber(L,1);
|
||||
int y1 = luaL_checknumber(L,2);
|
||||
int x2 = luaL_checknumber(L,3);
|
||||
int y2 = luaL_checknumber(L,4);
|
||||
melonDS::u32 color = luaL_checknumber(L,5);
|
||||
|
||||
QPainter painter(bundle->luaCanvas->imageBuffer);
|
||||
painter.setPen(color);
|
||||
painter.drawLine(x1,y1,x2,y2);
|
||||
return 0;
|
||||
}
|
||||
AddLuaFunction(Lua_line,Line);
|
||||
|
||||
int Lua_rect(lua_State* L)
|
||||
{
|
||||
LuaBundle* bundle = get_bundle(L);
|
||||
melonDS::u32 color = luaL_checknumber(L,5);
|
||||
int x = luaL_checknumber(L,1);
|
||||
int y = luaL_checknumber(L,2);
|
||||
int width = luaL_checknumber(L,3);
|
||||
int height = luaL_checknumber(L,4);
|
||||
|
||||
QPainter painter(bundle->luaCanvas->imageBuffer);
|
||||
painter.setPen(color);
|
||||
painter.drawRect(x,y,width,height);
|
||||
return 0;
|
||||
}
|
||||
AddLuaFunction(Lua_rect,Rect);
|
||||
|
||||
int Lua_fillrect(lua_State* L)
|
||||
{
|
||||
LuaBundle* bundle = get_bundle(L);
|
||||
melonDS::u32 color = luaL_checknumber(L,5);
|
||||
int x = luaL_checknumber(L,1);
|
||||
int y = luaL_checknumber(L,2);
|
||||
int width = luaL_checknumber(L,3);
|
||||
int height = luaL_checknumber(L,4);
|
||||
|
||||
QPainter painter(bundle->luaCanvas->imageBuffer);
|
||||
painter.setPen(color);
|
||||
painter.fillRect(x,y,width,height,color);
|
||||
return 0;
|
||||
}
|
||||
AddLuaFunction(Lua_fillrect,FillRect);
|
||||
|
||||
int Lua_Ellipse(lua_State* L)
|
||||
{
|
||||
LuaBundle* bundle = get_bundle(L);
|
||||
melonDS::u32 color = luaL_checknumber(L,5);
|
||||
int x = luaL_checknumber(L,1);
|
||||
int y = luaL_checknumber(L,2);
|
||||
int width = luaL_checknumber(L,3);
|
||||
int height = luaL_checknumber(L,4);
|
||||
|
||||
QPainter painter(bundle->luaCanvas->imageBuffer);
|
||||
painter.setPen(color);
|
||||
painter.drawEllipse(x,y,width,height);
|
||||
return 0;
|
||||
}
|
||||
AddLuaFunction(Lua_Ellipse,Ellipse);
|
||||
|
||||
int Lua_keystrokes(lua_State* L)
|
||||
{
|
||||
LuaBundle* bundle = get_bundle(L);
|
||||
lua_createtable(L,0,bundle->getEmuInstance()->keyStrokes.size());
|
||||
for (int i = 0; i<bundle->getEmuInstance()->keyStrokes.size(); i++)
|
||||
{
|
||||
lua_pushinteger(L,bundle->getEmuInstance()->keyStrokes.at(i));
|
||||
lua_seti(L,-2,i);
|
||||
}
|
||||
bundle->getEmuInstance()->keyStrokes.clear();
|
||||
return 1;
|
||||
}
|
||||
AddLuaFunction(Lua_keystrokes,Keys);
|
||||
|
||||
//DrawImage(string path, int x, int y,[int source x=0], [int source y=0], [int source w=-1],[int source h=-1])
|
||||
int Lua_drawImage(lua_State* L)
|
||||
{
|
||||
LuaBundle* bundle = get_bundle(L);
|
||||
const char* path = luaL_checklstring(L,1,NULL);
|
||||
int x = luaL_checkinteger(L,2);
|
||||
int y = luaL_checkinteger(L,3);
|
||||
int sx = luaL_optinteger(L,4,0);
|
||||
int sy = luaL_optinteger(L,5,0);
|
||||
int sw = luaL_optinteger(L,6,-1);
|
||||
int sh = luaL_optinteger(L,7,-1);
|
||||
|
||||
QPainter painter(bundle->luaCanvas->imageBuffer);
|
||||
QImage image;
|
||||
if (bundle->imageHash->contains(path))
|
||||
{
|
||||
image=(*bundle->imageHash)[path];
|
||||
}
|
||||
else
|
||||
{
|
||||
image=QImage((QString)path);
|
||||
(*bundle->imageHash)[path] = image;
|
||||
}
|
||||
painter.drawImage(x,y,image,sx,sy,sw,sh);
|
||||
return 0;
|
||||
}
|
||||
AddLuaFunction(Lua_drawImage,DrawImage);
|
||||
|
||||
int Lua_clearImageHash(lua_State* L)
|
||||
{
|
||||
LuaBundle* bundle = get_bundle(L);
|
||||
bundle->imageHash->clear();
|
||||
return 0;
|
||||
}
|
||||
AddLuaFunction(Lua_clearImageHash,ClearHash);
|
||||
|
||||
int Lua_getJoy(lua_State* L)
|
||||
{
|
||||
LuaBundle* bundle = get_bundle(L);
|
||||
melonDS::u32 buttonMask=bundle->getEmuInstance()->getInputMask();//current button state.
|
||||
const char* keys[12] =
|
||||
{//Buttons in order of mask.
|
||||
"A","B","Select","Start",
|
||||
"Right","Left","Up","Down",
|
||||
"R","L","X","Y"
|
||||
};
|
||||
lua_createtable(L, 0, 12);
|
||||
for (melonDS::u32 i=0;i<12;i++)
|
||||
{
|
||||
lua_pushboolean(L,0 >= (buttonMask&(1<<i)));
|
||||
lua_setfield(L,-2,keys[i]);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
AddLuaFunction(Lua_getJoy,GetJoy);
|
||||
|
||||
}
|
116
src/frontend/qt_sdl/LuaMain.h
Normal file
116
src/frontend/qt_sdl/LuaMain.h
Normal file
|
@ -0,0 +1,116 @@
|
|||
#ifndef LUAMAIN_H
|
||||
#define LUAMAIN_H
|
||||
#include <QDialog>
|
||||
#include <QPlainTextEdit>
|
||||
#include <QFileInfo>
|
||||
#include <lua.hpp>
|
||||
#include "EmuInstance.h"
|
||||
|
||||
class LuaConsole: public QPlainTextEdit
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
LuaConsole(QWidget* parent=nullptr);
|
||||
public slots:
|
||||
void onGetText(const QString& string);
|
||||
void onClear();
|
||||
};
|
||||
|
||||
class LuaBundle;
|
||||
|
||||
class LuaConsoleDialog: public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
LuaConsoleDialog(QWidget* parent);
|
||||
LuaBundle* getLuaBundle(){return bundle;};
|
||||
LuaConsole* console;
|
||||
QFileInfo currentScript;
|
||||
QPushButton* buttonOpenScript;
|
||||
QPushButton* buttonStartStop;
|
||||
QPushButton* buttonPausePlay;
|
||||
QScrollBar* bar;
|
||||
bool flagClosed;
|
||||
void onLuaSaveState(QString);
|
||||
void onLuaLoadState(QString);
|
||||
protected:
|
||||
void closeEvent(QCloseEvent *event) override;
|
||||
LuaBundle* bundle;
|
||||
signals:
|
||||
void signalNewLua();
|
||||
void signalClosing();
|
||||
void signalLuaSaveState(const QString&);
|
||||
void signalLuaLoadState(const QString&);
|
||||
public slots:
|
||||
//void onStartStop();
|
||||
void onOpenScript();
|
||||
void onStop();
|
||||
void onPausePlay();
|
||||
void onLuaUpdate();
|
||||
};
|
||||
|
||||
//Based on ScreenLayout::GetScreenTransforms
|
||||
enum LuaCanvasTarget
|
||||
{
|
||||
canvasTarget_TopScreen = 0,
|
||||
canvasTarget_BottomScreen = 1,
|
||||
canvasTarget_OSD = 2 //Used for drawing to OSD / non-screen target
|
||||
};
|
||||
|
||||
struct OverlayCanvas
|
||||
{
|
||||
QImage* imageBuffer; // buffer edited by luascript
|
||||
QImage* displayBuffer; //buffer displayed on screen
|
||||
QImage* buffer1;
|
||||
QImage* buffer2;
|
||||
QRect rectangle;
|
||||
LuaCanvasTarget target = canvasTarget_OSD;
|
||||
bool isActive = true; // only active overlays are drawn
|
||||
unsigned int GLTexture; // used by GL rendering
|
||||
bool GLTextureLoaded;
|
||||
OverlayCanvas(int x, int y,int w, int h, LuaCanvasTarget target = canvasTarget_OSD);
|
||||
void flip();//used to swap buffers / update canvas
|
||||
bool flipped; //used to signal update to graphics.
|
||||
};
|
||||
|
||||
typedef int(*luaFunctionPointer)(lua_State*);
|
||||
struct LuaFunction
|
||||
{
|
||||
luaFunctionPointer cfunction;
|
||||
const char* name;
|
||||
LuaFunction(luaFunctionPointer,const char*,std::vector<LuaFunction*>*);
|
||||
};
|
||||
|
||||
class LuaBundle
|
||||
{
|
||||
EmuInstance* emuInstance;
|
||||
EmuThread* emuThread;
|
||||
LuaConsoleDialog* luaDialog;
|
||||
lua_State* luaState = nullptr;
|
||||
int basketID;
|
||||
|
||||
void luaResetOSD();
|
||||
void luaPrint(QString string);
|
||||
void luaClearConsole();
|
||||
|
||||
OverlayCanvas* currentCanvas = nullptr;
|
||||
friend class LuaConsolDialog;
|
||||
public:
|
||||
LuaBundle(LuaConsoleDialog* dialog,EmuInstance* inst);
|
||||
lua_State* getLuaState(){return luaState;};
|
||||
EmuThread* getEmuThread(){return emuThread;};
|
||||
EmuInstance* getEmuInstance(){return emuInstance;};
|
||||
LuaConsoleDialog* getluaDialog(){return luaDialog;};
|
||||
void printText(QString string);
|
||||
void createLuaState();
|
||||
void luaUpdate();
|
||||
bool flagPause = false;
|
||||
bool flagStop = false;
|
||||
bool flagNewLua = false;
|
||||
OverlayCanvas* luaCanvas = nullptr;
|
||||
std::vector<OverlayCanvas>* overlays;
|
||||
QHash<QString, QImage>* imageHash;
|
||||
|
||||
};
|
||||
|
||||
#endif
|
|
@ -64,4 +64,47 @@ void main()
|
|||
}
|
||||
)";
|
||||
|
||||
#endif // OSD_SHADERS_H
|
||||
//Fragment Shader for overlay copied from melonPrimeDS project.
|
||||
const char* kScreenFS_overlay = R"(#version 140
|
||||
|
||||
uniform sampler2D OverlayTex;
|
||||
|
||||
smooth in vec2 fTexcoord;
|
||||
|
||||
uniform vec2 uOverlayPos;
|
||||
uniform vec2 uOverlaySize;
|
||||
uniform int uOverlayScreenType;
|
||||
|
||||
out vec4 oColor;
|
||||
|
||||
void main()
|
||||
{
|
||||
const vec2 dsSize = vec2(256.0, 193.0); // +1 on y for pixel gap
|
||||
|
||||
vec2 uv = fTexcoord * vec2(1.0, 2.0);
|
||||
|
||||
if (uOverlayScreenType < 1)
|
||||
{
|
||||
// top screen
|
||||
uv -= uOverlayPos / dsSize;
|
||||
uv *= dsSize / uOverlaySize;
|
||||
} else {
|
||||
// bottom screen
|
||||
uv -= vec2(0.0, 1.0);
|
||||
uv -= (uOverlayPos + vec2(0.0, 1.0)) / dsSize;
|
||||
uv *= dsSize / uOverlaySize;
|
||||
}
|
||||
|
||||
vec4 pixel = texture(OverlayTex, uv);
|
||||
pixel.rgb *= pixel.a;
|
||||
|
||||
if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0)
|
||||
{
|
||||
oColor = vec4(0.0, 0.0, 0.0, 0.0);
|
||||
} else {
|
||||
oColor = pixel.bgra;
|
||||
}
|
||||
}
|
||||
)";
|
||||
|
||||
#endif // OSD_SHADERS_H
|
|
@ -46,8 +46,11 @@
|
|||
#include "main_shaders.h"
|
||||
#include "OSD_shaders.h"
|
||||
#include "font.h"
|
||||
|
||||
#include "version.h"
|
||||
|
||||
#include "LuaMain.h"
|
||||
|
||||
using namespace melonDS;
|
||||
|
||||
|
||||
|
@ -772,13 +775,29 @@ void ScreenPanelNative::setupScreenLayout()
|
|||
}
|
||||
}
|
||||
|
||||
// From ScreenLayout::GetScreenTransforms
|
||||
// TopScreen = 0
|
||||
// BottomScreen = 1
|
||||
// OSD / non-screen target = 2 (used by Lua stuff)
|
||||
void ScreenPanelNative::drawOverlays(QPainter* painter,int type)
|
||||
{
|
||||
LuaConsoleDialog* dialog = mainWindow->getLuaDialog();
|
||||
if (!dialog)
|
||||
return;
|
||||
LuaBundle* lua = dialog->getLuaBundle();
|
||||
|
||||
for (OverlayCanvas& overlay : *lua->overlays)
|
||||
if ((overlay.target == type) && overlay.isActive)
|
||||
painter->drawImage(overlay.rectangle,*overlay.displayBuffer);
|
||||
}
|
||||
|
||||
void ScreenPanelNative::paintEvent(QPaintEvent* event)
|
||||
{
|
||||
QPainter painter(this);
|
||||
|
||||
// fill background
|
||||
painter.fillRect(event->rect(), QColor::fromRgb(0, 0, 0));
|
||||
|
||||
painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
|
||||
auto emuThread = emuInstance->getEmuThread();
|
||||
|
||||
if (emuThread->emuIsActive())
|
||||
|
@ -799,12 +818,15 @@ void ScreenPanelNative::paintEvent(QPaintEvent* event)
|
|||
memcpy(screen[1].scanLine(0), nds->GPU.Framebuffer[frontbuf][1].get(), 256 * 192 * 4);
|
||||
emuThread->frontBufferLock.unlock();
|
||||
|
||||
painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
|
||||
QRect screenrc(0, 0, 256, 192);
|
||||
|
||||
for (int i = 0; i < numScreens; i++)
|
||||
{
|
||||
painter.setTransform(screenTrans[i]);
|
||||
painter.drawImage(screenrc, screen[screenKind[i]]);
|
||||
if (osdEnabled)
|
||||
drawOverlays(&painter,screenKind[i]);
|
||||
}
|
||||
emuInstance->renderLock.unlock();
|
||||
}
|
||||
|
@ -832,6 +854,8 @@ void ScreenPanelNative::paintEvent(QPaintEvent* event)
|
|||
|
||||
painter.resetTransform();
|
||||
|
||||
drawOverlays(&painter,canvasTarget_OSD);
|
||||
|
||||
for (auto it = osdItems.begin(); it != osdItems.end(); )
|
||||
{
|
||||
OSDItem& item = *it;
|
||||
|
@ -1012,7 +1036,23 @@ void ScreenPanelGL::initOpenGL()
|
|||
logoTexture = tex;
|
||||
|
||||
transferLayout();
|
||||
|
||||
OpenGL::CompileVertexFragmentProgram(overlayShader,
|
||||
kScreenVS,kScreenFS_overlay,
|
||||
"OverlayShader",
|
||||
{{"vPosition", 0}, {"vTexcoord", 1}},
|
||||
{{"oColor", 0}});
|
||||
|
||||
glUseProgram(overlayShader);
|
||||
|
||||
overlayScreenSizeULoc = glGetUniformLocation(overlayShader, "uScreenSize");
|
||||
overlayTransformULoc = glGetUniformLocation(overlayShader, "uTransform");
|
||||
overlayPosULoc = glGetUniformLocation(overlayShader, "uOverlayPos");
|
||||
overlaySizeULoc = glGetUniformLocation(overlayShader, "uOverlaySize");
|
||||
overlayScreenTypeULoc = glGetUniformLocation(overlayShader, "uOverlayScreenType");
|
||||
|
||||
glInited = true;
|
||||
|
||||
}
|
||||
|
||||
void ScreenPanelGL::deinitOpenGL()
|
||||
|
@ -1041,7 +1081,20 @@ void ScreenPanelGL::deinitOpenGL()
|
|||
|
||||
glDeleteProgram(osdShader);
|
||||
|
||||
|
||||
glDeleteProgram(overlayShader);
|
||||
|
||||
if (mainWindow->getLuaDialog())
|
||||
{
|
||||
std::vector<OverlayCanvas>* overlays = mainWindow->getLuaDialog()->getLuaBundle()->overlays;
|
||||
for (OverlayCanvas& overlay : *overlays)
|
||||
{
|
||||
if (overlay.GLTextureLoaded)
|
||||
{
|
||||
glDeleteTextures(1,&overlay.GLTexture);
|
||||
overlay.GLTextureLoaded=false;
|
||||
}
|
||||
}
|
||||
}
|
||||
glContext->DoneCurrent();
|
||||
|
||||
lastScreenWidth = lastScreenHeight = -1;
|
||||
|
@ -1083,6 +1136,50 @@ void ScreenPanelGL::osdDeleteItem(OSDItem* item)
|
|||
ScreenPanel::osdDeleteItem(item);
|
||||
}
|
||||
|
||||
void ScreenPanelGL::drawOverlays(int screenType,int screen)
|
||||
{
|
||||
if (!mainWindow->getLuaDialog())
|
||||
return;
|
||||
|
||||
std::vector<OverlayCanvas>* overlays = mainWindow->getLuaDialog()->getLuaBundle()->overlays;
|
||||
for (OverlayCanvas& overlay : *overlays)
|
||||
{
|
||||
if (!overlay.isActive || overlay.target != screenType)
|
||||
continue;
|
||||
if (!overlay.GLTextureLoaded)//Load texture if none loaded
|
||||
{
|
||||
glGenTextures(1,&overlay.GLTexture);
|
||||
glBindTexture(GL_TEXTURE_2D, overlay.GLTexture);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, overlay.rectangle.width(), overlay.rectangle.height(), 0, GL_RGBA, GL_UNSIGNED_BYTE, overlay.displayBuffer->bits());
|
||||
overlay.GLTextureLoaded = true;
|
||||
}
|
||||
if (overlay.flipped) //Only update texture if needed
|
||||
{
|
||||
glBindTexture(GL_TEXTURE_2D, overlay.GLTexture);
|
||||
glTexSubImage2D(GL_TEXTURE_2D,0,0,0,overlay.rectangle.width(),overlay.rectangle.height(),GL_RGBA,GL_UNSIGNED_BYTE,overlay.displayBuffer->bits());
|
||||
overlay.flipped = false;
|
||||
}
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, overlay.GLTexture);
|
||||
glUniform2f(overlayPosULoc,overlay.rectangle.left(),overlay.rectangle.top());
|
||||
glUniform2f(overlaySizeULoc,overlay.rectangle.width(),overlay.rectangle.height());
|
||||
|
||||
if(screenType == canvasTarget_OSD) // OSD gets drawn differently then top or bottom screen target
|
||||
{
|
||||
glDrawArrays(GL_TRIANGLES, 0, 2*3);
|
||||
continue;
|
||||
}
|
||||
|
||||
glUniform1i(overlayScreenTypeULoc, screenType);
|
||||
glUniformMatrix2x3fv(overlayTransformULoc, 1, GL_TRUE,screenMatrix[screen]);
|
||||
glDrawArrays(GL_TRIANGLES,screenType == 0 ? 0 : 2*3, 2*3);
|
||||
}
|
||||
}
|
||||
|
||||
void ScreenPanelGL::drawScreenGL()
|
||||
{
|
||||
if (!glContext) return;
|
||||
|
@ -1202,6 +1299,29 @@ void ScreenPanelGL::drawScreenGL()
|
|||
|
||||
if (osdEnabled)
|
||||
{
|
||||
|
||||
glUseProgram(overlayShader);
|
||||
|
||||
//Need to blend this layer onto the screen layer!
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
glUniform2f(overlayScreenSizeULoc, w / factor, h / factor);
|
||||
|
||||
screenSettingsLock.lock();
|
||||
|
||||
glBindBuffer(GL_ARRAY_BUFFER, screenVertexBuffer);
|
||||
glBindVertexArray(screenVertexArray);
|
||||
|
||||
for(int i = 0;i<numScreens;i++){
|
||||
drawOverlays(screenKind[i],i);
|
||||
}
|
||||
|
||||
|
||||
screenSettingsLock.unlock();
|
||||
|
||||
|
||||
|
||||
osdMutex.lock();
|
||||
|
||||
u32 y = kOSDMargin;
|
||||
|
@ -1220,6 +1340,8 @@ void ScreenPanelGL::drawScreenGL()
|
|||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
drawOverlays(canvasTarget_OSD,0);
|
||||
|
||||
for (auto it = osdItems.begin(); it != osdItems.end(); )
|
||||
{
|
||||
OSDItem& item = *it;
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
#include <QScreen>
|
||||
#include <QCloseEvent>
|
||||
#include <QTimer>
|
||||
#include <QPainter>
|
||||
|
||||
#include "glad/glad.h"
|
||||
#include "ScreenLayout.h"
|
||||
|
@ -166,6 +167,7 @@ protected:
|
|||
|
||||
private:
|
||||
void setupScreenLayout() override;
|
||||
void drawOverlays(QPainter* painter,int type);
|
||||
|
||||
QImage screen[2];
|
||||
QTransform screenTrans[kMaxScreenTransforms];
|
||||
|
@ -204,6 +206,7 @@ protected:
|
|||
|
||||
private:
|
||||
void setupScreenLayout() override;
|
||||
void drawOverlays(int type,int screen);
|
||||
|
||||
std::unique_ptr<GL::Context> glContext;
|
||||
bool glInited;
|
||||
|
@ -230,6 +233,10 @@ private:
|
|||
|
||||
void osdRenderItem(OSDItem* item) override;
|
||||
void osdDeleteItem(OSDItem* item) override;
|
||||
|
||||
GLuint overlayShader;
|
||||
GLuint overlayScreenSizeULoc, overlayTransformULoc;
|
||||
GLuint overlayPosULoc, overlaySizeULoc, overlayScreenTypeULoc;
|
||||
};
|
||||
|
||||
#endif // SCREEN_H
|
||||
|
|
|
@ -84,6 +84,8 @@
|
|||
#include "Window.h"
|
||||
#include "AboutDialog.h"
|
||||
|
||||
#include "LuaMain.h"
|
||||
|
||||
using namespace melonDS;
|
||||
|
||||
|
||||
|
@ -415,6 +417,11 @@ MainWindow::MainWindow(int id, EmuInstance* inst, QWidget* parent) :
|
|||
actDateTime = menu->addAction("Date and time");
|
||||
connect(actDateTime, &QAction::triggered, this, &MainWindow::onOpenDateTime);
|
||||
|
||||
menu->addSeparator();
|
||||
|
||||
actLuaScript = menu->addAction("Lua Script");
|
||||
connect(actLuaScript,&QAction::triggered,this,&MainWindow::onOpenLuaScript);
|
||||
|
||||
menu->addSeparator();
|
||||
|
||||
actEnableCheats = menu->addAction("Enable cheats");
|
||||
|
@ -1524,6 +1531,13 @@ void MainWindow::onEjectGBACart()
|
|||
updateCartInserted(true);
|
||||
}
|
||||
|
||||
void MainWindow::onLuaSaveState(const QString& filename)
|
||||
{
|
||||
emuThread->emuPause();
|
||||
emuInstance->saveState(filename.toStdString());
|
||||
emuThread->emuUnpause();
|
||||
}
|
||||
|
||||
void MainWindow::onSaveState()
|
||||
{
|
||||
int slot = ((QAction*)sender())->data().toInt();
|
||||
|
@ -1559,6 +1573,13 @@ void MainWindow::onSaveState()
|
|||
}
|
||||
}
|
||||
|
||||
void MainWindow::onLuaLoadState(const QString& filename)
|
||||
{
|
||||
emuThread->emuPause();
|
||||
emuInstance->loadState(filename.toStdString());
|
||||
emuThread->emuUnpause();
|
||||
}
|
||||
|
||||
void MainWindow::onLoadState()
|
||||
{
|
||||
int slot = ((QAction*)sender())->data().toInt();
|
||||
|
@ -1700,6 +1721,22 @@ void MainWindow::onOpenPowerManagement()
|
|||
PowerManagementDialog* dlg = PowerManagementDialog::openDlg(this);
|
||||
}
|
||||
|
||||
void MainWindow::onOpenLuaScript()
|
||||
{
|
||||
if (luaDialog && luaDialog->flagClosed)
|
||||
{
|
||||
delete luaDialog;
|
||||
luaDialog = nullptr;
|
||||
}
|
||||
if (luaDialog)
|
||||
return;
|
||||
luaDialog = new LuaConsoleDialog(this);
|
||||
luaDialog->show();
|
||||
connect(emuThread,&EmuThread::signalLuaUpdate,luaDialog,&LuaConsoleDialog::onLuaUpdate);
|
||||
connect(luaDialog,&LuaConsoleDialog::signalLuaSaveState,this,&MainWindow::onLuaSaveState);
|
||||
connect(luaDialog,&LuaConsoleDialog::signalLuaLoadState,this,&MainWindow::onLuaLoadState);
|
||||
}
|
||||
|
||||
void MainWindow::onEnableCheats(bool checked)
|
||||
{
|
||||
localCfg.SetBool("EnableCheats", checked);
|
||||
|
|
|
@ -40,6 +40,7 @@
|
|||
|
||||
class EmuInstance;
|
||||
class EmuThread;
|
||||
class LuaConsoleDialog;
|
||||
|
||||
const int kMaxRecentROMs = 10;
|
||||
|
||||
|
@ -110,6 +111,9 @@ public:
|
|||
|
||||
EmuInstance* getEmuInstance() { return emuInstance; }
|
||||
Config::Table& getWindowConfig() { return windowCfg; }
|
||||
|
||||
LuaConsoleDialog* getLuaDialog() {return luaDialog;}
|
||||
|
||||
int getWindowID() { return windowID; }
|
||||
|
||||
bool winHasMenu() { return hasMenu; }
|
||||
|
@ -166,7 +170,9 @@ private slots:
|
|||
void onInsertGBACart();
|
||||
void onInsertGBAAddon();
|
||||
void onEjectGBACart();
|
||||
void onLuaSaveState(const QString& filename);
|
||||
void onSaveState();
|
||||
void onLuaLoadState(const QString& filename);
|
||||
void onLoadState();
|
||||
void onUndoStateLoad();
|
||||
void onImportSavefile();
|
||||
|
@ -177,6 +183,7 @@ private slots:
|
|||
void onStop();
|
||||
void onFrameStep();
|
||||
void onOpenPowerManagement();
|
||||
void onOpenLuaScript();
|
||||
void onOpenDateTime();
|
||||
void onEnableCheats(bool checked);
|
||||
void onSetupCheats();
|
||||
|
@ -272,6 +279,7 @@ private:
|
|||
|
||||
EmuInstance* emuInstance;
|
||||
EmuThread* emuThread;
|
||||
LuaConsoleDialog* luaDialog=nullptr;
|
||||
|
||||
Config::Table& globalCfg;
|
||||
Config::Table& localCfg;
|
||||
|
@ -320,6 +328,7 @@ public:
|
|||
#ifdef __APPLE__
|
||||
QAction* actPreferences;
|
||||
#endif
|
||||
QAction* actLuaScript;
|
||||
QAction* actInputConfig;
|
||||
QAction* actVideoSettings;
|
||||
QAction* actCameraSettings;
|
||||
|
|
BIN
tools/LuaScripts/Lua-Logo_128x128.png
Normal file
BIN
tools/LuaScripts/Lua-Logo_128x128.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.1 KiB |
160
tools/LuaScripts/LuaScriptTest.lua
Normal file
160
tools/LuaScripts/LuaScriptTest.lua
Normal file
|
@ -0,0 +1,160 @@
|
|||
-- Simple Script to test most of the different Lua Functions for MelonDS
|
||||
-- Written by NPO197
|
||||
|
||||
MelonClear()
|
||||
print("This text Should be cleared")
|
||||
MelonClear()
|
||||
|
||||
print("Running Test...")
|
||||
|
||||
u32Data = Readu32(0x00000000)
|
||||
print(string.format("DataZero: %x",u32Data))
|
||||
|
||||
canvas = MakeCanvas(0,0,500,500)
|
||||
SetCanvas(canvas)
|
||||
ClearOverlay()
|
||||
|
||||
FillRect(0,0,55,11,0xffffffff)
|
||||
Text(0,9,"Test Message")
|
||||
Line(0,10,55,10,0x00ff00ff)
|
||||
Rect(0,0,55,11,0xff00ff00)
|
||||
|
||||
DrawImage("Lua-Logo_128x128.png",0,60)
|
||||
|
||||
Text(0,200,"WASD to move \"lua Stylus\", Q to tap screen.",0xffffff)
|
||||
Text(0,210,"T to create savestate, R to load savestate.",0xffffff)
|
||||
|
||||
Flip()
|
||||
|
||||
|
||||
-------- Main Loop ----------
|
||||
|
||||
updateFlags = {}
|
||||
Stylus = {x=0,y=0,}
|
||||
textCanvas = MakeCanvas(0,12,500,100)
|
||||
vstylusCanvas = MakeCanvas(0,0,256,192,1) -- bottom screen
|
||||
|
||||
--Gets Called once every frame
|
||||
function _Update()
|
||||
updateFlags = {} -- Reset updateFlags at start of each frame
|
||||
|
||||
--Text Functions
|
||||
SetCanvas(textCanvas)
|
||||
ClearOverlay()
|
||||
local y = 0
|
||||
for _,tfunct in ipairs({
|
||||
MousePosText,
|
||||
MouseButtonText,
|
||||
KeysText,
|
||||
JoyText
|
||||
}) do
|
||||
y = y+10
|
||||
Text(0,y,tfunct(),0xffffff)
|
||||
end
|
||||
Flip()
|
||||
|
||||
--SaveState stuff
|
||||
if KeyPress("T") then
|
||||
StateSave("SaveState_Auto.mln")
|
||||
elseif KeyPress("R") then
|
||||
StateLoad("SaveState_Auto.mln")
|
||||
end
|
||||
|
||||
--StylusLoop
|
||||
for key,dir in pairs({
|
||||
--Key = {dx,dy}
|
||||
["W"] = { 0,-1},
|
||||
["A"] = {-1, 0},
|
||||
["S"] = { 0, 1},
|
||||
["D"] = { 1, 0}
|
||||
}) do
|
||||
if KeyHeld(key) then
|
||||
Stylus.x=Stylus.x+dir[1]
|
||||
Stylus.y=Stylus.y+dir[2]
|
||||
end
|
||||
end
|
||||
|
||||
if KeyHeld("Q") then
|
||||
NDSTapDown(Stylus.x,Stylus.y)
|
||||
else
|
||||
NDSTapUp()
|
||||
end
|
||||
|
||||
DrawStylus()
|
||||
end
|
||||
|
||||
--Text Functions
|
||||
|
||||
function MousePosText()
|
||||
mouse = GetMouse()
|
||||
return "MousePos:"..mouse.X..","..mouse.Y
|
||||
end
|
||||
|
||||
function MouseButtonText()
|
||||
str = ""
|
||||
for k,v in pairs(GetMouse()) do
|
||||
if k~="X" and k~="Y" and v then
|
||||
str = str..k
|
||||
end
|
||||
end
|
||||
return "MouseBtn:"..str
|
||||
end
|
||||
|
||||
typed = ""
|
||||
keys = {}
|
||||
function KeysText()
|
||||
keys = Keys()
|
||||
str = ""
|
||||
for _,i in pairs(keys) do
|
||||
if pcall(string.char,i) then
|
||||
str = str..string.char(i)
|
||||
else
|
||||
print("NonAscii:"..i)
|
||||
typed = ""
|
||||
end
|
||||
end
|
||||
typed = typed..str
|
||||
return "Keys:"..typed
|
||||
end
|
||||
|
||||
joys = {}
|
||||
function JoyText()
|
||||
joys = GetJoy()
|
||||
str = ""
|
||||
for k,v in pairs(joys) do
|
||||
if v then
|
||||
str = str..k
|
||||
end
|
||||
end
|
||||
return "Joy:"..str
|
||||
end
|
||||
|
||||
function DrawStylus()
|
||||
SetCanvas(vstylusCanvas)
|
||||
ClearOverlay()
|
||||
Ellipse(Stylus.x-5,Stylus.y-5,10,10,0xffffffff)
|
||||
Ellipse(Stylus.x-2,Stylus.y-2,4,4,0x00000000)
|
||||
Flip()
|
||||
end
|
||||
|
||||
function KeyHeld(keyStr)
|
||||
-- Only need to update mask once per frame
|
||||
if updateFlags.KeyboardCheck == nil then
|
||||
mask = HeldKeys()
|
||||
updateFlags.KeyboardCheck = true
|
||||
end
|
||||
return mask[string.byte(keyStr:sub(1,1))]
|
||||
end
|
||||
|
||||
Btns = {}
|
||||
function KeyPress(keyStr)
|
||||
if KeyHeld(keyStr) and Btns[keyStr] then
|
||||
Btns[keyStr] = false
|
||||
return true
|
||||
end
|
||||
Btns[keyStr] = true
|
||||
return false
|
||||
end
|
||||
|
||||
|
||||
|
178
tools/LuaScripts/Lua_Docs.md
Normal file
178
tools/LuaScripts/Lua_Docs.md
Normal file
|
@ -0,0 +1,178 @@
|
|||
# Lua Script Overview
|
||||
### Note
|
||||
Lua scripting support is still a work in progress, lua features and functions are subject to change.
|
||||
|
||||
## Hello World Eample
|
||||
Example of a Lua script that, when run, will display "Hello World", and a frame counter on the top screen.
|
||||
|
||||
```Lua
|
||||
screenID = 0 -- TopScreen = 0, BottomScreen = 1, OSD = 2
|
||||
canvas = MakeCanvas(0,0,100,100,screenID)
|
||||
SetCanvas(canvas)
|
||||
frameCount = 0
|
||||
|
||||
-- '_Update()' gets called once every frame
|
||||
function _Update()
|
||||
frameCount = frameCount+1
|
||||
ClearOverlay()
|
||||
Text(0,10,"Hello World: "..frameCount,0xffffff) --White text
|
||||
Flip()
|
||||
end
|
||||
```
|
||||
|
||||
## Types and Notation
|
||||
|
||||
`uReturnedVal FunctionName(uArg1,uArg2,[uOptionalArg1 = defaultVal],[uOptionalArg2 = defaultVal],...)`
|
||||
- `FunctionName` the name of the function
|
||||
- `uArg` Non optional argument expected of type "u" (lua types are n="number",s="string",b="boolean", t="table")
|
||||
- `[uOptionalArg = defaultVal]` optional argument expected of type "u" if not provided defaults to `defaultVal`
|
||||
- `uReturnedVal` Value returned by function (usually `nil`)
|
||||
|
||||
## Function List
|
||||
|
||||
`nil print(sPrintText)`
|
||||
- Print `sPrintText` to the lua consol
|
||||
|
||||
`nil MelonClear()`
|
||||
- Clearse the consol in the lua dialog window.
|
||||
|
||||
`n_u8Value Readu8(nAddress)`
|
||||
- Read Data unisigned one Byte from u32 address `nAddress`
|
||||
- `n_u16Value Readu16(nAddress)` Read Data unisigned two Bytes from u32 address `nAddress`
|
||||
- `n_u32Value Readu32(nAddress)` Read Data unisigned four Bytes from u32 address `nAddress`
|
||||
|
||||
`n_s8Value Readu8(nAddress)`
|
||||
- Read Data signed one Byte from u32 address `nAddress`
|
||||
- `n_s16Value Readu16(nAddress)` Read Data signed two Bytes from u32 address `nAddress`
|
||||
- `n_s32Value Readu32(nAddress)` Read Data signed four Bytes from u32 address `nAddress`
|
||||
|
||||
`nil NDSTapDown(nX,nY)`
|
||||
- Push down on the bottom touchscreen at pixel position X,Y.
|
||||
- touchscreen dimensions are 256x192.
|
||||
|
||||
`nil NDSTapUp()`
|
||||
- Releases the touch screen.
|
||||
|
||||
`nil StateSave(sFileName)`
|
||||
- Creates a savestate and saves it as `sFileName`.
|
||||
|
||||
`nil StateLoad(sFileName)`
|
||||
- Attempts to load a savestate with the filename `sFileName`.
|
||||
|
||||
`tMouseState GetMouse()`
|
||||
- Returns a lua table of the mouse X/Y coordinates and button states.
|
||||
- Table keys are: `X, Y, Left, Middle, Right, XButton1, XButton2`
|
||||
- Example
|
||||
```Lua
|
||||
if GetMouse().Left == true then
|
||||
print("Click at:"..GetMouse().X..','..GetMouse().Y)
|
||||
end
|
||||
```
|
||||
|
||||
`tJoyState GetJoy()`
|
||||
- Returns a lua table of the Joypad button states.
|
||||
- Table keys are: `A,B,Select,Start,Right,Left,Up,Down,R,L,X,Y`
|
||||
- Example
|
||||
```Lua
|
||||
function JoyText()
|
||||
joys = GetJoy()
|
||||
str = ""
|
||||
for k,v in pairs(joys) do
|
||||
if v then
|
||||
str = str..k
|
||||
end
|
||||
end
|
||||
return "Joy:"..str
|
||||
end
|
||||
```
|
||||
|
||||
`tKeyMask HeldKeys()`
|
||||
- Returns a lua table containing the value "true" for each held key and "nil" for any key not currently held.
|
||||
- check the Qt docs for a list of key codes: https://doc.qt.io/qt-6/qt.html#Key-enum
|
||||
- Example
|
||||
```Lua
|
||||
--Loop over all currently held keys
|
||||
for k,_ in pairs(HeldKeys()) do
|
||||
print("KeyPressed:"..k)
|
||||
end
|
||||
--Check if the "Q" key is currently pressed
|
||||
if HeldKeys()[string.byte("Q")] then
|
||||
print("\"Q\" Pressed!")
|
||||
end
|
||||
```
|
||||
|
||||
`tKeyStrokes Keys()`
|
||||
- Returns a lua table of all keys pressed since the last time `Keys()` was called...
|
||||
- DIFFERENT from `HeldKeys()`
|
||||
- This function keeps proper track of the order of keys typed, and if the same key was pressed multiple times since the last check.
|
||||
- Mostly used for cases that need faster typing/higher poll rate... otherwise use HeldKeys() for most use cases.
|
||||
- Example
|
||||
```Lua
|
||||
typed = ""
|
||||
keys = {}
|
||||
function KeysText()
|
||||
keys = Keys()
|
||||
str = ""
|
||||
for _,i in pairs(keys) do
|
||||
if pcall(string.char,i) then
|
||||
str = str..string.char(i)
|
||||
else
|
||||
print("NonAscii:"..i)
|
||||
typed = ""
|
||||
end
|
||||
end
|
||||
typed = typed..str
|
||||
return "Keys:"..typed
|
||||
end
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
`nCanvasID MakeCanvas(nX, nY,nWidth,nHeight,[nScreenTarget = 2],[bIsActive = true])`
|
||||
- Creates a new canvas and returns it's ID.
|
||||
- You must set a canvas using `SetCanvas` before drawing anything to the screen.
|
||||
- `nScreenTarget` options are 0 = TopScreen, 1 = BottomScreen, 2 = OSD / non-screen target (default)
|
||||
- `bIsActive` must be true for the canvas to be drawn to the screen (true by default)
|
||||
- `nCanvasID` is the ID of the newly created Canvas.
|
||||
|
||||
`nil SetCanvas(nCanvasID)`
|
||||
- Sets the current canvas to `nCanvasID`
|
||||
- All future drawing functions will draw to the *currently set canvas*.
|
||||
|
||||
`nil ClearOverlay()`
|
||||
- Clears the *currently set canvas* by setting all pixels to `0x00000000` transparent black
|
||||
|
||||
`nil Flip()`
|
||||
- Tells MelonDS that the *currently set canvas* has been updated and must be redrawn.
|
||||
- This may be handled automatically in the future...
|
||||
|
||||
`nil Text(nX,nY,sMessage, [nColor = 0x000000],[nFontSize = 9],[sFontFamily = 'Helvetica'])`
|
||||
- Draws `sMessage` to the *currently set canvas* given the color and font specified.
|
||||
- Qt will attempt to match `sFontFamily` to the closest font it can find available...
|
||||
|
||||
`nil Line(nX1,nY1,nX2,nY2,nColor)`
|
||||
- Draws a line on the *currently set canvas* fom (x1,y1) to (x2,y2) of the specified color.
|
||||
|
||||
`nil Rect(nX,nY,nWidth,nHeight,ncolor)`
|
||||
- Draws a 1p width Rectangle on the *currently set canvas* of the given size position and color.
|
||||
|
||||
`nil FillRect(nX,nY,nWidth,nHeight,ncolor)`
|
||||
- Draws a filled in Rectangle on the *currently set canvas* of the given size position and color.
|
||||
|
||||
`nil Ellipse(nX,nY,nWidth,nHeight,ncolor)`
|
||||
- Draws a 1p width Ellipse on the *currently set canvas* of the given size position and color.
|
||||
|
||||
`nil DrawImage(sImagePath,nX,nY,[nSourceX = 0],[nSourceY = 0],[nSourceWidth = -1],[nSourceHeight = -1])`
|
||||
- Draws the image saved at `sImagePath` to the *currently set canvas* at the given position.
|
||||
- By default copies the entire image to the canvas.
|
||||
- optional arguments Source-(X,Y,Width,Height) determain where in the source image to copy from.
|
||||
|
||||
`nil ClearImageHash()`
|
||||
- Clears the interal hash of all images drawn using DrawImage
|
||||
- Only needed if RAM is being filled up with lots of different Image files.
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -17,7 +17,8 @@
|
|||
{
|
||||
"name": "libslirp",
|
||||
"platform": "linux"
|
||||
}
|
||||
},
|
||||
"lua"
|
||||
],
|
||||
"features": {
|
||||
"qt6": {
|
||||
|
|
Loading…
Add table
Reference in a new issue