Create account / Log in

Lua

Discussion area for the development team.

Moderators: uckelman, Tim M

Re: Lua

Postby Brent Easton » May 16th, 2020, 10:44 pm

We don't actually need a platform that 'supports' Lua in any way, we just build it in. Vassal NextGen will be written in C++ so that's all we need supported. We just compile our own copy of Lua (written in C) in and build our own interface. Anything 'fancy' we need scripts to do, we provide as a call in Vassal NG. In fact, we are better off without any in-built platform support within Lua as we need to fully control what functionality is available to Lua scripts.

QT is looking interesting. Since we last discussed this, Nokia has sold QT on and it is now available for use under Open Source licenses. It has proper support for IOS and Android.
User avatar
Brent Easton
 
Posts: 3068
Joined: December 21st, 2007, 3:06 am
Location: Berry, NSW, Australia

Re: Lua

Postby Rhett » May 19th, 2020, 6:07 pm

Yes, we package our custom Lua (and widgets) with every module. That way every module is independent. The user does not have to install Lua or anything else.

I have been trying to implement a few features with wxLua so as to test what a script like this can do.


Code: Select all
package.cpath = '/home/test/Documents/wxLua-build/lib/Debug/lib?.so'
require('wx')

frame = wx.wxFrame(wx.NULL, wx.wxID_ANY, "pilot Vassal 4",
                   wx.wxDefaultPosition, wx.wxSize(450, 450),
                   wx.wxDEFAULT_FRAME_STYLE)
                   
                   
panel = wx.wxPanel(frame, wx.wxID_ANY, wx.wxDefaultPosition, wx.wxSize(300, 300))


map = wx.wxBitmap("/home/test/projects/vassal/map.png")
piece1 = wx.wxBitmap("/home/test/projects/vassal/piece1.png")
piece2 = wx.wxBitmap("/home/test/projects/vassal/piece2.png")


Counter = { x, y, bitmap, panel }
Counter.__index = Counter


         
function Counter:create(x, y, piece)
   local cnt = {}
   setmetatable(cnt, Counter)
   cnt.x = x
   cnt.y = y
   cnt.bitmap = piece
   cnt.panel = wx.wxPanel(panel, wx.wxID_ANY, wx.wxPoint(x,y), wx.wxSize(50, 50))   
   local OnLeftDown = function(event)
      local dropSource = wx.wxDropSource(cnt.panel)      
      dropSource.SetData(dropSource, wx.wxBitmapDataObject(cnt.bitmap))
      local result = dropSource.DoDragDrop(dropSource)
   end   
   cnt.panel:Connect(wx.wxEVT_LEFT_DOWN, OnLeftDown)
   return cnt
end

function Counter:paint()
   local dc = wx.wxPaintDC(self.panel)
   dc.DrawBitmap(dc, self.bitmap, 0, 0, false)
   dc:delete()
end


counters = {}
table.insert(counters, Counter:create(14, 10, piece1))
table.insert(counters, Counter:create(196, 114, piece2))




function OnLeftDown(event)
   local pos = wx.wxGetMousePosition()
   pos = frame:ScreenToClient(pos)
   print(pos.x.." "..pos.y)
end

panel:Connect(wx.wxEVT_LEFT_DOWN, OnLeftDown)


function OnPaint(event)
    local dc = wx.wxPaintDC(panel)
    dc.DrawBitmap(dc, map, 0, 0, false)   
    for i = 1, #counters do
      counters[i]:paint()
   end
    dc:delete()
end
                 
panel:Connect(wx.wxEVT_PAINT, OnPaint)



frame:Show(true)


The files map.png, piece1.png and piece2.png can be anything but my two piece sizes were 50x50.

The first problem with the script is that it is possible to load files from the host file system. This is a feature of the widgets classes. This must not be allowed. I presume that is is possible to turn off class member functions in a custom widget.

The second problem came when I tried to implement drag-and-drop. wxWidgets has classes for this: wxDropSource and wxDropTarget. wxDropSource works (and when you run the script you see that). wxDropTarget did not work. An instance of this class could simply not be created. I looked at the source code in wxLua and found this in wxLua-2.8.12.3-src/bindings/wxwidgets/wxcore_clipdrag.i

