MCode Tutorial (Compiled Code in AHK)

Helpful script writing tricks and HowTo's
User avatar
nnnik
Posts: 4500
Joined: 30 Sep 2013, 01:01
Location: Germany

MCode Tutorial (Compiled Code in AHK)

30 Sep 2013, 12:00

This is a Tutorial I had been looking for.
It takes hours and hours to figure out all of this by yourself. :shock:
That's why I'll make a Tutorial here.
That all of you can easily understand what MCode is and what it is used for. :D


First of all I'm going to talk about what MCode is and why it is being used.
After that we'll have a look at some practical examples.
While doing this I'll tell you about the Problems. :shock:

1. Introduction:What is MCode ?
MCode is compiled code. For example if you make a C++ program it'll result in this kind of code.
AHK Scripts form the opposite of compiled code. In an AHK script the interpreter looks at the script text and then executes the corresponding code.
In an compiled language (e.g. C++) the compiler creates such Code directly.
The CPU is directly able to read such code.
MCode is such compiled code that can be used inside an AHK script.
We are able to execute this code via DllCall.
For Example the example MCode of the compiler I use is:
2,x86:aipYww==,x64:uCoAAADD
This is a Function that simply return 42 (The answer to all questions and everything.).

1. Introduction:What is MCode good for?
MCode is one of the various ways to increase the speed of your Script/Program.
Especially if you're calculating with a huge amount of Data.
For example you can make functions that can change a GDI+ picture.
MCode also gives you the possibility to execute code on a very low level (Assembly).

1. Introduction:Sounds nice. What is needed?
1. Knowledge about C++ and DllCalls:
If you don't think you're good enough in DllCalls forget it.
You can find an super nice C++ here:
http://www.cplusplus.com/doc/tutorial/

2. A compiler:
The compiler I use can be found at:
http://ahkscript.org/boards/viewtopic.php?f=6&t=4642
You need to install GCC
http://www.autohotkey.com/board/topic/55162-mcodegen-easily-transform-cc-code-into-mcode/
You need to install Visual C++
I'll add any other suggestions here.


3. an MCode Function:
I use this one:

Code: Select all

MCode(mcode)
{
  static e := {1:4, 2:1}, c := (A_PtrSize=8) ? "x64" : "x86"
  if (!regexmatch(mcode, "^([0-9]+),(" c ":|.*?," c ":)([^,]+)", m))
    return
  if (!DllCall("crypt32\CryptStringToBinary", "str", m3, "uint", 0, "uint", e[m1], "ptr", 0, "uint*", s, "ptr", 0, "ptr", 0))
    return
  p := DllCall("GlobalAlloc", "uint", 0, "ptr", s, "ptr")
  if (c="x64")
    DllCall("VirtualProtect", "ptr", p, "ptr", s, "uint", 0x40, "uint*", op)
  if (DllCall("crypt32\CryptStringToBinary", "str", m3, "uint", 0, "uint", e[m1], "ptr", p, "uint*", s, "ptr", 0, "ptr", 0))
    return p
  DllCall("GlobalFree", "ptr", p)
}
2. Examples


After you've added the function to your stdli directory you are ready to program your own functions.
The example code, of the compiler I mentioned above is this one:

Code: Select all

int MyFunction()
{
  return 42;
}
As stated above it always returns 42.
When you press the Create Machine Code button, the online compiler will create this code.

Code: Select all

MyFunction := MCode("2,x86:uCoAAADD,x64:uCoAAADD")
After you've added it to your script you can call it via DllCall.
Your Script should look like this.

Code: Select all

MyFunction := MCode("2,x86:uCoAAADD,x64:uCoAAADD")
Msgbox % DllCall(MyFunction,"cdecl")
Note: You have to specify cdecl in the DllCall function in order to execute it because it is the standard calling convention in C/C++.

But now a more complex function.
It searches for the end of an string: The 0-Char:

Code: Select all

int stringlen(char *str)
{
  int i=0;
  for (; str[i]!=0; i++);
  return i;
}
The result is the following code:

Code: Select all

stringlen := MCode("2,x86:i0wkBDPAOAF0B0CAPAgAdfnD,x64:M8A4AXQKSP/B/8CAOQB19vPD")
MsgBox, % DllCall(stringlen, "astr", "test","cdecl")
Now an even more complex function.

