mbed HUD for Crysis

This is a variation of my FlightSimInstrument project, but this time using the USBHID interface from Crysis!

My aim is to display the player's current health and remaining ammunition on a Text LCD display. This will be done by writing a Crysis mod with a custom game DLL.

/media/uploads/mblokzijl/_scaled_img_0642.jpg

mbed + crysis in action (well, in the Sandbox 2 editor - the godmode came in handy while trying to take a picture ;-) )

mbed code

We'll be displaying two values, health & remaining ammo, so a fairly simple USB HID report will do:

main.cpp

union hid_report_t {
    uint8_t bytes[sizeof(int16_t)*2];
    struct {
        uint16_t health;
        uint16_t ammo;
    } data;
};

I'm using a union to make it both easy for USBHID to write stuff into the structure (you can simply pass it a pointer to the bytes array, and easy for us to read the health and remaining ammo values from it. Here's the full mbed code:

Import programCrysisHUD

This code will display two unsigned shorts (representing health & remaining ammo) on a TextLCD display. The mbed receives them via the USB HID interface.

The only "oddity" I encountered was that I needed to flip the bytes on the mbed side, even though both my Intel desktop and the mbed are little-endian. The __REV16() function did this just fine though, using an assembly instruction (see core_cmInstr.h).

PC code

I'm using Crysis 1 - You'll need Crysis patched to v1.2, the Crysis Mod SDK v1.2 and Visual Studio (any version above 2005 should be OK, and some people appear to have managed to compile game DLLs with free tools, too: some basic info). It's also worthwhile installing the Sandbox 2 editor (it's on the Crysis DVD), since it makes debugging a bit easier.

Unfortunately, the links on Crytek's website seem to be broken - hopefully they'll fix them soon - but other sites (fileplanet etc) still seem to have a copy. I luckily still had the Mod SDK installed on my HDD from long ago, though I'm not entirely sure where I got it from. Anyway, the installer's MD5 checksum is 436334c74e0bb7364c6fc3724be9d50a, and the SHA1 sum is 3e09ba3e5018f8a071c64e012d4a3238ddcaa4fa, in case you want to cross-check :-)