Code: Select all
class wxDropTarget // FIXME implement virtual
{
    //wxDropTarget(wxDataObject* data = NULL) pure virtual functions in MSW
    virtual bool GetData( );
    //wxDragResult GetDefaultAction( );
    //virtual wxDragResult OnData(wxCoord x, wxCoord y, wxDragResult def );
    //virtual bool OnDrop(wxCoord x, wxCoord y );
    //virtual wxDragResult OnEnter(wxCoord x, wxCoord y, wxDragResult def );
    //virtual wxDragResult OnDragOver(wxCoord x, wxCoord y, wxDragResult def );
    //virtual void OnLeave( );
    //void SetDataObject(wxDataObject* data );
    //void SetDefaultAction(wxDragResult action );
};


All members except GetData() have not been implemented. I can not possibly comment on why, but it shows that everything has to be tested and that any given widget may not work completely.

Else I am happy with what I can do with wxLua. Note how click events are captured by the Counter class, so that when you click on a counter its own OnLeftDown is triggered.

Use of widgets have great potential as it gives the module designer an unlimited amount of access to the data structures of the module such as map and counters. You can also draw what you want on the screen.

The question remains what happens when the script file(s) start to get huge. This is a question of both writing and maintaining the scripts for the module maker and a question of efficiency when the module becomes large and complex.

What a script does:

  • Holds the game data structures and their associated functions, like map, grid and counter.
  • Draws the Map window and a tabbed Counter window with counters/cards that can be dragged to the map.

What vassal.cpp does:
  • All loading and saving of games.
  • All communication with the server.
  • Draws the Room window and the Chat window,
  • Logfiles/Undo functionality.

Effective communication of data between script and vassal.cpp is essential.

The efficiency of such a design remains to be seen. But it certainly gives the module designer great freedom.
Rhett
 
Posts: 38
Joined: March 18th, 2014, 9:32 am

Re: Lua

Postby Rhett » July 9th, 2020, 11:39 am

I have now made the Qt eqvivalent of the Wx drag-and-drop script.

Code: Select all
package.cpath = "/home/test/projects/alben/build-lqt/lib/?.so"

local QtCore = require 'qtcore'
local QtGui = require 'qtgui'
local QtWidgets = require 'qtwidgets'


local app = QtWidgets.QApplication.new(0, {})


local window = QtWidgets.QMainWindow.new()

window:setMinimumWidth(640)
window:setMinimumHeight(400)
                 
                   
local frame = QtWidgets.QFrame.new(window)

frame:setMinimumWidth(640)
frame:setMinimumHeight(400)
frame:setAcceptDrops(true)


function frame:dragEnterEvent(event)
    event:acceptProposedAction()
end

function frame:dropEvent(event)
    local mimeData = QtCore.QMimeData.new()
    mimeData = event:mimeData()
    local byteArray = QtCore.QByteArray.new(mimeData:data('application/x-alben-counter'))
    local x = byteArray:mid(0,2):toInt()
    local y = byteArray:mid(2,2):toInt()
    local offset = QtCore.QPoint.new(x, y)
    event:source():move(event:pos() - offset)
    event:accept()
end



map = QtGui.QImage("/home/test/projects/alben/scripts/Map.png")

piece1 = QtGui.QPixmap("/home/test/projects/alben/scripts/piece1.png")
piece2 = QtGui.QPixmap("/home/test/projects/alben/scripts/piece2.png")


Counter = { pixmap, frame }
Counter.__index = Counter


         
function Counter:create(x, y, image)
    local cnt = {}
    setmetatable(cnt, Counter)
    cnt.pixmap = image
    cnt.frame = QtWidgets.QFrame.new(frame)
    cnt.frame:setMinimumWidth(50)
    cnt.frame:setMaximumWidth(50)
    cnt.frame:setMinimumHeight(50)
    cnt.frame:setMaximumHeight(50)   
    cnt.frame:move(x, y)
    cnt.frame:setAcceptDrops(false)
    local leftDownHandler = function(self, mouseEvent)
        local drag = QtGui.QDrag(self)     
        drag:setPixmap(cnt.pixmap)
        local offset = mouseEvent:pos()
        drag:setHotSpot(offset)
        local byteArray = QtCore.QByteArray.new()
        local byteArrayY = QtCore.QByteArray.new()
        byteArray:setNum(offset:x())
        byteArrayY:setNum(offset:y())
        byteArray:append(byteArrayY)
        local mimeData = QtCore.QMimeData.new()
        mimeData:setData('application/x-alben-counter', byteArray)
        drag:setMimeData(mimeData)
        dropAction = drag:exec()
        return true
    end
    cnt.frame.mousePressEvent = leftDownHandler
    return cnt
