Posted: 24th Jul 2021 18:21
I had a request for some pointers on making plugins for AppGameKit with PureBasic so I thought I would post a detailed 'How To' guide

PureBasic is a powerful language that has a long history and a bright future, v6 will see native inline C support and a new C backend and compiler, this will open many doors when combined with AppGameKit, it really is a very easy to use and powerful union.

The Basics:

The first thing we need to do is setup some folders:

navigate to your AppGameKit install dir and find the Plugins folder...

Classic: C:\Program Files (x86)\The Game Creators\AGK2\Tier 1\Compiler\Plugins
Studio: C:\Program Files (x86)\The Game Creators\AppGameKit Studio\Plugins

Create a folder, name it myPlugin (yes this is basic, humour me for now, aye), Inside this folder create a text file, name it 'Commands.txt' and paste the following text

+ Code Snippet
#CommandName,ReturnType,ParameterTypes,Windows,Linux,Mac,Android,iOS,Windows64
MyFunction,0,0,MyFunction,0,0,0,0,0


we will worry about what this content means in a bit, lets get something working first.

Create the PB project

Now, Open PureBasic (x86 - 32 Bit, we will deal with 64 bit later) and create a project, again call it myPlugin and save it where ever you save your PB stuff,

Use the project creation dialog to add a file, call it main.pb and click the 'Create Project' button, this will open the project overview tab.

Now we need to set the compile targets, in the 'Project Targets' list, right click 'Default Target' and click 'Edit Target', this opens the project compiler options.

Set executable format, Shared DLL

Set the input file to the main.pb file that was created with the project and set the output executable to :

Classic: C:\Program Files (x86)\The Game Creators\AGK2\Tier 1\Compiler\Plugins\myPlugin\Windows.dll

Studio: C:\Program Files (x86)\The Game Creators\AppGameKit Studio\Plugins\myPlugin\Windows.dll

you can add both if you want to compile for both systems but set your preferred as default (I set classic) (checkbox at bottom of compile targets list) and set the other 'Studio' Enable in build all targets, now we can compile our plugin for both Classic and Studio at a single button click, handy stuff

Click OK in the dialog, you are now greeted with an empty code page, first lets test our compile targets to make sure we got it right, go to the main menu, click Compiler>>Build All Targets, a dialog will appear and tell you what's going on, when its done you should now have a new DLL in the plugin folder (you will need to launch PureBasic with admin rights to compile to the Program Files folder)

if everything is good, we can add some code, if something went wrong, check each step and try again.

Add some code

Now we will add the a basic function:

When AppGameKit loads the plugin it looks for a function 'ReceiveAGKPtr', this function is called by the runtime and sends the full set of function pointers to the plugin, we will setup this function but not use it yet, this function must exist for the plugin to work so we set this us with a C prototype

+ Code Snippet
PrototypeC _GetAGKFunction(Function.p-ascii)

ProcedureCDLL ReceiveAGKPtr(*GetFunction)

 GetAGKFunction._GetAGKFunction=*GetFunction

EndProcedure


we will come back to this later.

Now lets add a basic function, we use the function name we put in the Commands.txt

