Jump to content
The simFlight Network Forums

PMDG NGX SDK


Recommended Posts

  • Replies 157
  • Created
  • Last Reply

Top Posters In This Topic

Hi Pete.

I've got a first test version of the dll ready (ngxSDK2Lua.dll), with just a dummy function "testNGX" that takes a parameter and returns it plus 1

Copied the ngxSDK2Lua.dll to modules/lua (modules/dll didn't seem to work) and called the function from a lua script in the modules folder:


require "ngxSDK2Lua"
a123 = ngxSDK2Lua.ngxTest(2)
ipc.log("Result ... " .. a123)
[/CODE]

I get this error:

[CODE]
LUA Error: error loading module 'ngxSDK2Lua' from file 'C:\Program Files (x86)\Microsoft Games\Microsoft Flight Simulator X\modules\lua\ngxSDK2Lua.dll':
The specified procedure could not be found
[/CODE]

So I tried with the luacom script posted here (following the instructions there) and I get the same error.

[CODE]
LUA Error: error loading module 'luacom' from file 'C:\Program Files (x86)\Microsoft Games\Microsoft Flight Simulator X\modules\lua\luacom.dll':
The specified module could not be found
[/CODE]

Any idea what's going on please?

EDIT: the error raises in the require statement

Link to comment
Share on other sites

I've got a first test version of the dll ready (ngxSDK2Lua.dll), with just a dummy function "testNGX" that takes a parameter and returns it plus 1

Copied the ngxSDK2Lua.dll to modules/lua (modules/dll didn't seem to work) and called the function from a lua script in the modules folder

The modules/dll optin was quite recently added. Is your FSUIPC up to date? (4.80 or later).

I get this error:


LUA Error: error loading module 'ngxSDK2Lua' from file 'C:\Program Files (x86)\Microsoft Games\Microsoft Flight Simulator X\modules\lua\ngxSDK2Lua.dll':
The specified procedure could not be found
[/CODE]

So I tried with the luacom script posted here (following the instructions there) and I get the same error.

[CODE]
LUA Error: error loading module 'luacom' from file 'C:\Program Files (x86)\Microsoft Games\Microsoft Flight Simulator X\modules\lua\luacom.dll':
The specified module could not be found
[/CODE]

Well, they are certainly NOT the same error. In the first one the module was obviously found and in the second one it wasn't.

In your one, it seems the procedure you are calling within the module hasn't been added. I think you need to register each contained function using luaL_register. At least I certainly do in FSUIPC. For example, for my Logic library I do this:

[CODE]
static const luaL_Reg logiclib[] =
{ {"Or", logic_or},
{"And", logic_and},
{"Nor", logic_nor},
{"Nand", logic_nand},
{"Xor", logic_xor},
{"Shl", logic_shl},
{"Shr", logic_shr},
{"Not", logic_not},
{NULL, NULL}
};

luaL_register(L, "logic", logiclib);

[/CODE]

What the equivalent would be for external libraries would be I'm afraid i don't know.

Pete

Link to comment
Share on other sites


#define NGX_VERSION "ngxSDK2Lua 0.1 2012-03-07"
#define NGX_AUTHORS "Darío Iriberri"
#ifdef __cplusplus
extern "C"
{
#include "include\lua.h"
#include "include\lauxlib.h"
}
#endif
extern "C" int my_ngxTest(lua_State *L)
{
return 102;
}
static int luaopen_ngxSDK2Lua(lua_State *L){
static const luaL_Reg asd [] = {
{"ngxTest", my_ngxTest},
{NULL,NULL}
};
luaL_register(L,"ngxSDK2Lua", asd);
return 101;
}
[/CODE]

This is what I've got right now.

The luaopen_ngxSDK2Lua function is supposed to be called when the require clause is executed in Lua, registering the function (at least that's what someone posted @ stckoverflow), but apparently that's not what's happening. I still get the same error message.

I'm looking at Luabind and other wrapper libs, but at this point I think this is all way over my head

Link to comment
Share on other sites

...at this point I think this is all way over my head

I think you are nearly there, but you are missing at least one important thing about C functions called by Lua.

In this code

extern "C" int my_ngxTest(lua_State *L)
{
return 102;
}
static int luaopen_ngxSDK2Lua(lua_State *L){
static const luaL_Reg asd [] = {
{"ngxTest", my_ngxTest},
{NULL,NULL}
};
luaL_register(L,"ngxSDK2Lua", asd);
return 101;
}[/CODE]

There are four things wrong.

First there's certainly no need to declare the actual contained library rourtines "extern "C"". In fact they can be "static int ..." declared, because their address is NOT exported in C fashion but by the list you return from the luaopen ... call.

Second, the value returned by any routine called by Lua is simply the number of parameters being returned. You are saying above that "my_ngxTest" returns 102 parameters. For that to be correct you have to have pushed those 102 parameters on the stack (using one or more of the assorted lua_push.... functions).

If you merely want it to return the value 102 you'd do this:

[CODE]
lua_pushnumber(L, 102);
return 1;[/CODE]

Third, it is your luaopen_ngxSDK2Lua function which needs exporting from the DLL so that it can be called by Lua as the function it wants to call to get the list of contained library functions. By not exporting this, you are getting the error you see. It can't find the luaopon_... function it needs to start things off. In fact you declared it "static" which is the direct opposite of exporting it! To export it declare it like this:

[CODE]
EXTERN_C __declspec(dllexport) int luaopen_ngxSDK2Lua(lua_State *L){
...[/CODE]

Fourth, the same consideration for the return from the luaopen applies as it does to the library function you want to add. In this case "luaL_register" has pushed the table of library functions onto the stack, so there's already the one item, all that this function should return, on the stack -- so you then just "return 1" to say so.

Regards

Pete

Link to comment
Share on other sites

extern "C" {
#include "include\lua.h"
#include "include\lauxlib.h"
#include "include\lualib.h"
}

static int my_ngxTest(lua_State *L)
{
return 1;
}

extern "C" __declspec(dllexport) int luaopen_ngxSDK2Lua(lua_State *L){
static const luaL_Reg asd [] = {
{"ngxTest", my_ngxTest},
{NULL,NULL}
};
luaL_register(L,"ngxSDK2Lua", asd);
return 1;
}[/CODE]

The error I get now:

[CODE]
1> Creating library E:\Projects\ngxSDK2Lua\Release\ngxSDK2Lua.lib and object E:\Projects\ngxSDK2Lua\Release\ngxSDK2Lua.exp
1>ngxSDK2Lua.obj : error LNK2001: unresolved external symbol _luaL_register
1>E:\Projects\ngxSDK2Lua\Release\ngxSDK2Lua.dll : fatal error LNK1120: 1 unresolved externals
[/CODE]

do I need to link my code to some dll (lua51.dll maybe?) in Visual Studio?

Link to comment
Share on other sites


static int my_ngxTest(lua_State *L)
{
return 1;
}[/CODE]

You still need to push the parameter being returned here, or change the returned value to 0 to show there is no result.

So I guess I need to link my code to the Lua binaries (lua51.dll?) in Visual Studio so that the compiler knows where the implementations are?

I guess I need to figure out where in the project properties the relevant paths should be set

Well, yes, you can use the DLL, but it would be nicer if you could usie a static library (lib) rather than a DLL, built into your DLL. Much easier for the user. I don't know if the Lua website fearures a static bindable version -- if not you could always simply compile the needed source into your DLL. You wouldn't need all of the modules of course, probably just the base, maybe some other essential modules. It depends on which Lua procedures you end up needing to call upon. So far you are only using luaL_register., so you probably need lauxlib.c and lbaselib.c.

Regards

Pete

Link to comment
Share on other sites

You still need to push the parameter being returned here, or change the returned value to 0 to show there is no result.

Well, yes, you can use the DLL, but it would be nicer if you could usie a static library (lib) rather than a DLL, built into your DLL. Much easier for the user. I don't know if the Lua website fearures a static bindable version -- if not you could always simply compile the needed source into your DLL. You wouldn't need all of the modules of course, probably just the base, maybe some other essential modules. It depends on which Lua procedures you end up needing to call upon. So far you are only using luaL_register., so you probably need lauxlib.c and lbaselib.c.

Regards

Pete

Ok, I added the Lua souces to my project and will be compiling them myself then. I picked Lua 5.1.5 since it's the last 5.1 revision issued.

Link to comment
Share on other sites

Success :)

Time to figure out how to pass data thorugh the stack an call functions from Lua

EDIT: it's working already. Pushing the result to the top of the stack and then retrieving the result worked just fine

require "ngxSDK2Lua"

a123 = ngxSDK2Lua.ngxTest()

ipc.log("Result ... " .. a123)

I'll see if I can pass some parameters to the function

Link to comment
Share on other sites

...now off to learn some Simconnect

You don't need much of it. the example in the 737NGX SDK is enough, though i don't like their method of looping calling the dispatcher. That's not needed if you use a callback and defined Windows message.

Regards

Pete

Link to comment
Share on other sites

I'm going to do only the read part. We already have all the tools to send stuff with Lvars and ipc.control.

yeah, I was also planning on calling the SimConnect_CallDispatch only when there's a read request instead of that loop.

Question is if I implement a function for each and every switch/light/knob, it will need to retrieve the entire "struct PMDG_NGX_Data" every time. Doesn't look like a very efficient solution.

Link to comment
Share on other sites

I'm going to do only the read part. We already have all the tools to send stuff with Lvars and ipc.control.

yeah, I was also planning on calling the SimConnect_CallDispatch only when there's a read request instead of that loop.

No, you misunderstand how Simconnect works I think. I's a subscribe-and-wait system. You tell it what you want to know about then wait for it to supply data as and when it changes. You save the data to supply to your clients when they ask for it.

it will need to retrieve the entire "struct PMDG_NGX_Data" every time. Doesn't look like a very efficient solution.

It will only notify you when something changes.

The method I recommend is to use Simconnect_Open with your message-only (HWND_MESSAGE class) Window handle as the 3rd parameter and a user message number defined by you as the 4th parameter. Then you call "CallDispatch" when you receive that message. This is bay far the most efficient method.

Regards

Pete

Link to comment
Share on other sites

So, I'm already connecting to the NGX.

I figured if this is some event driven implementation, I don't really need to register any C++ functions, but instead be able to call some custom Lua functions once some event is triggered.

And I already got to print the state of the AFT LEFT FUEL PUMP in Lua... right after that FSX crashed to desktop, haha. I guess you can't have it all :P

So far I'm still using the implementation in the example provided by PMDG in their PMDG_NGX_connectionTest.cpp, but it's looking great already

Link to comment
Share on other sites

And I already got to print the state of the AFT LEFT FUEL PUMP in Lua... right after that FSX crashed to desktop, haha. I guess you can't have it all :P

I'd be interested to know the details of that crash, as I wouldn't want it to be anything to do with FSUIPC. Could you provide the details from the Windows error log please? Also the version number of FSUIPC.

Regards

Pete

Link to comment
Share on other sites


fsx.exe
10.0.61637.0
46fadb14
ngxSDK2Lua.dll
0.0.0.0
4f5b700a
c0000005
000226e0
1af0
01ccfed102b9f7ef
C:\Program Files (x86)\Microsoft Games\Microsoft Flight Simulator X\fsx.exe
C:\Program Files (x86)\Microsoft Games\Microsoft Flight Simulator X\modules\lua\ngxSDK2Lua.dll
7871b4cb-6ac4-11e1-8c55-e556d68d0a19
[/CODE]

Doesn't look to me like it has anything to do with FSUIPC. It's crashing right after calling the lua callback function. Maybe it's not returning to the dll execution?

I'm thinking maybe spawning a separate thread to make the callbacks

Here's my current implementation:

1.- Initialization Lua file. Registers the init function and call it with the lua callback path:

[CODE]
require "ngxSDK2Lua"

ipc.log("In!!!!!!!!!!!!!!!!!!!!!!!!!!!")
response = ngxSDK2Lua.ngxSDKInit("C:\\Program Files (x86)\\Microsoft Games\\Microsoft Flight Simulator X\\Modules\\lua\\callback.lua")
ipc.log("Result ... " .. response)
[/CODE]

2.- ngxSDK2Lua.cpp

[CODE]
// ngxSDKtoLua.cpp : Defines the exported functions for the DLL application.
//
#define NGX_VERSION "ngxSDK2Lua 0.1 2012-03-07"
#define NGX_AUTHORS "Darío Iriberri"
#include <windows.h>
#include <tchar.h>
#include <stdio.h>
#include <strsafe.h>
#include <string>
#include <iostream>
extern "C" {
#include "lua-5.1.5\src\lua.h"
#include "lua-5.1.5\src\lauxlib.h"
#include "lua-5.1.5\src\lualib.h"
}
#include "SimConnect.h"
#include "PMDG_NGX_SDK.h"
#include "LuaCall.h"
int quit = 0;
HANDLE hSimConnect = NULL;
bool AircraftRunning = false;
PMDG_NGX_Control Control;
bool NGX_FuelPumpLAftLight = true;
bool NGX_TaxiLightSwitch = false;
bool NGX_LogoLightSwitch = false;
lua_State* LuaGlobal;
const char* luaPath;
static enum DATA_REQUEST_ID {
DATA_REQUEST,
CONTROL_REQUEST,
AIR_PATH_REQUEST
};
// This function is called when NGX data changes
void ProcessNGXData (PMDG_NGX_Data *pS)
{
// Open Lua File
if(luaL_loadfile(LuaGlobal, luaPath) || lua_pcall(LuaGlobal, 0, 0, 0)) { // open Lua callback file
throw std::string(std::string(lua_tostring(LuaGlobal, -1)));
}
// test the data access:
// get the state of an annunciator light and display it
if (pS->FUEL_annunLOWPRESS_Aft[0] != NGX_FuelPumpLAftLight)
{
NGX_FuelPumpLAftLight = pS->FUEL_annunLOWPRESS_Aft[0];
if (NGX_FuelPumpLAftLight) {
// Lua callback function
std::cout << LuaCall<std::string, std::string>(LuaGlobal, "luaCallback").call("LOW PRESS LIGHT: [ON]") << std::endl;
}
}
}
void CALLBACK dispatchProc(SIMCONNECT_RECV* pData, DWORD cbData, void *pContext)
{
switch(pData->dwID)
{
case SIMCONNECT_RECV_ID_CLIENT_DATA: // Receive and process the NGX data block
{
SIMCONNECT_RECV_CLIENT_DATA *pObjData = (SIMCONNECT_RECV_CLIENT_DATA*)pData;
switch(pObjData->dwRequestID)
{
case DATA_REQUEST:
{
PMDG_NGX_Data *pS = (PMDG_NGX_Data*)&pObjData->dwData;
ProcessNGXData(pS);
break;
}
}
break;
}
case SIMCONNECT_RECV_ID_QUIT:
{
quit = 1;
break;
}
default:
printf("\nReceived:%d",pData->dwID);
break;
}
}
void initCommunication()
{
HRESULT hr;
if (SUCCEEDED(SimConnect_Open(&hSimConnect, "PMDG NGX Test", NULL, 0, 0, 0)))
{
printf("\nConnected to Flight Simulator!");

// 1) Set up data connection
// Associate an ID with the PMDG data area name
hr = SimConnect_MapClientDataNameToID (hSimConnect, PMDG_NGX_DATA_NAME, PMDG_NGX_DATA_ID);
// Define the data area structure - this is a required step
hr = SimConnect_AddToClientDataDefinition (hSimConnect, PMDG_NGX_DATA_DEFINITION, 0, sizeof(PMDG_NGX_Data), 0, 0);
// Sign up for notification of data change.
// SIMCONNECT_CLIENT_DATA_REQUEST_FLAG_CHANGED flag asks for the data to be sent only when some of the data is changed.
hr = SimConnect_RequestClientData(hSimConnect, PMDG_NGX_DATA_ID, DATA_REQUEST, PMDG_NGX_DATA_DEFINITION,
SIMCONNECT_CLIENT_DATA_PERIOD_ON_SET, SIMCONNECT_CLIENT_DATA_REQUEST_FLAG_CHANGED, 0, 0, 0);

// 2) Set up control connection
// First method: control data area
Control.Event = 0;
Control.Parameter = 0;
// Associate an ID with the PMDG control area name
hr = SimConnect_MapClientDataNameToID (hSimConnect, PMDG_NGX_CONTROL_NAME, PMDG_NGX_CONTROL_ID);
// Define the control area structure - this is a required step
hr = SimConnect_AddToClientDataDefinition (hSimConnect, PMDG_NGX_CONTROL_DEFINITION, 0, sizeof(PMDG_NGX_Control), 0, 0);

// Sign up for notification of control change.
hr = SimConnect_RequestClientData(hSimConnect, PMDG_NGX_CONTROL_ID, CONTROL_REQUEST, PMDG_NGX_CONTROL_DEFINITION,
SIMCONNECT_CLIENT_DATA_PERIOD_ON_SET, SIMCONNECT_CLIENT_DATA_REQUEST_FLAG_CHANGED, 0, 0, 0);
// 3) Request current aircraft .air file path
hr = SimConnect_RequestSystemState(hSimConnect, AIR_PATH_REQUEST, "AircraftLoaded");

// 5) Main loop
while( quit == 0 )
{
// receive and process the NGX data
SimConnect_CallDispatch(hSimConnect, dispatchProc, NULL);
Sleep(1);
}
hr = SimConnect_Close(hSimConnect);
}
else
printf("\nUnable to connect!\n");
}
////////////////////////////////
// Registering Lua functions. //
////////////////////////////////
static int cpp_ngxSDKInit(lua_State *L)
{
luaPath = luaL_checklstring(L, 1, NULL);

LuaGlobal = L;
initCommunication();
std::string s(luaPath);
s = "NGX SDK CLient Init OK.... Path = " + s;
char* c = new char[s.length() + 1];
strcpy(c, s.c_str());

lua_pushstring(L, c);
return 1;
}
extern "C" __declspec(dllexport) int luaopen_ngxSDK2Lua(lua_State *L){
static const luaL_Reg asd [] = {
{"ngxSDKInit", cpp_ngxSDKInit},
{NULL,NULL}
};
luaL_register(L,"ngxSDK2Lua", asd);
return 0;
}
[/CODE]

3.- callback.lua. Simply prints the result string in the FSUIPC log

[CODE]
function luaCallback(value)
ipc.log("Lua Callback: " .. value)
end
[/CODE]

Link to comment
Share on other sites

The debugger in VS said:

Unhandled exception at 0x76f415ee in fsx.exe: 0xC0000005: Access violation reading location 0x00000000.

I have no idea how to debug in this environment

EDIT: the callstack ends at > ntdll.dll!76f415ee()

Link to comment
Share on other sites


fsx.exe
10.0.61637.0
46fadb14
ngxSDK2Lua.dll
0.0.0.0
4f5b700a
c0000005
000226e0
[/CODE]

Doesn't look to me like it has anything to do with FSUIPC. It's crashing right after calling the lua callback function. Maybe it's not returning to the dll execution?

It's crashing in your DLL with an access error at offset 0x226E0 in the DLL. That should enable you to identify exactly what value is wrong.

I'm thinking maybe spawning a separate thread to make the callbacks

I browsed your code and I must admit I'm completely mystified as to what it is you are doing now. I thought you were simply making a DLL which contained a number of C functions to supply requested NGX data to any Lua program. Why is the DLL itself executing yet another Lua program? What's all this about callbacks?

If you are trying to get around the problem of the loop calling simConnect's dispatcher, then that should probably be in its own thread.. In fact the DLL could create a thread which has the sole job of keeping a copy of the PMDG data up to date, in its own memory. The main DLL thread would provide the functions called by Lua to retrieve reqested data from that copy. This emulates the way FSUIPC works -- it keeps the data in the offsets up to date with dispatches from simConnect, and supplies those as requested to client programs.

If you were to use the Windows message method I suggested, it would still need to be done in a thread -- and I think the message loop needs to be in the same thread as the message window.

Regards

Pete

Link to comment
Share on other sites

I have no idea how to debug in this environment

The MS Visual Studio debugger can run FSX with your module. You just tell the debugger ot load FSX, copmile in Debug mode, put the DLL in the right place, and click the menu itsem to start debugging.

Ah, hang on. The PMDG NGX code checks for a debugger running and causes a crash, deliberately I think to stop hacking. you have to get FSX started first, with the NGX running, THEN use the "attach to process" option in the Debug menu. Then you are okay.

Pete

Link to comment
Share on other sites

It's crashing in your DLL with an access error at offset 0x226E0 in the DLL. That should enable you to identify exactly what value is wrong.

I browsed your code and I must admit I'm completely mystified as to what it is you are doing now. I thought you were simply making a DLL which contained a number of C functions to supply requested NGX data to any Lua program. Why is the DLL itself executing yet another Lua program? What's all this about callbacks?

If you are trying to get around the problem of the loop calling simConnect's dispatcher, then that should probably be in its own thread.. In fact the DLL could create a thread which has the sole job of keeping a copy of the PMDG data up to date, in its own memory. The main DLL thread would provide the functions called by Lua to retrieve reqested data from that copy. This emulates the way FSUIPC works -- it keeps the data in the offsets up to date with dispatches from simConnect, and supplies those as requested to client programs.

If you were to use the Windows message method I suggested, it would still need to be done in a thread -- and I think the message loop needs to be in the same thread as the message window.

Regards

Pete

Let me try to explain the idea behind this and feel free to suggest any changes.

1.- In ipc.ready I will do the


require "ngxSDK2Lua"
[/CODE]

this calls the "luaopen_ngxSDK2Lua" function and registers the function "ngxSDKInit"

then I call (again in ipc.ready)...

[CODE]
response = ngxSDK2Lua.ngxSDKInit("C:\\Program Files (x86)\\Microsoft Games\\Microsoft Flight Simulator X\\Modules\\lua\\callback.lua")
[/CODE]

...to store the path where the lua functions that implement the module are (and also initialize the SDK). Those are the Lua functions that will be called whenever the SDK detects a change.

So for example, now I have the function "luaCallback" that takes the string parameter "LOW PRESS LIGHT: [ON]" when the dispatcher detects that the LEFT_AFT_FUEL_PUMP light has lit

Implementing that "luaCallback" function in the "callback.lua" file to send a signal to some panel to lit up a light would be an example of how to use it.

So that's what changed, since the SDK is event triggered, I thought I could call the lua part when an event is triggered, instead of having Lua query the data.

The code to call those Lua functions is this (found it in the internet):

[CODE]
if(luaL_loadfile(LuaGlobal, luaPath) || lua_pcall(LuaGlobal, 0, 0, 0)) { // open Lua callback file
throw std::string(std::string(lua_tostring(LuaGlobal, -1)));
}
// Lua callback function
std::cout << LuaCall<std::string, std::string>(LuaGlobal, "luaCallback").call("LOW PRESS LIGHT: [ON]") << std::endl;

[/CODE]

Thanks for the suggestion on debugging, I'll see if I can get that to work

Link to comment
Share on other sites

Let me try to explain the idea behind this and feel free to suggest any changes.

Actually I did understand WHAT you were doing, just not WHY. It just seems rather complicated. Or maybe it's just me?

So that's what changed, since the SDK is event triggered, I thought I could call the lua part when an event is triggered, instead of having Lua query the data.

Yes, that's what I find so complicated. How is this going to work for a user who wants to populate all his hardware displays with indicators and values from the NGX data?

If you wanted to do it that way why not simply use a mechanism like my "event" system. Let the Lua program which is doing the "requiring" call an event function in your C DLL which provides it with the name of the function to be called when a specified value changes. You'd need to build a list of those. Then, at the end of that Lua program, instead of exiting back to FSUIPC, it would need call an "execute" function in your DLL which then processes the dispatches, in a loop as now.

This way you don't need to execute a separate Lua plug-in, it is nice and tidy all in one Lua and one DLL, both running in the one thread. The only trouble is that because the thread never exits back to FSUIPC, that same Lua can't use the FSUIPC event system. (But that applies to your current method too).

The other way is the way I proposed before, which is having a separate thread in the DLL which is maintaining the data copy with functions which supply values on request. The problem with that is how to detect that the Lua plug-in has terminated so that the separate thread can be terminated tidily.

I'm starting to wonder if it wouldn't be much tidier to forget the Lua way of doing it altogether, and just have an EXE program which gets the NGX data and posts them to assigned FSUIPC offsets, which can then be read by any Lua plug-ins, and other programs, even on a WideFS client. I would actually consider building this into FSUIPC (it is easy as it already has a working SimConnect interface), but I don't like some sections of PMDG's EULA. I'd need PMDG's blessing first. Maybe I should write to Mr. Randazzo.

Regards

Pete

Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×
×
  • Create New...

Important Information

By using this site, you agree to our Terms of Use. Guidelines Privacy Policy We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.