end



counters = {}
table.insert(counters, Counter:create(14, 10, piece1))
table.insert(counters, Counter:create(196, 114, piece2))



function frame:mousePressEvent(event)
    local pos = event:pos()
    print(event:type())
    print(pos:x().." "..pos:y())
end



function frame:paintEvent(event)
    local painter = QtGui.QPainter(frame)
    painter:begin(frame)
    painter:drawImage(QtCore.QRect(0, 0, 640, 400), map)
    for i = 1, #counters do
        painter:drawPixmap(QtCore.QRect(counters[i].frame:x(), counters[i].frame:y(), 50, 50), counters[i].pixmap)
    end
    painter['end'](painter)
end
                 


frame:show()
window.show(window)


app.exec()


This was not easy. I have been through a lot of issues but the drag and drop now actually works.

Qt is a very polished and professional product and well documented. Recently it has gotten an Open Source version. Go to the download page here. How to download and build is well documented and need not be repeated. I recommend to only download modules qtcore, qtgui and qtwidgets as they are likely the only ones needed in this context. Do not download qtwebengine.

Make sure you have 2GB virtual RAM and a 50GB virtual hard disk on your virtual machine.

The Lua binding is done with lqt. The Github is here.

How to build lqt is well documented. I order for me to build on Linux I had to make my own CMakeLists.txt. You can find it on my Github here. Replace the lqt CMakeLists.txt with this one. Note that this builds for Linux only. You must change the directories to your own directories for build-qt5, build-lqt and lqt. Also, what is needed for the find-packages is really just this:

Code: Select all
find_package(Qt5 REQUIRED
   Core
   Gui
   Widgets
)


You also have to make a FindLuaJiT.cmake in /usr/share/cmake-3.10/Modules to find luajit.

The drag and drop script is run with

Code: Select all
./luajit qtLua.lua


You find qtLua.lua and a sample Map.png, piece1.png and piece2.png here.


Issues:

  • The dragged image flickers, likely because of paint conflicts.
  • The documentation and knowledge-base for what we can call qtLua is basically non-existent. wxLua was much better in this regard.


TODO:
  • Next up is to implement zoom of map and counters. This may be trivial or not.
  • Then I will implement a mock-up of a real board game with a large map and about 100 counters. I will make Lua classes for Map, Grid, Counter and Stack. Each class will be in a separate file (like in Java). I hope to be able to cope with scope and size. This will in many ways decide how practical a design like this is.
Rhett
 
Posts: 38
Joined: March 18th, 2014, 9:32 am

Re: Lua

Postby Rhett » July 30th, 2020, 10:27 am

An update on the project.

I discovered that the Linux binding in lqt was not complete. In particular the class QMenuBar that creates a drop-down menu on top of the application window is missing. This is (needless to say) a vital feature that must be in place.

The binding is (for reasons I do not now know) platform-dependent. lqt state that they currently support MSW and Mac binding. An important goal for the project is to support MSW, Mac, Linux, Android and iOS, so the binding has to be implemented on all 5 platforms.

I am getting help from the guys behind lqt. I hope I don't have to spend much time with the binding code itself. It means entering a new field I know nothing about. But then again, is that not always how it is with new technology?

I hope to resolve this soon. Making a MSW binding for the prototype is also possible. I have to find a good old MSW C++ compiler. I am not part of the MSW world anymore. Anyone know of a good, free C++ compiler that is compatible with Windows 7 and over? How about Borland?
Rhett
 
Posts: 38
Joined: March 18th, 2014, 9:32 am

Re: Lua

Postby Flint1b » July 30th, 2020, 11:32 am

mingw?
User avatar
Flint1b
 
Posts: 402
Joined: May 19th, 2020, 12:27 am
Location: Colonia Agrippina

Re: Lua

Postby uckelman » July 30th, 2020, 12:16 pm

Thus spake Flint1b:
> mingw?

That would be my suggestion as well.

--
J.
User avatar
uckelman
Site Admin
 
Posts: 8789
Joined: December 10th, 2007, 9:48 am
Location: Durham, England