+ Code Snippet
ProcedureCDLL MyFunction()
  
    MessageRequester("PureBasic", "Hello from a PB plugin", #PB_MessageRequester_Info)
  
EndProcedure


And once again, Menu>>Compile>>Build all Targets.


Now open AppGameKit, any flavor if you added both compile targets, or just the one you added and create a new project, call it Example - Plugin

Add:

#Import_Plugin myPlugin
or
#Import_Plugin myPlugin as mp

In the first case we call our functions with the syntax myPlugin.MyFunction, in the second case we use an alias 'mp' so the call becomes mp.MyFunction

+ Code Snippet
// show all errors
SetErrorMode(2)

#Import_Plugin myPlugin

// set window properties
SetWindowTitle( "Plugin - Example" )
SetWindowSize( 1024, 768, 0 )
SetWindowAllowResize( 1 ) // allow the user to resize the window

// set display properties
SetVirtualResolution( 1024, 768 ) // doesn't have to match the window
SetOrientationAllowed( 1, 1, 1, 1 ) // allow both portrait and landscape on mobile devices
SetSyncRate( 30, 0 ) // 30fps instead of 60 to save battery
SetScissor( 0,0,0,0 ) // use the maximum available screen space, no black borders
UseNewDefaultFonts( 1 ) // since version 2.0.22 we can use nicer default fonts

myPlugin.MyFunction()

do
    Print( ScreenFPS() )
    Sync()
loop


or

+ Code Snippet
// Project: Plugin - Example 
// Created: 2021-07-24

// show all errors
SetErrorMode(2)

#Import_Plugin myPlugin as mp

// set window properties
SetWindowTitle( "Plugin - Example" )
SetWindowSize( 1024, 768, 0 )
SetWindowAllowResize( 1 ) // allow the user to resize the window

// set display properties
SetVirtualResolution( 1024, 768 ) // doesn't have to match the window
SetOrientationAllowed( 1, 1, 1, 1 ) // allow both portrait and landscape on mobile devices
SetSyncRate( 30, 0 ) // 30fps instead of 60 to save battery
SetScissor( 0,0,0,0 ) // use the maximum available screen space, no black borders
UseNewDefaultFonts( 1 ) // since version 2.0.22 we can use nicer default fonts

mp.MyFunction()

do
    Print( ScreenFPS() )
    Sync()
loop


Now hit run, you should see a dialog box with a message "Hello from a PB plugin"

Congrats you have made your first Plugin.

Arguments

Ok so now lets send in some arguments, go back to the Commands.txt file


Change : MyFunction,0,0,MyFunction,0,0,0,0,0

To MyFunction,0,S,MyFunction,0,0,0,0,0

This tells AppGameKit we want to send 1 string argument to the pkugin (S=String, I= Integer, F = Float)

Sidenote:
If we had 2 string and an integer we would stack them like so: MyFunction,0,SSI,MyFunction,0,0,0,0,0

Now go back to the PureBasic file and change the function to:

+ Code Snippet
ProcedureCDLL MyFunction(message.s)
  
    MessageRequester("PureBasic", message, #PB_MessageRequester_Info)
  
EndProcedure


and recompile, now to the AppGameKit project and change

mp.MyFunction()

to

mp.MyFunction("Hello from AGK")

and click run ... you should see a dialog box with jibberish, see PureBasic is Unicode, AppGameKit is not so we need to let PB know its Ascii text coming in, we do this with a helper function and a memory peek

+ Code Snippet
Procedure.s S(in.s)
  ProcedureReturn PeekS(@in, -1, #PB_Ascii)
EndProcedure


and wrap the text in our function

+ Code Snippet
ProcedureCDLL MyFunction(message.s)
  
    MessageRequester("PureBasic", S(message), #PB_MessageRequester_Info)
  
EndProcedure


compile, go back to AppGameKit and run,, you should now see the message "Hello from AGK"

As we have a message requester with 2 string arguments, let add them both


Commands.txt
MyFunction,0,SS,MyFunction,0,0,0,0,0

PureBasic:
ProcedureCDLL MyFunction(title.s, message.s)

MessageRequester(S(title), S(message), #PB_MessageRequester_Info)

EndProcedure

AGK:
mp.MyFunction("AppGameKit", "Hello from AGK")

Now see if you can add the integer icon argument by yourself


Return Values

Now lets return a value from the plugin, we tell AppGameKit this function returns a value in the Commands.txt

MyFunction,I,SS,MyFunction,0,0,0,0,0

for this case we return an integer value

Now in PureBasic, lets change the message requester to a Yes/No dialog and return its value

+ Code Snippet
ProcedureCDLL MyFunction(title.s, message.s)
  
    ProcedureReturn MessageRequester(S(title), S(message), #PB_MessageRequester_YesNo)
  
EndProcedure


and in AppGameKit we check the value, but lets add a button so we can check each case

+ Code Snippet
// Project: Plugin - Example 
// Created: 2021-07-24

// show all errors
SetErrorMode(2)

#Import_Plugin myPlugin as mp

// set window properties
SetWindowTitle( "Plugin - Example" )
SetWindowSize( 1024, 768, 0 )
SetWindowAllowResize( 1 ) // allow the user to resize the window

// set display properties
SetVirtualResolution( 1024, 768 ) // doesn't have to match the window
SetOrientationAllowed( 1, 1, 1, 1 ) // allow both portrait and landscape on mobile devices
SetSyncRate( 30, 0 ) // 30fps instead of 60 to save battery
SetScissor( 0,0,0,0 ) // use the maximum available screen space, no black borders
UseNewDefaultFonts( 1 ) // since version 2.0.22 we can use nicer default fonts


AddVirtualButton(1, 100, 100, 50)

do
	
	if GetVirtualButtonPressed(1)
		yay=mp.MyFunction("AppGameKit", "Would you like to learn more?")
	endif
	
	if yay = 6
		Print( "You clicked Yes" )
	elseif yay = 7
		Print( "You clicked No" )
	else
		Print( "Click the button" )
	endif
	
    Print( ScreenFPS() )
    Sync()
loop


And there you have it, sending and receiving data to/from a plugin


Returning Strings:

Floats and Integers can be returned directly but we have to handle strings a little different.

For this we head back into PureBasic and the ReceiveAGKPtr function, we need to import some string functions, for this we use the _GetAGKFunction prototype we setup earlier

SideNote, there is a full function wrapper attached to this post, I am showing it this way so you understand what's going on in the code.

+ Code Snippet
PrototypeC _GetAGKFunction(Function.p-ascii)

; store the prototypes in a global
Global agkCreateString : PrototypeC.i AGKCommand0(size.i)
Global agkDeleteString : PrototypeC AGKCommand1(ptr.i)

ProcedureCDLL ReceiveAGKPtr(*GetFunction)

  GetAGKFunction._GetAGKFunction=*GetFunction
  
  ; ask AGK for pointers to the following functions
  agkCreateString.AGKCommand0=GetAGKFunction("CREATESTRING_S_L")
  agkDeleteString.AGKCommand1=GetAGKFunction("DELETESTRING_0_S")
  
EndProcedure


Change Commands.txt from Intger return to String

MyFunction,S,SS,MyFunction,0,0,0,0,0



In PB, we need to create a string pointer to send to AppGameKit, for this we use a handy helper function to poke the memory

+ Code Snippet
Procedure _agkMakeString(String.s)
  Size=StringByteLength(String)
  If Size>0
   *Ptr=agkCreateString(Size+1)
   If *Ptr
     PokeS(*Ptr, String, Size, #PB_Ascii)
     ProcedureReturn *Ptr
   EndIf
 EndIf
EndProcedure



And change our function to:

+ Code Snippet
ProcedureCDLL MyFunction(title.s, message.s)
  
  Protected RetString.s=""
  
  Select MessageRequester(S(title), S(message), #PB_MessageRequester_YesNo)
    Case #PB_MessageRequester_Yes
      RetString="The plugin reports YES" 
    Case #PB_MessageRequester_No
      RetString="The plugin reports NO" 
  EndSelect
  
  ProcedureReturn _agkMakeString(RetString)
  
EndProcedure



And change the AppGameKit code to:

+ Code Snippet
// Project: Plugin - Example 
// Created: 2021-07-24

// show all errors
SetErrorMode(2)

#Import_Plugin myPlugin as mp

// set window properties
SetWindowTitle( "Plugin - Example" )
SetWindowSize( 1024, 768, 0 )
SetWindowAllowResize( 1 ) // allow the user to resize the window

// set display properties
SetVirtualResolution( 1024, 768 ) // doesn't have to match the window
SetOrientationAllowed( 1, 1, 1, 1 ) // allow both portrait and landscape on mobile devices
SetSyncRate( 30, 0 ) // 30fps instead of 60 to save battery
SetScissor( 0,0,0,0 ) // use the maximum available screen space, no black borders
UseNewDefaultFonts( 1 ) // since version 2.0.22 we can use nicer default fonts


AddVirtualButton(1, 100, 100, 50)

yay$ = "Click the Button"
do
	
	if GetVirtualButtonPressed(1)
		yay$=mp.MyFunction("AppGameKit", "Would you like to learn more?")
	endif
	
	Print( yay$ )
	
    Print( ScreenFPS() )
    Sync()
loop


run the project, now the string is returned from the plugin,

There is one important factor to consider when using agkCreateString, string pointers must be deleted with agkDeleteString but we have the situation where we return the pointer and no longer have access to it, in this case I THINK! AppGameKit handles the deletion of the pointer, I ran some tests with a global pointer and while the pointer refers to valid memory the contents seem to be deleted, there is no documentation to support or disprove this so some input here would help @TGC


Errors:

If you want to prompt an error message from within your plugin we use another helper function

+ Code Snippet
Procedure agkPushError(String.s)
  Size=StringByteLength(String)
  If Size>0
   *Ptr=agkCreateString(Size+1)
   If *Ptr
     PokeS(*Ptr, String, Size, #PB_Ascii)
      agkPluginError(*Ptr)
      agkDeleteString(*Ptr)
    EndIf
  EndIf
EndProcedure


and we muse add the agkPluginError to oue ReceiveAGKPtr function

+ Code Snippet
PrototypeC _GetAGKFunction(Function.p-ascii)

; store the prototypes in a global
Global agkCreateString : PrototypeC.i AGKCommand0(size.i)
Global agkDeleteString : PrototypeC AGKCommand1(ptr.i)
Global agkPluginError : PrototypeC AGKCommand895(szErr.i)

ProcedureCDLL ReceiveAGKPtr(*GetFunction)

  GetAGKFunction._GetAGKFunction=*GetFunction
  
  ; ask AGK for pointers to the following functions
  agkCreateString.AGKCommand0=GetAGKFunction("CREATESTRING_S_L")
  agkDeleteString.AGKCommand1=GetAGKFunction("DELETESTRING_0_S")
  agkPluginError.AGKCommand895=GetAGKFunction("PLUGINERROR_0_S")
  
EndProcedure



And add an error to our function

+ Code Snippet
ProcedureCDLL MyFunction(title.s, message.s)
  
  Protected RetString.s=""
  
  If S(title) = "AGK"
    agkPushError("You can not use AGK as a title!")
  EndIf  
    
  Select MessageRequester(S(title), S(message), #PB_MessageRequester_YesNo)
    Case #PB_MessageRequester_Yes
      RetString="The plugin reports YES" 
    Case #PB_MessageRequester_No
      RetString="The plugin reports NO" 
  EndSelect
  
  ProcedureReturn _agkMakeString(RetString)
  
EndProcedure


and change the call in agk to prompt the error

+ Code Snippet
	if GetVirtualButtonPressed(1)
		yay$=mp.MyFunction("AGK", "Would you like to learn more?")
	endif


This simply shows the AppGameKit message box, you could of course just use your own feed back system but this function is here for convince.

And that is the basic setup of PureBasic for plugin production, the full wrap of ReceiveAGKPtr exposing all AppGameKit functions is attached below, the overloaded calls have been commented out and not all functions are tested but should work, you could literally write your entire game in PureBasic and include it and run with a single function call in AGK.

Next, Advanced usage
Posted: 24th Jul 2021 18:21
Window Handle

Once we get the AppGameKit window handle we can do all sorts of intrestng stuff, lets start by adding a function to our PureBasic code.

+ Code Snippet
Global AGK_HWND.i

ProcedureCDLL RegisterWindow(window_title.s)
  
  Protected hwnd = FindWindow_(#Null, S(window_title))
  If IsWindow_(hwnd)
    AGK_HWND = hwnd
    ProcedureReturn #True
  Else
    agkPushError("AGK window not found, please check title.")
    ProcedureReturn #False
  EndIf
EndProcedure


Compile the plugin

Add to Commands.txt

RegisterWindow,I,S,RegisterWindow,0,0,0,0,0

and change AppGameKit code

+ Code Snippet
registerd = mp.RegisterWindow("Plugin - Example")

do
	Print( "Registerd = " + Str(registerd) )
	
    Print( ScreenFPS() )
    Sync()
loop


So now we can send the title to the plugin and store its handle and get an error report if the title can not be found, this is a staple of most of my plugins, heres why.

Callbacks

Callback functions are an intregrial part of the windows messaging system, so lets add one to the AppGameKit window, under the hood AppGameKit already has a callback in Core.cpp, we will get a pointer to this so we can call it from our callback when we are done, we must do this or bad things will happen.


This is the very least your callback should contain

+ Code Snippet
Procedure AGKCallback(hwnd, uMsg, wParam, lParam)

  Select uMsg
	; MS advise to remove props from windows so we do that here
    Case  #WM_DESTROY
      ; remove the prop when the window closes
      RemoveProp_(hwnd, "oldProc") 
  EndSelect
  
  ; call the old window callback
  ProcedureReturn CallWindowProc_(GetProp_(hwnd,"oldProc"), hwnd, uMsg, wParam, lParam)
  
EndProcedure


Add a function to set the callback

+ Code Snippet
Procedure SetAGKCallback(hwnd, *callback)
  
  Protected oldProc
  
  ; get the old callback and store it in a window prop
  oldProc = GetWindowLongPtr_(hwnd, #GWLP_WNDPROC)
  SetProp_(hwnd, "oldProc", oldProc)
  
  ; set the new callback
  SetWindowLongPtr_(hwnd, #GWLP_WNDPROC, *callback)
  
EndProcedure


and change the RegisterWindow function to set the callback

+ Code Snippet
ProcedureCDLL RegisterWindow(window_title.s)
  
  Protected hwnd = FindWindow_(#Null, S(window_title))
  If IsWindow_(hwnd)
    AGK_HWND = hwnd
    SetAGKCallback(AGK_HWND, @AGKCallback())
    ProcedureReturn #True
  Else
    agkPushError("AGK window not found, please check title.")
    ProcedureReturn #False
  EndIf
EndProcedure


Compile the plugin and run the AppGameKit project to ensure everythibg is working, nothing should change to be honest it should just act normally as we have not change any behaviour we only added a callback function, now we can have some fun ...

Lets intercept the window close button

Add a case to the select statment in the AGKCallback:

+ Code Snippet
    Case  #WM_CLOSE
      ProcedureReturn #True


Compile the plugin and run the project, dang the window wont close, no panic hit the run button again the it will kill the runtime, remove or comment the return and now it closes.

If your project requires users to save data and they exit without saving there is no way to warn them, if we intercept the close message like this we can check if the user data needs saving and inform the user with the obligtory "Do you want to save" message, a simple but effective use of a callback.

EG:

+ Code Snippet
    Case  #WM_CLOSE
      If bUserDataChange 
        ProcedureReturn #True
      EndIf


Limit the window size

+ Code Snippet
    Case #WM_GETMINMAXINFO:
    
      *mmi.MINMAXINFO
      *mmi = lParam
      *mmi\ptMinTrackSize\x = 800;
      *mmi\ptMinTrackSize\y = 600;
      *mmi\ptMaxTrackSize\x = 1024;
      *mmi\ptMaxTrackSize\y = 768;


If you want your window to be resibile but within a range.

There are a whole host of things we can do with the window from here, depennds on your needs.


Embedding AppGameKit Window

Now have full access to the plugin system and a window callback in place we are ready to take full control of the AppGameKit window by embedding it into a PureBasic UI but before we do lets tidy up our project a bit

In PureBasic, add the wrapper file to the project folder and add it to the project (Project Tab>>Project Options>>Project Files>>Open and start the main.pb with the below code

+ Code Snippet
IncludeFile "AGKPluginWrapper.pbi"

Procedure.s  S(in.s)
  ProcedureReturn PeekS(@in, -1, #PB_Ascii)
EndProcedure

Global AGK_HWND.i

Procedure AGKCallback(hwnd, uMsg, wParam, lParam)

  Select uMsg
    Case  #WM_DESTROY
      ; remove the prop when the window closes
      RemoveProp_(hwnd, "oldProc")

  EndSelect
  
  ; call the old window callback
  ProcedureReturn CallWindowProc_(GetProp_(hwnd,"oldProc"), hwnd, uMsg, wParam, lParam)
  
EndProcedure

Procedure SetAGKCallback(hwnd, *callback)
  
  Protected oldProc
  
  ; get the old callback and store it in a window prop
  oldProc = GetWindowLongPtr_(hwnd, #GWLP_WNDPROC)
  SetProp_(hwnd, "oldProc", oldProc)
  
  ; set the new callback
  SetWindowLongPtr_(hwnd, #GWLP_WNDPROC, *callback)
  
EndProcedure

ProcedureCDLL RegisterWindow(window_title.s)
  
  Protected hwnd = FindWindow_(#Null, S(window_title))
  If IsWindow_(hwnd)
    AGK_HWND = hwnd
    SetAGKCallback(AGK_HWND, @AGKCallback())
    ProcedureReturn #True
  Else
    agkPushError("AGK window not found, please check title.")
    ProcedureReturn #False
  EndIf
EndProcedure


And Commands.txt

#CommandName,ReturnType,ParameterTypes,Windows,Linux,Mac,Android,iOS,Windows64
RegisterWindow,I,S,RegisterWindow,0,0,0,0,0

And AppGameKit code

+ Code Snippet
// Project: Plugin - Example 
// Created: 2021-07-24

// show all errors
SetErrorMode(2)

#Import_Plugin myPlugin as mp

// set window properties
SetWindowTitle( "Plugin - Example" )
SetWindowSize( 1024, 768, 0 )
SetWindowAllowResize( 1 ) // allow the user to resize the window

// set display properties
SetVirtualResolution( 1024, 768 ) // doesn't have to match the window
SetOrientationAllowed( 1, 1, 1, 1 ) // allow both portrait and landscape on mobile devices
SetSyncRate( 30, 0 ) // 30fps instead of 60 to save battery
SetScissor( 0,0,0,0 ) // use the maximum available screen space, no black borders
UseNewDefaultFonts( 1 ) // since version 2.0.22 we can use nicer default fonts

registerd = mp.RegisterWindow("Plugin - Example")
do
    Print( ScreenFPS() )
    Sync()
loop


So now we have a clean set of project to work with and the full AppGameKit API to play with, lets build a basic form, add a container and a button

+ Code Snippet
Procedure OpenUI()
  
  OpenWindow(#Window_Main, x, y, 862, 430, "", #PB_Window_SystemMenu | #PB_Window_MinimizeGadget | #PB_Window_MaximizeGadget | #PB_Window_SizeGadget | #PB_Window_WindowCentered)
  
  ContainerGadget(#Container_AGK, 120, 14, 560, 406, #PB_Container_Flat)
  CloseGadgetList()
  
  ButtonGadget(#Button_AddSprite, 10, 14, 100, 25, "Add Sprite")
  
  BindEvent(#PB_Event_CloseWindow, @CloseUI(), #Window_Main)
  BindGadgetEvent(#Button_AddSprite, @AddSprite(), #PB_EventType_LeftClick)
  
EndProcedure


and a helper function to embed the agk window into our PB window

+ Code Snippet
Procedure EmbedAGK()
  
  If IsWindow_(AGK_HWND)

    ; set the window icon from the agk window
    agk_icon=GetClassLongPtr_(AGK_HWND, #GCL_HICON)
    SendMessage_(WindowID(#Window_Main), #WM_SETICON, 0, agk_icon)
     
    ; tell the container to accept child objects
    SetWindowLong_(GadgetID(#Container_AGK), #GWL_STYLE, GetWindowLong_(GadgetID(#Container_AGK), #GWL_STYLE) | #WS_CLIPCHILDREN)
     
    ; remove agk window styles and set it as child
    lStyle = GetWindowLong_(AGK_HWND, #GWL_STYLE)
    lStyle&~(#WS_CAPTION | #WS_THICKFRAME | #WS_MINIMIZE | #WS_MAXIMIZE | #WS_SYSMENU | #WS_POPUP)
    SetWindowLong_(AGK_HWND, #GWL_STYLE, lStyle | #WS_CHILD) 
     
    ; remove agk window extended styles
    lExStyle = GetWindowLong_(AGK_HWND, #GWL_EXSTYLE)
    lExStyle&~(#WS_EX_DLGMODALFRAME | #WS_EX_CLIENTEDGE | #WS_EX_STATICEDGE)
    SetWindowLong_(AGK_HWND, #GWL_EXSTYLE, lExStyle)
         
    ;; now set the container as agk's parent
    SetParent_(AGK_HWND, GadgetID(#Container_AGK))
 
    SetWindowPos_(AGK_HWND, #Null, 0, 0, GadgetWidth(#Container_AGK), GadgetHeight(#Container_AGK), #SWP_SHOWWINDOW)
    
  EndIf
  
EndProcedure




And call teese functions from RegisterWindow

+ Code Snippet
ProcedureCDLL RegisterWindow(window_title.s)
  
  Protected hwnd = FindWindow_(#Null, S(window_title))
  If IsWindow_(hwnd)
    AGK_HWND = hwnd
    SetAGKCallback(AGK_HWND, @AGKCallback())
    OpenUI()
    EmbedAGK()
    ProcedureReturn #True
  Else
    agkPushError("AGK window not found, please check title.")
    ProcedureReturn #False
  EndIf
EndProcedure


We need to make sure our application can close so add these

+ Code Snippet
ProcedureCDLL UIClose()
  Shared App_CLOSE
  ProcedureReturn App_CLOSE
EndProcedure

Procedure CloseUI()
  Shared App_CLOSE
  App_CLOSE=#True
EndProcedure


and finally add a function for the Add Sprite button

+ Code Snippet
Procedure AddSprite()
  
  spr_id=agkCreateSprite(0)
  agkSetSpritePosition(spr_id, 200, 100)
  agkSetSpriteSize(spr_id, 50, 50)
  
EndProcedure


Now compile the plugin

in Commands.txt add:
UIClose,I,0,UIClose,0,0,0,0,0

and in agk, change the loop code to

+ Code Snippet
registerd = mp.RegisterWindow("Plugin - Example")
Repeat
    Print( ScreenFPS() )
    Sync()
Until mp.UIClose()


And finally run the project, AppGameKit is now effectivally a PureBasic gadget but unlike PB gadgets we can not bind events to AppGameKit so we need a update function

Add to PB
+ Code Snippet
ProcedureCDLL Update()
    ; your update code here

EndProcedure


Add to Commands.txt
Update,0,0,Update,0,0,0,0,0

Set to AGK
+ Code Snippet
registerd = mp.RegisterWindow("Plugin - Example")
Repeat
	mp.Update()
    Print( ScreenFPS() )
    Sync()
Until mp.UIClose()


Now we have an update function called at the frame rate set by agk

Now there was some issue I encouterd with mouse and keyboard events not making it to the AppGameKit window because of the parental rearranging so that is why I left the callback because this will be needed to pipe them correctly, I will append that info to this thread when I have written it I cant remember what functions were affected so I will have to test

But that for the basic part is pretty much it, you build your PureBasic UI as normal but use BindEvent, BindGadget/MenuEvent, if you absoluly need an event loop for gadgets then attach a callback to the UI window and use that but DO NOT add a repeat loop inside the plugin.

Have fun.
Posted: 24th Jul 2021 22:00
Absolutely wonderful! Thank you for your instruction.
Posted: 25th Jul 2021 12:12
Does the PureBasics license allows wrapping its API inside DLLs? I remember a case back then that this wasn't allowed someone was creating a 3d engine with purebasic, basically wrapping its api
Posted: 25th Jul 2021 12:27
This was both informative and fascinating. Thank you so much for posting this!
Posted: 25th Jul 2021 15:22
Thank you for your instruction.


No Problem Mat, did you get it up and running?

Does the PureBasics license allows wrapping its API inside DLLs?


You are right, you can not just wrap up PB's commands and export them, but there is the caveat that PB wraps the WinAPI so you can export as much of that as you like as PB can not take ownership of it, so as long as you include as much API as possible, like I usually do then its well within the EULA.

at the end of the day you can not make a DLL without exporting some of PB' functionally, what Fred asks is simply don't wrap the API and call it your own, which is fair enough.

This was both informative and fascinating.


Yea I am not exactly cut out for this tutorial stuff but had to recreate most of the code so I just documented the steps I took, I know its a bit messy but that's my work flow lol

I would be happy to let someone a bit more literate than myself reformat the entire thing
Posted: 25th Jul 2021 17:36
I am not sure how stable this is, it would need more testing, but ....

add this function to PB
+ Code Snippet
Procedure ThreadFunc(i)
  
  x=0
  Repeat
    x=x+1
    spr_id=agkCreateSprite(0)
    agkSetSpritePosition(spr_id, Random(500), Random(300))
    agkSetSpriteSize(spr_id, Random(50, 10), Random(50, 10))
    agkSetSpriteColor(spr_id, Random(255), Random(255), Random(255), Random(255, 100))
    
    Delay(1)
  Until x = 100000
 
EndProcedure


and start the thread in the OpenUI function

+ Code Snippet
Procedure OpenUI()
  
  OpenWindow(#Window_Main, x, y, 862, 430, "", #PB_Window_SystemMenu | #PB_Window_MinimizeGadget | #PB_Window_MaximizeGadget | #PB_Window_SizeGadget | #PB_Window_WindowCentered)
  
  CreateThread(@ThreadFunc(), #Null)



and in AppGameKit set:
+ Code Snippet
registerd = mp.RegisterWindow("Plugin - Example")
spr_id=CreateSprite(0)
SetSpritePosition(spr_id, 100, 100)
SetSpriteSize(spr_id, 50, 50)
SetSpriteColor(spr_id, 255, 0, 0, 255)
SetSpriteDepth(spr_id, 0)
angle=0  
Repeat
	mp.Update()
	inc angle, 1
	SetSpriteAngle(spr_id, angle)
    Print( ScreenFPS() )
    Sync()
Until mp.UIClose()


Threaded support, the thread is creating 100,000 sprites in a loop while AppGameKit is spinning the red sprite.

OpenGL is single threaded, how does this even work?
Posted: 25th Jul 2021 22:38
Right, as mentioned the keyboard events do not make it to agk because of the parental rearrangement so we have to pipe the events.

in the OpenUI function, under the OpenWindow, put: SetWindowCallback

+ Code Snippet
  OpenWindow(#Window_Main, x, y, 862, 430, "", #PB_Window_SystemMenu | #PB_Window_MinimizeGadget | #PB_Window_MaximizeGadget | #PB_Window_SizeGadget | #PB_Window_WindowCentered)
  SetWindowCallback(@UICallback(), #Window_Main)



and add this callback function

+ Code Snippet
Procedure UICallback(hWnd, uMsg, wParam, lParam)
  
  Select uMsg
    Case #WM_KEYDOWN
      SendMessage_(AGK_HWND, #WM_KEYDOWN, wParam, lParam)
    Case #WM_KEYUP
      SendMessage_(AGK_HWND, #WM_KEYUP, wParam, lParam)
  EndSelect
  
  ProcedureReturn #PB_ProcessPureBasicEvents
  
EndProcedure  


compile and run, the keyboard events are now piped correctly to the agk window so the default callback can take care of them
Posted: 25th Jul 2021 23:01
This is definitely information that belongs in a wiki or at the very least, in a sticky post. This is a great way to circumvent some of AGK's limitation especially when it comes to interacting with the OS directly...well at least Windows.
Posted: 26th Jul 2021 0:25
Well, PureBasic is cross platform so this system can be applied to Linux and MacOS but the windowing code would need to be change (any function with an underscore at the end) and I don't use either so that would take someone else to implement but the wrapper and base code is ready to compile on any desktop platform.

It is a great way to circumvent some of AGK's limitation without the complexity of C++ and 7GB overhead of VS!!
Posted: 26th Jul 2021 14:29
This is the source for my CL plugin, I am posting this so you can see the usefulness of the plugin system and window callbacks.

PB Source
+ Code Snippet
IncludeFile "AGKPlugin.pbi"

Global agkHWND.i

Macro S(str)
  PeekS(@str, StringByteLength(str), #PB_Ascii)
EndMacro

Structure DROP_INFO
  file.s
EndStructure

Global NewList drop.DROP_INFO()

Procedure agkWndProc(hwnd, uMsg, wParam, lParam)
  
  Define *oldProc=GetProp_(hwnd, "oldProc")
  
  Select uMsg 
      
    ; Select window message
    Case #WM_CLOSE
      
      RemoveProp_(hwnd, "oldProc")
 
    Case #WM_DROPFILES                                                                        ; Files were dropped
      
      *DroppedFilesArea = wParam                                                              ; Pointer to the structure that contains the dropped files
      NumberOfCharactersDropped.i = DragQueryFile_(*DroppedFilesArea , $FFFFFFFF, #Null$, 0)  ; Return value is a count of the characters copied
      ClearList(drop())
      
      For index.i = 0 To NumberOfCharactersDropped.i - 1                                      ; Iterate through the character size returned
        
        file_path.s = Space(#MAX_PATH)                                                        ; Build a buffer big enough for the characters to be copied
        DragQueryFile_(*DroppedFilesArea , index, @file_path, #MAX_PATH)                       ; Get next filename character
        
        GetCursorPos_(p.POINT) 
        GetClientRect_(hwnd, rc.RECT)

        AddElement( drop() )
        drop()\file=file_path
        
      Next
      
      DragFinish_(*DroppedFilesArea)                                                                      ; Clear the filename buffer area
  EndSelect                                                                                              ; No more selections
  
  ProcedureReturn CallWindowProc_(*oldProc, hwnd, uMsg, wParam, lParam)
  
EndProcedure

ProcedureCDLL CL_SetDrop(name.s, state)
  
  agkHWND = FindWindow_(#Null, S(name))
  If IsWindow_(agkHWND)
    
    oldProc = GetWindowLongPtr_(agkHWND, #GWLP_WNDPROC)
    SetProp_(agkHWND, "oldProc", oldProc)
    SetWindowLongPtr_(agkHWND, #GWLP_WNDPROC, @agkWndProc())
    
    DragAcceptFiles_(agkHWND, state)
  Else
    MessageRequester("CL", "Could not find window with name "+S(name))
  EndIf
  ProcedureReturn 0
  
EndProcedure

ProcedureCDLL CL_DropCount()
  
  Protected Result.i
  Result = ListSize(drop())
  ProcedureReturn Result
  
EndProcedure

ProcedureCDLL CL_DropGet(Index.l)
  
  Protected Result.s=""
  If Index>=0 And Index < ListSize(drop())
    
    If SelectElement(drop(), Index)
      Result = drop()\file
      *Ptr = agkMakeString(Result)
      ProcedureReturn *Ptr    
    Else
      MessageRequester("", "Nothing at index")
    EndIf
    
  Else
    MessageRequester("", "Index ("+Str(Index)+")out of range")
  EndIf

  
EndProcedure

ProcedureCDLL CL_DropClear()
  If ListSize(drop()) > 0
    ClearList(drop())
  EndIf
EndProcedure



ProcedureCDLL CL_Count()
  
  Protected Result.i
  Result = CountProgramParameters()
  ProcedureReturn Result
  
EndProcedure

ProcedureCDLL CL_Get(Index.i)
  
  Protected Result.s
  Result = ProgramParameter(Index)
  *StringPtr = agkMakeString(Result)
  ProcedureReturn *StringPtr
  
EndProcedure

ProcedureCDLL CL_GetPathPart(file.s)
  
  Protected Result.s
  Result = GetPathPart(S(file))
  StringPtr = agkMakeString(Result)
  ProcedureReturn StringPtr
  
EndProcedure

ProcedureCDLL CL_GetFilePart(file.s, ext.b)
  
  Protected Result.s
  If ext
    Result = GetFilePart(S(file))
  Else
    Result = GetFilePart(S(file), #PB_FileSystem_NoExtension)
  EndIf
  
  StringPtr = agkMakeString(Result)
  ProcedureReturn StringPtr
  
EndProcedure

ProcedureCDLL CL_GetExtPart(file.s, dot.b)
  
  Protected Result.s
  If dot
    Result = "."+GetExtensionPart(S(file))
  Else
    Result = GetExtensionPart(S(file))
  EndIf
  StringPtr = agkMakeString(Result)
  ProcedureReturn StringPtr
  
EndProcedure

ProcedureCDLL CL_GetMIMEType(Extension.s)
  Extension = "." + S(Extension)
  hKey.l = 0
  KeyValue.s = Space(255)
  datasize.l = 255
  If RegOpenKeyEx_(#HKEY_CLASSES_ROOT, Extension, 0, #KEY_READ, @hKey)
      KeyValue = "application/octet-stream"
  Else
      If RegQueryValueEx_(hKey, "Content Type", 0, 0, @KeyValue, @datasize)
          KeyValue = "application/octet-stream"
      Else
          KeyValue = Left(KeyValue, datasize-1)
      EndIf
    RegCloseKey_(hKey)
  EndIf
  StringPtr = agkMakeString(KeyValue)
  ProcedureReturn StringPtr
EndProcedure
 

 


Commands.txt
+ Code Snippet
#CommandName,ReturnType,ParameterTypes,Windows,Linux,Mac,Android,iOS,Windows64
Count,I,0,CL_Count,0,0,0,0,CL_Count
Get,S,I,CL_Get,0,0,0,0,CL_Get
SetDropEnable,I,SI,CL_SetDrop,0,0,0,0,CL_SetDrop
DropCount,I,0,CL_DropCount,0,0,0,0,CL_DropCount
DropGet,S,I,CL_DropGet,0,0,0,0,CL_DropGet
DropClear,0,0,CL_DropClear,0,0,0,0,CL_DropClear
GetPathPart,S,S,CL_GetPathPart,0,0,0,0,CL_GetPathPart
GetFilePart,S,SI,CL_GetFilePart,0,0,0,0,CL_GetFilePart
GetExtPart,S,SI,CL_GetExtPart,0,0,0,0,CL_GetExtPart
GetMIMEType,S,S,CL_GetMIMEType,0,0,0,0,CL_GetMIMEType



Example
+ Code Snippet
#Import_Plugin CL as cl

SetErrorMode(2)
SetWindowTitle( "CL - Example" )
SetWindowSize( 1024, 768, 0 )
SetWindowAllowResize( 1 )
SetVirtualResolution( 1024, 768 )
SetOrientationAllowed( 1, 1, 1, 1 )
SetSyncRate( 30, 0 )
SetScissor( 0,0,0,0 )
UseNewDefaultFonts( 1 )



Function LoadSpriteFromImage(image as string, x, y)

	if GetFileExists(image)
		img = LoadImage(image)
		spr=CreateSprite(img)
		SetSpritePosition(spr, x, y)
	endif
	
EndFunction

// Enable file drop on the window
cl.SetDrop("CL - Example", 1)

// Command Line
if cl.Count()>0
	for i=0 to cl.Count()-1
		LoadSpriteFromImage( "raw:"+cl.Get(i), 10*i, 10*i)
    next
endif
	
do
	mouse_x = GetRawMouseX()
	mouse_y = GetRawMouseY()
	
	// File Drop
	if cl.DropCount()>0
		for i=0 to cl.DropCount()-1
			LoadSpriteFromImage( "raw:"+cl.DropGet(i) , mouse_x, mouse_y)
		next
		cl.DropClear() // important, clear the list or this will repeat
	endif
	
    Sync()
loop


EDIT: I just change the code above because it was not working on 64bit, it seems 64bit is a lot more fussy about argument typing, I changed some int;s to longs and removed typed info from pointer arguments and it works as expected on both 32 and 64 bit ... something learned today!
Posted: 28th Jul 2021 3:35
is there any way to embed AGK.Lib directly into Purebasic? Imagine the power that would give AppGameKit xD
Posted: 28th Jul 2021 6:19
If the transpiler Version of PB works like my language Cerberus X, which I assume, then it should be possible.
Posted: 28th Jul 2021 16:02
I have looed at it, and failed!!

The AppGameKit lib would need to be recompiled as DLL with exported functions, and I probably could muster the skill level needed unfortunately Visual Studio runs like a snail on my system it takes an hour to compile the lib to test the slightest code change so mustering the patience to see the task through it not going to happen, I have made a few local changes to my AppGameKit and to be honest the time it takes I could just write a PB plugin and have lunch!!

I looked at AGKSharp (AGKWrapper.dll) with DLL Export Viewer and most of the functions look wrappable but there are a few commands (format) I don't quite understand, I think they might be internal, but the dll is outdated anyway.



I would love to have AppGameKit ported to a PureBasic lib or dll but the framework will need completely rewritten, or wrapped?
Posted: 28th Jul 2021 23:34
A while ago, I went to fellow community member Adam Biser, asking if he could convert the plugin he made AGKPython, to .DLL that I could use with PUREBASIC

thanks to his genius, he managed to make a simple example " use the Message() command, to display a MessageBox, and apparently it worked, I'll share it here :

Adam's mail :
"


Note : Download files :
https://www.soqueto.com/cdn/agk/agkpb.zip
Posted: 29th Jul 2021 1:58
Impressive!

It compiles and runs out the box after setting #AGK_Base_Path, recreating the C++ template is not much of task just dupe Core.cpp and edit for PB, its all the same function calls if I go the API route or I might even be able to render AppGameKit in a PB OpenGL gadget which would be very handy for multiple instances running in threads (think Godot type editor, multiple 3D and 2D tabs)

I'll have to study the symbols.txt and see if I can scan it and pull the function set with a tool, I ripped the plugin function set with this method, scan the header and build the include file automatically, and sort any errors after, its far quicker than coding each function one by one.

One step closer to having my perfect engine, AppGameKit function set in PureBasic, my personal utopia! lol

Thank you SkinK4ir3 and Adam Biser.
Posted: 29th Jul 2021 16:28
OK, I don't think this is the solution, I started coding a framework with this and immediately hit a brick wall, some functions do not seem to be exposed by the static lib, (InitGraphics) for one or at least there is no symbol for it in the symbols.txt and also reading over at the PB forums it seems that static libs for PB must be compiled with the same version and with the same toolchain ... have you seen the Wiki on this ... not something I consider fun!! lol

So its going to need to be wrapped, see you in a couple of weeks, I'm off to my man cave with a large supply of caffeine!!
Posted: 30th Jul 2021 2:54
@PartTimeCoder: First, let me say that I'm not very familiar with PureBasic. What I did was just a test case to show it was possible.
What InitGraphics are you meaning? I'm not seeing it defined in the interpreter project.
Posted: 30th Jul 2021 3:54
agk::InitGraphics is called In Core.cpp inside tWinMain function at the bottom of the file along with a few other setup functions that also can not find in the symbols.txt file.

The Message function is a direct wrap of MessageBoxA so at a guess I assume this is why it can be called through your framework, I also imported a handful of other functions and although there was no linker error I am unable to test them as I can not Init the window

I was talking to someone at the PB forums and he gave me a few tips for compiling for PB but I don't know if the AppGameKit lib will like the settings as it needs to be compiled as C, this will also prevent the name mangling on the function names and (hopefully) solve the issue of importing functions but I got that xAudio2 issue going on here I need to solve first, downloaded a new repo to sync it with Git and its now refusing to compile!
Posted: 30th Jul 2021 4:14
Still not seeing InitGraphics. I see
+ Code Snippet
agk::InitGL( (void*) hWnd );

Is that what you mean?

EDIT: If so then maybe this?
InitGL(hwnd.i) As "?InitGL@agk@AGK@@SAXPAX@Z"

Guessing on the parameter type.

EDIT 2:
OH! Now I see. They changed it to InitGraphics for Studio. In Classic it's InitGL. The symbols are for Classic, not Studio.
Symbols for studio attached.

Command to get symbols:
"C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30037\bin\Hostx86\x86\dumpbin" /symbols AGKWindows.lib > symbols.txt
This depends on VS2019 community edition being installed. "14.29.30037" may differ.