Code: Select all

unsigned int MyFunction(unsigned int a,unsigned int b)
{
if (a>0)  
return MyFunction(a-1,b*a);
else
return b;
}
This function calculates the factulty of the nr. a.
In order to do so it calls itself.
Now the AHK code.

Code: Select all

MyFunction := MCode("2,x86:i0wkBItEJAiFyXQKjWQkAA+vwUl1+sM=,x64:hcl0Bw+v0f/JdfmLwsM=")
Msgbox % DllCall(MyFunction,"int",3,"int",1,"cdecl")
The result is 6 as expected.

But you have to write too much.(Yes I'm lazy)
Actually you dont need this part: ,"int",1,"cdecl") We could create a caller function that does the thing we want.
In order to use the stdcall convention(Wich is the standard for the DllCall function) you have to put a _stdcall before the function.
Your C++ code will look like this:

Code: Select all

unsigned int MyFunction(unsigned int a,unsigned int b)
{
if (a>0)  
return MyFunction(a-1,b*a);
else
return b;
}

unsigned int _stdcall MyFunctionCaller(unsigned int a)
{
return MyFunction(a,1);
}
The result:

Code: Select all

MyFunction := MCode("2,x86:i0wkBItEJAiFyXQKjWQkAA+vwUl1+sM=,x64:hcl0Bw+v0f/JdfmLwsM=")
MyFunctionCaller := MCode("
(LTrim Join
2,x86:i0QkBIXAdA5QSFDoAAAAAIPECMIEALgBAAAAwgQA,x64:i9GFyXQH/8npAAAAALgBAAAAww==
)")
Msgbox % DllCall(MyFunctionCaller,"uint",3)
But the Messagebox is empty. :o
No we didn't make a mistake. The Problems name is MCode:
OK Lets imagine you have a AHK script, but you have to call every function by its position: (e.g. call Char nr. 144.).
This is where your function starts at Char 144.
Now lets imagine we put some new code in front of your function.
Your function will be moved backwards e.g. 20 Chars.
If you don't change the starting nr. of your function you wont be able to call it properly.
In Machine Code it is exactly like this:
A function is called by calling the start address of the function.
But the function is added after the AHK stuff.
That's why your function is calling something else. This leads to errors.
The compiler is able to tell you the starting address it expected:
(Note: Not for 64 bit PCs)

Code: Select all

/*
unsigned int MyFunction(unsigned int a,unsigned int b)
{
if (a>0)  
return MyFunction(a-1,b*a);
else
return b;
}

unsigned int MyFunctionaddress()
{
return (unsigned int)(&MyFunction);
}
*/
MyFunction := MCode("2,x86:i0wkBItEJAiFyXQKjWQkAA+vwUl1+sM=,x64:hcl0Bw+v0f/JdfmLwsM=")
MyFunctionaddress := MCode("2,x86:uAAAAADD,x64:SI0FAAAAAMM=")
Msgbox % "The expected Address of the function is :" DllCall(MyFunctionaddress)
Msgbox % "The actual address of the function is :" MyFunction
So you cant call functions in a normal way. :evil:
So what do we do now :?:
The answer is function pointers. :!:...:?:
Functionpointers are documented here:
http://www.cplusplus.com/doc/tutorial/pointers/
Please read through the site mentioned before reading further.


To call our function in MCode we have to give it the pointer to our function.

Code: Select all

unsigned int MyFunction(unsigned int a,unsigned int b)
{
if (a>0)  
return MyFunction(a-1,b*a);
else
return b;
}

unsigned int _stdcall MyFunctionCaller(unsigned int a,unsigned int(*MyFunction)(unsigned int,unsigned int) )
{
return (*MyFunction)(a,1);
}
Results in:

Code: Select all

MyFunction := MCode("2,x86:i0wkBItEJAiFyXQKjWQkAA+vwUl1+sM=,x64:hcl0Bw+v0f/JdfmLwsM=")
MyFunctionCaller := MCode("2,x86:i0QkBGoBUP9UJBCDxAjCCAA=,x64:SIvCugEAAABI/+A=")

When we call the new function we also have to add the Function pointer.

Code: Select all

MyFunction := MCode("2,x86:i0wkBItEJAiFyXQKjWQkAA+vwUl1+sM=,x64:hcl0Bw+v0f/JdfmLwsM=")
MyFunctionCaller := MCode("2,x86:i0QkBGoBUP9UJBCDxAjCCAA=,x64:SIvCugEAAABI/+A=")
Msgbox % DllCall(MyFunctionCaller,"uint", 3,"UPtr",MyFunction)
6 was expected and if you did everything right it should be the result.

There are also other things that wont work in autohotkey:
  1. Global&Static Variables
  2. Objects(thiscall konvention)
  3. Sometimes even calling the own function
  4. Preset Floats can cause difficulties cause compiler rather sores them at a specific address than in a code.

Also feel free to share any MCode function you've created here.
I might need some good examples for further chapters.
Recommends AHK Studio
Alibaba
Posts: 480
Joined: 29 Sep 2013, 16:15
Location: Germany

Re: MCode Tutorial (Compiled Code in AHK)

30 Sep 2013, 12:36

Great idea!
I already had some serious problems with it. :D
"Nothing is quieter than a loaded gun." - Heinrich Heine
User avatar
nnnik
Posts: 4500
Joined: 30 Sep 2013, 01:01
Location: Germany

Re: MCode Tutorial (Compiled Code in AHK)

01 Oct 2013, 00:11

I think I'll finish it today.
I thought I could do it without thinking much, but that isn't the case.
Recommends AHK Studio
Alibaba
Posts: 480
Joined: 29 Sep 2013, 16:15
Location: Germany

Re: MCode Tutorial (Compiled Code in AHK)

01 Oct 2013, 11:17

One does not simply write a tutorial without thinking much.
"Nothing is quieter than a loaded gun." - Heinrich Heine
User avatar
nnnik
Posts: 4500
Joined: 30 Sep 2013, 01:01
Location: Germany

Re: MCode Tutorial (Compiled Code in AHK)

01 Oct 2013, 13:01

And I'm not simply one :P
Recommends AHK Studio
Alibaba
Posts: 480
Joined: 29 Sep 2013, 16:15
Location: Germany

Re: MCode Tutorial (Compiled Code in AHK)

01 Oct 2013, 17:41

nnnik wrote:And I'm not simply one :P
Best answer. +1
"Nothing is quieter than a loaded gun." - Heinrich Heine
User avatar
joedf
Posts: 8940
Joined: 29 Sep 2013, 17:08
Location: Canada
Contact:

Re: MCode Tutorial (Compiled Code in AHK)

01 Oct 2013, 20:14

fix the BBcode!!!! ahhhh BBcode apocalypse!
BBcolypse. :D
Image Image Image Image Image
Windows 10 x64 Professional, Intel i5-8500, NVIDIA GTX 1060 6GB, 2x16GB Kingston FURY Beast - DDR4 3200 MHz | [About Me] | [About the AHK Foundation] | [Courses on AutoHotkey]
[ASPDM - StdLib Distribution] | [Qonsole - Quake-like console emulator] | [LibCon - Autohotkey Console Library]
User avatar
nnnik
Posts: 4500
Joined: 30 Sep 2013, 01:01
Location: Germany

Re: MCode Tutorial (Compiled Code in AHK)

02 Oct 2013, 02:10

Yeah I know but somehow im too lazy.
...Done
Recommends AHK Studio
User avatar
joedf
Posts: 8940
Joined: 29 Sep 2013, 17:08
Location: Canada
Contact:

Re: MCode Tutorial (Compiled Code in AHK)

02 Oct 2013, 02:24

nnnik wrote:Yeah I know but somehow im too lazy.
...Done
same here... lol :P i feel your pain.
Image Image Image Image Image
Windows 10 x64 Professional, Intel i5-8500, NVIDIA GTX 1060 6GB, 2x16GB Kingston FURY Beast - DDR4 3200 MHz | [About Me] | [About the AHK Foundation] | [Courses on AutoHotkey]
[ASPDM - StdLib Distribution] | [Qonsole - Quake-like console emulator] | [LibCon - Autohotkey Console Library]
User avatar
nnnik
Posts: 4500
Joined: 30 Sep 2013, 01:01
Location: Germany

Re: MCode Tutorial (Compiled Code in AHK)

02 Oct 2013, 03:39

Well the current Problem is that Bentschi has shut down his MCode generator.
So you'll all have to install Visual C++ Express 2010 and compile your MCode that way.
Since I have no copy of the Script Bentschi use it'll take some time until I'm finished with this.
Recommends AHK Studio
User avatar
joedf
Posts: 8940
Joined: 29 Sep 2013, 17:08
Location: Canada
Contact:

Re: MCode Tutorial (Compiled Code in AHK)

02 Oct 2013, 10:26

What if I simply compile my source lets say "hello.c",
And the open it in a hex reader? And take that binary code?
Oh wait will it have like stack memory initialization that will not be needed?
"mov ax" etc. ?
Image Image Image Image Image
Windows 10 x64 Professional, Intel i5-8500, NVIDIA GTX 1060 6GB, 2x16GB Kingston FURY Beast - DDR4 3200 MHz | [About Me] | [About the AHK Foundation] | [Courses on AutoHotkey]
[ASPDM - StdLib Distribution] | [Qonsole - Quake-like console emulator] | [LibCon - Autohotkey Console Library]
User avatar
nnnik
Posts: 4500
Joined: 30 Sep 2013, 01:01
Location: Germany

Re: MCode Tutorial (Compiled Code in AHK)

02 Oct 2013, 10:36

Yeah you have a lot of stuff that you don't need.
Recommends AHK Studio
User avatar
joedf
Posts: 8940
Joined: 29 Sep 2013, 17:08
Location: Canada
Contact:

Re: MCode Tutorial (Compiled Code in AHK)

02 Oct 2013, 10:39

Ok, thats what I thought, other than that, it should be essentially the same.
well I'll search the net, for a "MCODE" compiler, If I find one, I'll post it. ;)
Image Image Image Image Image
Windows 10 x64 Professional, Intel i5-8500, NVIDIA GTX 1060 6GB, 2x16GB Kingston FURY Beast - DDR4 3200 MHz | [About Me] | [About the AHK Foundation] | [Courses on AutoHotkey]
[ASPDM - StdLib Distribution] | [Qonsole - Quake-like console emulator] | [LibCon - Autohotkey Console Library]
User avatar
nnnik
Posts: 4500
Joined: 30 Sep 2013, 01:01
Location: Germany