Re: Lua

Postby Flint1b » July 30th, 2020, 12:43 pm

Alternatively, clang/LLVM.
User avatar
Flint1b
 
Posts: 402
Joined: May 19th, 2020, 12:27 am
Location: Colonia Agrippina

Re: Lua

Postby Brent Easton » July 30th, 2020, 12:43 pm

MingW is a mini-unix environment running under Windows that allows you to use the GNU gcc C++ compiler. I have had good results using this within Eclipse, although you can also use it with other IDE's, or roll you own make files and use it with just a text editor.

It is an adventure to install, find a good tutorial and follow it closely. Once installed, I had no problems and was able to download and build pretty much any unix library or utility I needed.

You can also use gcc with the cygwin environment, but I could never get this to work.
User avatar
Brent Easton
 
Posts: 3068
Joined: December 21st, 2007, 3:06 am
Location: Berry, NSW, Australia

Re: Lua

Postby uckelman » July 30th, 2020, 12:57 pm

Thus spake Brent Easton:
> MingW is a mini-unix environment running under Windows that allows you
> to use the GNU gcc C++ compiler. I have had good results using this
> within Eclipse, although you can also use it with other IDE's, or roll
> you own make files and use it with just a text editor.
>
> It is an adventure to install, find a good tutorial and follow it
> closely. Once installed, I had no problems and was able to download and
> build pretty much any unix library or utility I needed.
>
> You can also use gcc with the cygwin environment, but I could never get
> this to work.

If you're using Linux, you're in luck:

I use mingw for cross-compiling Windows libraries and executables every
day, and have for quite some years now. A lot of Linux distributions
have mingw packages, and these days it mostly just works.

--
J.
User avatar
uckelman
Site Admin
 
Posts: 8789
Joined: December 10th, 2007, 9:48 am
Location: Durham, England

Re: Lua

Postby Flint1b » July 30th, 2020, 5:19 pm

Figures. Linux is a better environment for the WINDOWS compiler than windows itself :D
User avatar
Flint1b
 
Posts: 402
Joined: May 19th, 2020, 12:27 am
Location: Colonia Agrippina

Re: Lua

Postby Rhett » July 30th, 2020, 8:49 pm

Thanks guys.

MinGW quickly led me to MXE, a complete cross-compile environment for Qt.

https://github.com/mxe/mxe

For use see here:

https://stackoverflow.com/questions/141 ... or-windows

I may use MXE. Problem: it seems to download Qt5.15 when lqt only works with Qt5.12 and Qt5.13.... Soon (?) I should be running a Windows version of the binding. After that I will make a simple "scale-and-rotate-an-image" script to test quality/speed of this in Lua under Qt.

I will also try to upload all files needed to run this script to my Git so that anyone with Windows can download and run it (or use Wine on Linux). This is important. The user should (in theory) not have to install anything, just download files, run the script and it will work. The Lua interpreter and the Qt binding lib files should come as a bundle. Only qtcore, qtgui and qtwidgets are needed (I presume).

luajit ---- 518.3 KB

qtcore.so ---- 24,7MB
qtgui.so ---- 22.2MB
lqtwidgets.so ---- 26.9MB

Relatively large files. They need of course only be downloaded once. May be dependent on other libraries.
Rhett
 
Posts: 38
Joined: March 18th, 2014, 9:32 am

Re: Lua

Postby Brent Easton » July 30th, 2020, 9:42 pm

Saying mingw IS a windows compiler is very confusing and incorrect. Mingw HAS compilers, and they are just the same standard GNU compilers used on linux machines.

mingw is basically a minimal set of libraries and a port of the standard GNU compilers that allows other GNU linux software to be compiled, built and run on a windows system. It allows you to compile and run native Windows apps without needing other 3rd party libraries.

When you use mingw to 'cross-compile from linux to windows' you are using a special version of the standard GNU C++ compiler that has been built with linux versions of the mingw libraries to run on linux to output windows compatible run time code.
User avatar
Brent Easton
 
Posts: 3068
Joined: December 21st, 2007, 3:06 am
Location: Berry, NSW, Australia

Re: Lua

Postby Flint1b » July 30th, 2020, 11:00 pm

Code: Select all
MinGW, a contraction of "Minimalist GNU for Windows"