Basic setup (if you get stuck anywhere, this video tutorial might be useful):

  1. Compile the HIDAPI project (see the USBHID C bindings cookbook page) - you'll need its files in a few steps time. I'll call HIDAPI's directory <hidapi dir>. Copy the <hidapi dir>\windows\Debug\hidapi.dll to <Crysis install dir>\Bin32\.
  2. Under <Crysis install dir>\Mods, there's a folder called CrysisMod - copy this, and rename it to mbedLcdMod
  3. Open mbedLcdMod\Code\GameDll.vcproj in Visual Studio; if necessary, follow the steps of the conversion wizard.
  4. Hit F6 (Build->Build solution), and check that everything compiles. If you're getting a compiler error and you're using VS2010, this post explains how to fix it.
  5. Open the project properties (Project->Properties), and set the Configuration to All configurations. Then, under Linker change Output File to $(OutDir)mbedLcdMod.dll. Under Linker->Input, add the hidapi.lib (it's in <hidapi dir>\windows\Debug) file to Additional Dependencies. Under C/C++, add <hidapi dir>\hidapi to Additional include directories.
  6. It's time for some code changes! Create a new class (Project->Add Class) called CTextLcd. Copy the source code from below into the TextLcd.h and TextLcd.cpp files.

TextLcd.h

#pragma once
#include <hidapi.h>

//the PC will need an extra byte for the reportID in the report
union hid_report_t {
    unsigned char bytes[sizeof(unsigned short)*2 +1];
    struct {
	unsigned char reportID;
        unsigned short health;
        unsigned short ammo;
    } data;
};

class CTextLcd
{
public:
	CTextLcd(void);
	~CTextLcd(void);
	void Init(void);
	void logDeviceInfo(void);
	void updateHealth(float h);
	void updateAmmo(int a);

private:
	//handle to mbed
	hid_device *m_handle;
	//the report we'll be sending to it
	hid_report_t m_report;

	//mbed USB vendor ID, product ID, Serial Number.
	const unsigned short mbed_vid, mbed_pid;
	wchar_t* mbed_SN;
	void sendReport(void);
};

TextLcd.cpp

#include "StdAfx.h"
#include "TextLcd.h"
#include <ISystem.h>

CTextLcd::CTextLcd(void) : mbed_vid(0x1234),
		mbed_pid(0x6), mbed_SN(NULL), m_handle(NULL)
{
	//initialise report - this will also set the report ID to 0.
	memset(m_report.bytes, 0x0, sizeof(m_report.bytes));
}

void CTextLcd::Init(void)
{
	if (m_handle)
		return;
	m_handle = hid_open(mbed_vid, mbed_pid, mbed_SN);
	if (!m_handle) {
		//for a production system, use proper exceptions.
		throw "unable to open device";
	}
}

CTextLcd::~CTextLcd(void)
{
	if (m_handle) {
		//close HID device
		hid_close(m_handle);
	}
	//Free static HIDAPI objects.
	hid_exit();
}

void CTextLcd::logDeviceInfo(void)
{
	if(!m_handle) {
		CryLogAlways("[MBED]: Could not log device info, since we don't have a handle to the device!");
		return;
	}
	const unsigned int max_str_len = 256;
	wchar_t wstr[max_str_len];
	int res;

	//this is just from the example, but useful so that we know we correctly connected to the mbed.

	// Read the Manufacturer String
	wstr[0] = 0x0000;
	res = hid_get_manufacturer_string(m_handle, wstr, max_str_len);
	if (res < 0)
		CryLogAlways("[MBED]: Unable to read manufacturer string");
	else
		CryLogAlways("[MBED]: Manufacturer String: %ls", wstr);

	// Read the Product String
	wstr[0] = 0x0000;
	res = hid_get_product_string(m_handle, wstr, max_str_len);
	if (res < 0)
		CryLogAlways("[MBED]: Unable to read product string");
	else
		CryLogAlways("[MBED]: Product String: %ls", wstr);

	// Read the Serial Number String
	wstr[0] = 0x0000;
	res = hid_get_serial_number_string(m_handle, wstr, max_str_len);
	if (res < 0)
		CryLogAlways("[MBED]: Unable to read serial number string");
	else
		CryLogAlways("[MBED]: Serial Number String: (%d) %ls", wstr[0], wstr);
}

void CTextLcd::updateHealth(float h)
{
	m_report.data.health = floor(h);
	sendReport();
}

void CTextLcd::updateAmmo(int a)
{
	if (a < 0)
		a = 0;
	m_report.data.ammo = (unsigned short)a;
	sendReport();
}

/*
 * send USB HID report to mbed Text LCD.
 */
void CTextLcd::sendReport(void)
{
	if (!m_handle)
		return;
	//it's important to use the .bytes, since the whole report structure might contain
	// some padding.
	int res = hid_write(m_handle, m_report.bytes, sizeof(m_report.bytes));
	if (res < 0)
		CryLogAlways("[MBED]: Could not send HID report to mbed: %ls", hid_error(m_handle));
}

Now that we've got a class that will communicate with our mbed, we just need to call it's functions from Crysis. Open the HUD\HUD.h file, and make the following changes:

HUD.h

/*near the other includes at the top, add: */
#include "TextLcd.h"

/*In the private section of class CHUD, add: */
CTextLcd* m_pTextLcd;

Now, open HUD.cpp, and make the following changes:

HUD.cpp

/*In the constructor of CHUD (CHUD::CHUD()), add the following towards the end: */

 	//mbed
	m_pTextLcd = NULL;

/*In the destructor CHUD::~CHUD(), add the following near the other SAFE_DELETEs: */

	SAFE_DELETE(m_pTextLcd);//dispose of mbed lcd

/*In the function bool CHUD::Init(), add the following to the end, just before 'return true': */

	//initialise mbed display.
	CryLogAlways("[MBED]: Initialising mbed...");
	try {
		m_pTextLcd = new CTextLcd();
		m_pTextLcd->Init();
		m_pTextLcd->logDeviceInfo();
	} catch(const char* err_msg) {
		CryLogAlways("[MBED]: Failed to initialise mbed: %s", err_msg);
	} catch(...) {
		CryLogAlways("[MBED]: Failed to initialise mbed: unknown exception");
	}

/* In void CHUD::UpdateHealth(), add the following into the "if(m_fHealth != fHealth || m_bFirstFrame)"-block, after the setHealth invocation: */

			//update mbed health display
			if (m_pTextLcd)
				m_pTextLcd->updateHealth(fHealth - 1.0f);//compensate for +1 above.

/* At the end of void CHUD::UpdatePlayerAmmo(), add the following:  */

	//update mbed
	if (m_pTextLcd)
		m_pTextLcd->updateAmmo(m_playerAmmo);

Hit Build->Build solution again. If all went well, it should compile.

Now, to test your mod, it's easiest to create a copy of either the Crysis or the Sandbox 2 Editor shortcut, right-click it and select properties, and add -mod mbedLcdMod to the "Target" field. Alternatively, here are some instructions on how to debug your Crysis C++ mods. Now, just load a level, and enjoy your extra HUD!

Where to go from here?

Here are just some ideas for making your own, custom HID device: - Try something similar for your favourite game/serious application - Add some buttons and make them "do stuff" in the game. - Add LEDs, a speaker or other "notification device" to alert you of e.g. helicopters in the area. - your creativity is the only limit (and well, what SDKs / mod kits allow you to do) :)