Re: MCode Tutorial (Compiled Code in AHK)

02 Oct 2013, 11:22

Bentschi build one from Visual C++ and an AHK file, but the Link to the AHK file is dead.
Recommends AHK Studio
User avatar
joedf
Posts: 8940
Joined: 29 Sep 2013, 17:08
Location: Canada
Contact:

Re: MCode Tutorial (Compiled Code in AHK)

02 Oct 2013, 15:44

Craaap...es! .... Yes. I said crapes :)
Image Image Image Image Image
Windows 10 x64 Professional, Intel i5-8500, NVIDIA GTX 1060 6GB, 2x16GB Kingston FURY Beast - DDR4 3200 MHz | [About Me] | [About the AHK Foundation] | [Courses on AutoHotkey]
[ASPDM - StdLib Distribution] | [Qonsole - Quake-like console emulator] | [LibCon - Autohotkey Console Library]
Alibaba
Posts: 480
Joined: 29 Sep 2013, 16:15
Location: Germany

Re: MCode Tutorial (Compiled Code in AHK)

02 Oct 2013, 19:21

nnnik wrote:Well the current Problem is that Bentschi has shut down his MCode generator.
So you'll all have to install Visual C++ Express 2010 and compile your MCode that way.
Since I have no copy of the Script Bentschi use it'll take some time until I'm finished with this.
Since when it's shut down? That's very unfortunate. :(
"Nothing is quieter than a loaded gun." - Heinrich Heine
User avatar
nnnik
Posts: 4500
Joined: 30 Sep 2013, 01:01
Location: Germany

Re: MCode Tutorial (Compiled Code in AHK)

03 Oct 2013, 02:32

The MCode generator works again.
I'll be working on the Tutorial now.
Recommends AHK Studio
User avatar
nnnik
Posts: 4500
Joined: 30 Sep 2013, 01:01
Location: Germany

Re: MCode Tutorial (Compiled Code in AHK)

03 Oct 2013, 02:39

If you have Visual Studio or Express(free) installed you can modify this script:

Code: Select all

/*! Adapted by TheGood
http://www.autohotkey.com/forum/viewtopic.php?p=364922
Last updated: August 17th, 2010
Rewrite for AHK 1.1+ Bentschi 2013
*/

#SingleInstance, off

installdir := "C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\"
;HKCU\HKEY_CURRENT_USER\Software\Microsoft\VCExpress\11.0_Config\Setup\VC
;ProductDir

if (!fileexist(vcvarsall := installdir "vcvarsall.bat"))
  exitapp, -1
opt := {opt:"x", lang:1, comp:1, enc:1, warn:1}
pcount = %0%
if (!fileexist(srcfile := AbsolutePath(%pcount%)))
  exitapp, -2
fileinfo := SplitPath(srcfile)
Loop, % pcount
{
  p := %A_Index%
  if (p="-minsize")
    opt.opt := 1
  else if (p="-maxspeed")
    opt.opt := 2
  else if (p="-noopt")
    opt.opt := "d"
  else if (p="-c")
    opt.lang := 1
  else if (p="-cpp")
    opt.lang := 2
  else if (p="-x86")
    opt.comp := 1
  else if (p="-x64")
    opt.comp := 2
  else if (p="-x86x64")
    opt.comp := 3
  else if (p="-hex")
    opt.enc := 1
  else if (p="-base64")
    opt.enc := 2
  else if (regexmatch(p, "^-warn([1-4])$", m))
    opt.warn := m1
}

bat := "@echo off`r`n"
outfiles := {}
for k,v in {1:"x86", 2:"x86_amd64"}
{
  if (opt.comp&k)
  {
    outfiles[v] := fileinfo.dir fileinfo.filename "_" v ".cod"
    FileDelete, % fileinfo.dir fileinfo.filename "_" v ".cod"
    bat .= "echo COMPILER: " v "`r`n"
    bat .= "echo OPTIONS: " ((opt.lang=1) ? "/TC" : "/TP") " /c /FAc /Facode.cod /O" opt.opt " /W" opt.warn "`r`n"
    bat .= "call """ vcvarsall """ " v "`r`n"
    bat .= "cl " ((opt.lang=1) ? "/TC" : "/TP") " /c /FAc /Facode.cod /O" opt.opt " /W" opt.warn " """ srcfile """`r`n"
    bat .= "echo ERRORLEVEL: %ERRORLEVEL%`r`n"
    bat .= "echo STRIP BEGIN MOVE`r`n"
    bat .= "move /Y """ fileinfo.dir "code.cod"" """ fileinfo.dir fileinfo.filename "_" v ".cod""`r`n"
    bat .= "echo STRIP END MOVE`r`n"
    bat .= "echo ---`r`n"
  }
}
SetWorkingDir, % fileinfo.dir
batfile := fileinfo.dir fileinfo.filename ".bat"
logfile := fileinfo.dir fileinfo.filename ".log"
FileDelete, % batfile
FileAppend, % bat, % batfile
FileDelete, % logfile
RunWait, % batfile " >> " logfile,, Hide
FileDelete, % fileinfo.dir "code.cod"
err := 0
Loop, read, % logfile
{
  if (regexmatch(A_LoopReadLine, "ERRORLEVEL: (.*)", m))
    err += m1
}
if (err)
{
  CleanLog()
  exitapp, -3
}
functions := {}
log := ""
for k,v in outfiles
{
  Loop, 10
  {
    if (!fileexist(v))
    {
      if (A_Index=10)
      {
        CleanLog()
        exitapp, -4
      }
      sleep, 200
    }
    else
      break
  }
  FileRead, code, % v
  f := {}
  while (RegExMatch(code, "ms)_?([^\s]+?)\s+PROC\s*(.*?)\s*[^\s]+\s*ENDP(.*)", m))
  {
    f.name := regexreplace(m1, "\?(.*?)@@.*", "$1")
    f.code := regexreplace(regexreplace(m2, "m)^[^\t]*\t([^\t\r\n]+)[^\r\n]*", "$1"), "\s+")
    log .= "Extract function: " f.name " [" k "] [" (strlen(f.code)//2) " bytes]`r`n"
    if (opt.enc!=1)
    {
      binlength := StringToBinary(4, f.code, bin)
      if (opt.enc=2)
        f.code := BinaryToString(1, &bin, binlength)
    }
		functions[f.name, k] := f.code
    code := m3
  }
}
out := ""
width := 100
for name,info in functions
{
  c := name " := MCode("""
  d := opt.enc
  for comp,code in info
  {
    if (comp="x86")
      d .= ",x86:"
    else if (comp="x86_amd64")
      d .= ",x64:"
    else
      continue
    d .= code
  }
  if (strlen(c)+strlen(d)+2<width)
    out .= c d """)`r`n"
  else
  {
    out .= c "`r`n(LTrim Join`r`n"
    while (strlen(d)>width)
    {
      out .= substr(d, 1, width) "`r`n"
      d := substr(d, width+1)
    }
    if (d)
      out .= d "`r`n"
    out .= ")"")`r`n"
  }
}
outfile := fileinfo.dir fileinfo.filename ".mcode"
FileDelete, % outfile
FileAppend, % out, % outfile
FileAppend, % log, % logfile
CleanLog()
ExitApp, 0

CleanLog()
{
  global
  FileRead, xlog, % logfile
  FileDelete, % logfile
  xlog := regexreplace(xlog, "im)^" regexreplace(fileinfo.filename ((fileinfo.ext) ? "." fileinfo.ext : ""), "(\\|[.*?+[{|()^$])", "\$1") "\s*")
  xlog := regexreplace(xlog, "i)" regexreplace(srcfile, "(\\|[.*?+[{|()^$])", "\$1"), "source")
  xlog := regexreplace(xlog, "s)STRIP BEGIN MOVE.*?STRIP END MOVE[\r\n]+")
  FileAppend, % xlog, % logfile
}
SplitPath(filename)
{
  if (regexmatch(filename, "^(.*\\)?([^.\\]*)\.?([^\\]*)$", m))
    return {filename:m2, dir:AbsolutePath(m1), ext:m3}
}
AbsolutePath(filename)
{
  if (!DllCall("shlwapi\PathIsRelativeW", "wstr", filename))
    return filename
  s := DllCall("GetFullPathNameW", "wstr", filename, "uint", 0, "ptr", 0, "ptr", 0)
  VarSetCapacity(buf, s*2, 0)
  DllCall("GetFullPathNameW", "wstr", filename, "uint", s, "ptr", &buf, "ptr", 0)
  return StrGet(&buf, s, "utf-16")
}
StringToBinary(type, str, byref buf)
{
  if (!DllCall("crypt32\CryptStringToBinary", "str", str, "uint", 0, "uint", type, "ptr", 0, "uint*", sout, "ptr", 0, "ptr", 0))
    return ""
  VarSetCapacity(buf, sout, 0)
  if (!DllCall("crypt32\CryptStringToBinary", "str", str, "uint", 0, "uint", type, "ptr", &buf, "uint*", sout, "ptr", 0, "ptr", 0))
    return ""
  return sout
}
BinaryToString(type, ptr, size)
{
  if (!DllCall("crypt32\CryptBinaryToStringW", "ptr", ptr, "uint", size, "uint", type, "ptr", 0, "uint*", sout))
    return
  VarSetCapacity(bout, sout<<1, 0)
  if (!DllCall("crypt32\CryptBinaryToStringW", "ptr", ptr, "uint", size, "uint", type, "ptr", &bout, "uint*", sout))
    return
  return regexreplace(StrGet(&bout, sout<<1, "utf-16"), "\s+")
}
Recommends AHK Studio
User avatar
nnnik
Posts: 4500
Joined: 30 Sep 2013, 01:01
Location: Germany

Re: MCode Tutorial (Compiled Code in AHK)

03 Oct 2013, 03:26

I have added another part I think I'll finish it today
Recommends AHK Studio
User avatar
joedf
Posts: 8940
Joined: 29 Sep 2013, 17:08
Location: Canada
Contact:

Re: MCode Tutorial (Compiled Code in AHK)

03 Oct 2013, 04:08

Cool! :D hooray!
Image Image Image Image Image
Windows 10 x64 Professional, Intel i5-8500, NVIDIA GTX 1060 6GB, 2x16GB Kingston FURY Beast - DDR4 3200 MHz | [About Me] | [About the AHK Foundation] | [Courses on AutoHotkey]
[ASPDM - StdLib Distribution] | [Qonsole - Quake-like console emulator] | [LibCon - Autohotkey Console Library]

Return to “Tutorials (v1)”

Who is online

Users browsing this forum: No registered users and 33 guests