It's primary target IS Windows, that whole project was made for Windows, it's a port of the GCC for Windows. Ok it's not a/one compiler, its the whole toolchain with assembler linker etc.

At least there's no clang/llvm fans here..
User avatar
Flint1b
 
Posts: 402
Joined: May 19th, 2020, 12:27 am
Location: Colonia Agrippina

Re: Lua

Postby Rhett » August 2nd, 2020, 12:20 pm

Ok, I realized I had all the necessary bindings to make the Scale-and-Rotate script under Linux.

I decided to do this first because it illustrates a few important points.

The script (qtScaleRotate.lua) looks like this:

Code: Select all
package.cpath = "./?.so"

local QtCore = require 'qtcore'
local QtGui = require 'qtgui'
local QtWidgets = require 'qtwidgets'


local app = QtWidgets.QApplication.new(0, {})
app.setApplicationName('qtLua .. Scale and Rotate')


local window = QtWidgets.QMainWindow.new()

window:setMinimumWidth(640)
window:setMinimumHeight(400)
                 

                 

image = QtGui.QImage("./sailboat.png")


inIcon = QtGui.QIcon("./zoom_in.png")
outIcon = QtGui.QIcon("./zoom_out.png")
rotateIcon = QtGui.QIcon("./rotate.png")

local buttonIn = QtWidgets.QPushButton(inIcon, " ", window)
local buttonOut = QtWidgets.QPushButton(outIcon, " ", window)
local buttonRotate = QtWidgets.QPushButton(rotateIcon, " ", window)

buttonIn:setFixedSize(80, 20)
buttonOut:setFixedSize(80, 20)
buttonRotate:setFixedSize(80, 20)



local buttonBox = QtWidgets.QWidget(window)
layout = QtWidgets.QHBoxLayout()
buttonBox:setLayout(layout)
layout:addWidget(buttonIn, 1)
layout:addWidget(buttonOut, 1)
layout:addWidget(buttonRotate, 1)

buttonBox:setMinimumWidth(window:width())
buttonBox:setMinimumHeight(25)






local frame = QtWidgets.QFrame.new(window)
frameWidth = window:width()
frameHeight = window:height() - 30
frame:setMinimumWidth(frameWidth)
frame:setMinimumHeight(frameHeight)
frame:setFrameRect(QtCore.QRect(0, 30, frameWidth, frameHeight))



-- rawset(_G, 'SIGNAL', function(s) return '2' .. s end)
-- rawset(_G, 'SLOT', function (s) return '1' .. s end)

--frame:__addslot('handler()', function(event)
    --local pos = event:pos()
    --print(event:type())
    --print(pos:x().." "..pos:y())
--    print('******')
--end)
--frame.connect(buttonIn, SIGNAL 'clicked()', frame, SLOT 'handler()')


local scales = { 0.2, 0.3, 0.44, 0.67, 1.0, 1.5 }
local scaleIndex = 5
local rotationIncrement = 60
local rotation = 0