Thanks for reading, hope you enjoyed it and feel free to post any feedback in the comments!

PS: I'll try to make a binary of the mod available in the near future.


7 comments on mbed HUD for Crysis:

12 Dec 2011

That looks really cool, I've just started building a PC monitoring system that'll display CPU/GPU temperatures on a touchscreen and I'm currently planning on just using a serial connection over the USB port to pass the data, but this looks like a really nice alternative.

Matt

13 Dec 2011

Hi Matt, Thanks for your comment, that sounds cool! Yeah, I think if you know if you've got well "structured" data (if for example you know you'll always be sending the CPU and GPU temperatures and maybe a character for the units) then the USB HID interface will work quite nicely. I especially liked the fact that I didn't need additional drivers on Windows, and compared to the serial interface you also don't need to know which port you're using, since you can just find the device by it's IDs.

For sending endless strings I might go with the serial interface though.. :)

Good luck!

20 Jan 2012

I've just started implementing this, I've only looked at the Mbed portion so far, but your code looks easy to understand :-) (and I've now learnt about unions) I've got a question about a couple of lines:

00051 health = _REV16(pHidData->data.health);

00052 ammo = _REV16(pHidData->data.ammo);

I'm just passing 8 bit ints (uint8_t), do I need to use a function like REV16? You mention that the bytes are in the wrong order, but I only have one byte... I haven't been able to find any info about REV16, do you have any links?

Cheers,

Matt

20 Jan 2012

Hi Matt, Cool, I'm glad it's understandable!

When using 1 byte datatypes (uint8_t), you don't need to do any byte order changes; this also counts for char-strings. I found that the USBHID example e.g. uses 1 byte data.

The only links I have are these two REV@Arm information center and core_cmInstr.h.

Michiel

21 Jan 2012

Hi Michiel,

Great, that makes my code even simpler :-)

I'll have a go at figuring out the windows client end today hopefully.

Cheers,

Matt

24 Jan 2012

Well, although I ended up with a massive dislike for C++'s string data types (my code seems to use most of them) I actually got it working.

My C++ skills are pretty minimal (I've done a fair amount of VB and PHP) so I was missing some fairly fundamental knowledge before I started this, like how to make a simple class. The fact that you included a simple C++ class helped me immensely and I ended up re-writing most of my existing code to make it much easier to read.

I'll post a notebook page up about it when I get a chance, but I just wanted to say thanks again for this write-up, without it I definitely wouldn't be using USBHID in my project.

Cheers,

Matt

25 Jan 2012

Hi Matt,

I'm glad to hear you got your PC monitoring system to work! It sounds like you learned a lot while working on it :-)

Thanks for the feedback, I'm glad I was able to help, and I look forward to reading your writeup! I just hope using USBHID didn't end up being more trouble than it was worth, but it's not always easy to judge beforehand.

Regards,

Michiel

Please log in to post comments.