function frame:mousePressEvent(event)
    local pos = event:pos()
    print(event:type())
    print(pos:x().." "..pos:y())
    if (buttonIn:geometry():contains(pos)) then
        if (scaleIndex < #scales) then
            scaleIndex = scaleIndex + 1
        end
    end
    if (buttonOut:geometry():contains(pos)) then
        if (scaleIndex > 1) then
            scaleIndex = scaleIndex - 1
        end
    end
    if (buttonRotate:geometry():contains(pos)) then
        if (rotation + rotationIncrement <= 180) then
            rotation = rotation + rotationIncrement
        else
            rotation = (rotation + rotationIncrement - 180) - 180
        end
    end
    frame:repaint()
end



function round(float)
    return math.floor(float)
end


function frame:paintEvent(event)
    local painter = QtGui.QPainter(frame)
    painter:begin(frame)
    painter:setClipRect(frame:frameRect())
    transform = QtGui.QTransform()
    transform:scale(scales[scaleIndex], scales[scaleIndex])
    transform:rotate(rotation)
    local newImage = image:transformed(transform, QtCore.TransformationMode.SmoothTransformation)
    local w = newImage:width()
    local h = newImage:height()
    local x = round((frameWidth - w)/2)
    local y = round((frameHeight - h)/2)
    painter:drawImage(QtCore.QRect(x, y, w, h), newImage)
    painter['end'](painter)
end
                 




window.show(window)

app.exec()


This is a screen of the script running (the sailboat has been scaled down and rotated):

https://github.com/RhettTR/Alben/tree/master/pilot/Scale-and-Rotate/screen.jpg

I managed to make a complete stand-alone version of the script. The following is a list of all the files needed to run the script:

Code: Select all
-rwxr-xr-x 1 meg meg  72526007 Jul 27 16:58 libQt5Core.so.5
-rwxr-xr-x 1 meg meg 124911321 Jul 27 17:10 libQt5Gui.so.5
-rwxr-xr-x 1 meg meg  99972370 Aug  1 20:11 libQt5Widgets.so.5
-rwxr-xr-x 1 meg meg  67071228 Aug  1 20:06 libQt5XcbQpa.so.5
-rwxr-xr-x 1 meg meg    518296 Jun  8 15:46 luajit
drwxrwxr-x 2 meg meg      4096 Aug  1 20:35 platforms
-rwxr-xr-x 1 meg meg  24695328 Aug  1 19:40 qtcore.so
-rwxr-xr-x 1 meg meg  22219952 Aug  1 19:48 qtgui.so
-rw-rw-r-- 1 meg meg      3228 Aug  2 11:27 qtScaleRotate.lua
-rwxr-xr-x 1 meg meg  26924912 Aug  1 19:52 qtwidgets.so
-rw-rw---- 1 meg meg       802 Aug  2 12:03 readme
-rw-rw-r-- 1 meg meg       397 Jul 31 16:38 rotate.png
-rw-rw-r-- 1 meg meg     25290 Aug  2 11:26 sailboat.png
-rw-rw-r-- 1 meg meg       960 Jul 31 16:31 zoom_in.png
-rw-rw-r-- 1 meg meg      1024 Jul 31 16:32 zoom_out.png

./platforms:
total 648
-rwxr-xr-x 1 meg meg 663377 Aug  1 20:40 libqxcb.so


As seen, you need a total of 8 libs. I had to change the RUNPATH of the libs with chrpath because I wanted to run them in the same directory as qtScaleRotate.lua. Or just put the libs in /usr/lib/x86_64-linux-gnu.

The libs are big. libQt5Gui.so.5 is over 124MB. The total runs up to about 437 MB. This is much. I could not upload these files to Git (max is 25MB). That means that anyone wanting to run the script has to build qt5 and lqt on their own computer....

A VASSAL distribution is 17.1 MB (3.3.2) ...

Likely not all Qt classes are needed, and libQt5Core.so, libQt5Gui.so and libQt5Widgets.so can be trimmed, but they will always be large files and its hardly appropriate to have such a huge distribution :?

The script file and the images used can be found on my Git.

Issues:


  • For some reason the QPushButton class does not have an overridable mousePressEvent handler. The way Qt wants this to be done is with Connect. I managed to set this up (as seen by what is commented out) but it didn't work. Instead I used buttonOut:geometry():contains(pos) in the Frame handler which is dirty but actually works ok.
  • The stand-alone script only accepted PNG files .. lol .. but is must be because of a missing plugin or such. My regular script could of course accept JPG files.
  • The binding (lqt) is not just a tool we take down from the shelf. The binding will have to be an integral part of Vassal 4. We will have to develop a custom-lqt if for no other reason because we need to remove all members/functions that in any way can read/write the host filesystem. An Android and iOS binding also has to be developed.
  • Not really an issue but I was very impressed by the way Qt handles image transformations. It is just a part of the Paint. The QTransform class does it very readily. The scaling and rotation goes fast and looks good, even on my debug compile. I am sure Qt does a fantastic job in handling images.

Rhett
 
Posts: 38
Joined: March 18th, 2014, 9:32 am

Re: Lua

Postby Flint1b » August 2nd, 2020, 1:44 pm

I know a library that can do this scaling/rotating on it's own, and on top of that keeps the coordinates the same, i.e. you can scale/rotate the map and the counters on it as much as you want, the x/y coordinates of the map stay exactly the same, there is no need to adjust them for the scale factor.

Library is called JavaFX :D
User avatar
Flint1b
 
Posts: 402
Joined: May 19th, 2020, 12:27 am
Location: Colonia Agrippina

PreviousNext

Return to Developers

Who is online

Users browsing this forum: No registered users and 0 guests